При первом знакомстве со SwiftUI кажется, что фреймворк предлагает избыточное количество механизмов для передачи данных: @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject и другие. Многие затрудняются выбрать подходящий инструмент для конкретной ситуации. Похоже на выбор между молотком и отвёрткой, когда вы ещё не знаете, с гвоздём или шурупом вам предстоит иметь дело. Эта проблема усугубляется тем, что выбор механизма передачи данных нельзя рассматривать изолированно от архитектуры приложения. Как говорил мой коллега в одном из проектов: "Выбирать PropertyWrapper для SwiftUI без понимания архитектуры — всё равно что выбирать коробку передач для машины, которую ещё не спроектировали".
Центральное понятие для понимания потоков данных в SwiftUI — концепция "единого источника истины" (Single Source of Truth). Она означает, что каждый фрагмент данных в приложении должен существовать в единственном экземпляре, а все остальные части интерфейса должны получать доступ к этому экземпляру, а не создавать копии.
Когда я начинал работать со SwiftUI, я часто допускал ошибку, создавая копии данных в разных представлениях, что приводило к рассинхронизации и трудноотслеживаемым багам. Ключевой урок, который я вынес: SwiftUI автоматически обновляет интерфейс при изменении источника истины, но вам нужно правильно организовать эти источники. Интересно отметить, что большинство проблем с потоками данных возникает из-за неверного понимания жизненного цикла SwiftUI. Представления в SwiftUI — не объекты, а описания того, что должно отображаться на экране. Они регулярно пересоздаются при изменении данных, и это фундаментальное отличие от UIKit требует иного мышления при проектировании архитектуры приложения. Выбор правильного механизма передачи данных зависит от нескольких факторов:
1. Где данные создаются и где используются.
2. Сколько представлений должны иметь доступ к этим данным.
3. Нужно ли этим представлениям изменять данные или только читать их.
4. Насколько эти данные долговечны в рамках жизненного цикла приложения.
При разработке сложного приложения для крупного банка я обнаружил, что иногда приходится комбинировать несколько механизмов передачи данных. Например, мы использовали @EnvironmentObject для глобальных настроек, @StateObject для экранных моделей и @Binding для передачи изменяемых значений между родительскими и дочерними представлениями.
Основные подходы к передаче данных
Поток данных в SwiftUI — это целая экосистема инструментов, каждый из которых имеет своё назначение. Чтобы разобраться, когда какой механизм применять, давайте рассмотрим основные подходы к передаче данных между представлениями.
@State для локального состояния
Самый базовый механизм управления данными в SwiftUI — проперти враппер @State. Его основная задача — хранить локальное, недолговечное состояние интерфейса, которое принадлежит только одному представлению. Когда значение свойства, помеченного @State, изменяется, SwiftUI автоматически перерисовывает представление.
| Swift | 1
2
3
4
5
6
7
8
| struct ToggleView: View {
@State private var isToggled = false
var body: some View {
Toggle("Включить функцию", isOn: $isToggled)
.padding()
}
} |
|
Есть важный нюанс: представления в SwiftUI — это структуры, а не классы. Они постоянно пересоздаются при обновлении UI. Без @State значение isToggled сбрасывалось бы при каждом обновлении. Но свойства с @State хранятся отдельно от структуры представления, в специальной памяти, управляемой SwiftUI. Это позволяет им сохранять значение между перерисовками.
Я помню свою первую ошибку со @State — попытку использовать его для хранения сложных данных, которые нужны многим представлениям. Результат? Копии данных в разных местах приложения рассинхронизировались. Это подводит нас к золотому правилу: @State должен использоваться только для данных, которые:- Нужны только внутри одного представления.
- Не связаны с бизнес-логикой.
- Управляют только отображением UI.
@Binding для двунаправленной связи
Когда нам нужно, чтобы дочернее представление могло изменять состояние, принадлежащее родительскому, на помощь приходит @Binding. Это не самостоятельный источник данных, а "ссылка" на данные, хранящиеся где-то еще (обычно в @State родительского представления).
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| struct ParentView: View {
@State private var text = "Привет"
var body: some View {
VStack {
Text(text).padding()
ChildView(text: $text)
}
}
}
struct ChildView: View {
@Binding var text: String
var body: some View {
TextField("Введите текст", text: $text)
.padding()
}
} |
|
Обратите внимание на $ перед переменной text в ParentView — это оператор проекции, который превращает свойство в биндинг. Когда ChildView изменяет text, это изменение отражается в ParentView, и наоборот.
При работе с биндингами есть одна хитрость, которую я открыл для себя в процессе разработки: вы можете создавать биндинги с трансформацией значений "на лету". Это полезно, когда форматы данных родителя и ребенка различаются:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| struct SliderView: View {
@Binding var percentage: Double
var body: some View {
VStack {
Text("\(Int(percentage * 100))%")
Slider(value: $percentage)
}
}
}
struct ParentView: View {
@State private var value = 0.0
var body: some View {
VStack {
SliderView(percentage: $value)
// Создание биндинга с трансформацией
Toggle("Больше половины", isOn: Binding(
get: { value > 0.5 },
set: { newValue in value = newValue ? 0.75 : 0.25 }
))
}
}
} |
|
@StateObject и @ObservableObject для глобального состояния
Когда данные становятся сложнее или должны быть доступны многим представлениям, @State и @Binding становятся неудобными. Именно тут в игру вступают @StateObject и @ObservableObject.
@ObservableObject — это протокол, которому должен соответствовать класс, чтобы SwiftUI мог отслеживать его изменения. @StateObject и @ObservedObject — проперти врапперы для хранения таких объектов в представлениях.
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| class UserSettings: ObservableObject {
@Published var username = "Гость"
@Published var isDarkModeEnabled = false
}
struct ProfileView: View {
@StateObject private var settings = UserSettings()
var body: some View {
VStack {
TextField("Имя пользователя", text: $settings.username)
Toggle("Темная тема", isOn: $settings.isDarkModeEnabled)
DetailView()
.environmentObject(settings)
}
.padding()
}
}
struct DetailView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
Text("Привет, \(settings.username)")
.padding()
}
} |
|
Ключевое различие между @StateObject и @ObservedObject:
@StateObject владеет объектом и отвечает за его жизненный цикл — используйте его там, где объект создается,
@ObservedObject просто наблюдает за объектом, созданным где-то ещё — используйте для доступа к уже существующему объекту.
С появлением новых технологий в iOS 17, Apple представила фреймворк Observation с макросом @Observable, который постепенно заменяет комбинацию ObservableObject и @Published. Если вы работаете с новыми проектами, стоит присмотреться к этому подходу:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @Observable
class UserSettings {
var username = "Гость"
var isDarkModeEnabled = false
}
struct ProfileView: View {
@State private var settings = UserSettings()
var body: some View {
VStack {
TextField("Имя пользователя", text: $settings.username)
Toggle("Темная тема", isOn: $settings.isDarkModeEnabled)
}
.padding()
}
} |
|
При работе над крупным приложением я столкнулся с распространённой ошибкой: создание слишком "тяжелых" наблюдаемых объектов, содержащих слишком много логики и данных. В итоге обновление одного маленького свойства часто вызывало перерисовку слишком большого количества представлений. Лучшая практика — разделять наблюдаемые объекты по принципу единой ответственности, чтобы обновления были точечными.
@EnvironmentObject для контекстных данных
Когда вам нужно передать данные глубоко в иерархию представлений, передача через цепочку инициализаторов становится громоздкой и создаёт ненужные зависимости между компонентами. Для таких ситуаций SwiftUI предлагает механизм @EnvironmentObject.
@EnvironmentObject позволяет сделать объект доступным для всего дерева представлений без явной передачи через каждый промежуточный уровень. Это похоже на глобальную переменную, но с ограниченной областью видимости и встроенным механизмом обновления UI.
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // В корневом представлении
struct RootView: View {
@StateObject private var userManager = UserManager()
var body: some View {
ContentView()
.environmentObject(userManager)
}
}
// В любом дочернем представлении - даже глубоко в иерархии
struct DeepChildView: View {
@EnvironmentObject var userManager: UserManager
var body: some View {
Text("Текущий пользователь: \(userManager.username)")
}
} |
|
Главное преимущество этого подхода — значительное уменьшение связанности кода. Представления не нуждаются в знании откуда взялись данные, они просто запрашивают их из окружения.
Помню забавный случай: я потратил час на отладку приложения, которое падало с непонятной ошибкой, пока не понял, что забыл добавить .environmentObject(...) для модально представленного экрана. Это распространённая проблема, потому что модальные представления создают новое окружение.
@Published свойства и их роль в обновлении интерфейса
Как SwiftUI узнаёт, когда нужно обновить интерфейс? Ключевым механизмом здесь является модификатор @Published, который вы можете добавить к свойствам объекта, соответствующего протоколу ObservableObject:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| class CartManager: ObservableObject {
@Published var items: [CartItem] = []
@Published var totalPrice: Double = 0.0
var itemCount: Int {
items.count
}
func addItem(_ item: CartItem) {
items.append(item)
totalPrice += item.price
}
} |
|
В этом примере при изменении массива items или значения totalPrice все представления, наблюдающие за CartManager, автоматически обновятся. При этом простое обращение к вычисляемому свойству itemCount не вызовет обновления, так как оно не помечено как @Published.
На практике я столкнулся с интересным кейсом: если вы модифицируете внутреннее содержимое массива или других коллекций, отмеченных @Published, SwiftUI может не заметить изменения. Для решения этой проблемы можно использовать паттерн "copy-and-reassign":
| Swift | 1
2
3
4
5
| func markItemAsPurchased(at index: Int) {
var updatedItems = items
updatedItems[index].isPurchased = true
items = updatedItems // Это вызовет обновление UI
} |
|
С появлением нового фреймворка Observation и макроса @Observable в iOS 17 этот подход меняется. Теперь SwiftUI автоматически отслеживает изменения всех свойств класса без необходимости в аннотации @Published:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
| @Observable
class CartManager {
var items: [CartItem] = []
var totalPrice: Double = 0.0
func addItem(_ item: CartItem) {
items.append(item)
totalPrice += item.price
// UI обновится при изменении любого отслеживаемого свойства
}
} |
|
@AppStorage для хранения данных между сеансами
Часто приложению требуется сохранять некоторые настройки между запусками. В UIKit для этого обычно использовался UserDefaults. SwiftUI предлагает элегантную обёртку вокруг этого механизма — @AppStorage.
| Swift | 1
2
3
4
5
6
7
8
9
10
11
| struct SettingsView: View {
@AppStorage("username") private var username: String = "Гость"
@AppStorage("isNotificationsEnabled") private var areNotificationsEnabled = true
var body: some View {
Form {
TextField("Имя пользователя", text: $username)
Toggle("Уведомления", isOn: $areNotificationsEnabled)
}
}
} |
|
Этот код автоматически сохраняет введённое пользователем имя и состояние переключателя уведомлений в UserDefaults. При следующем запуске приложения эти значения будут восстановлены. При этом вам не нужно вручную работать с UserDefaults.standard.
@AppStorage поддерживает базовые типы данных: String, Int, Double, Bool и URL. Для хранения более сложных типов можно использовать Data и кодирование/декодирование:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| struct ComplexSettings: Codable {
var theme: String
var fontScale: Double
var privacyOptions: [String: Bool]
}
struct AdvancedSettingsView: View {
@AppStorage("complexSettings") private var complexSettingsData: Data = {
let defaultSettings = ComplexSettings(
theme: "system",
fontScale: 1.0,
privacyOptions: ["analytics": true, "personalization": false]
)
return try! JSONEncoder().encode(defaultSettings)
}()
private var settings: ComplexSettings {
get {
try! JSONDecoder().decode(ComplexSettings.self, from: complexSettingsData)
}
set {
complexSettingsData = try! JSONEncoder().encode(newValue)
}
}
var body: some View {
VStack {
Text("Текущая тема: \(settings.theme)")
// ... другие элементы интерфейса
}
}
} |
|
Хотя @AppStorage удобен, у него есть ограничения. Он лучше всего подходит для небольших фрагментов данных. Для более сложных сценариев стоит обратить внимание на @SceneStorage (для сохранения состояния между обновлениями сцены) и другие механизмы хранения, такие как Core Data или SwiftData.
В одном из своих проектов я допустил ошибку, пытаясь хранить большой объём данных в @AppStorage. Это приводило к заметным задержкам при запуске приложения, так как данные синхронно десериализовались при инициализации представления. Это привело нас к созданию собственного асинхронного решения для загрузки настроек.
@SceneStorage для сохранения состояния сцены
В отличие от @AppStorage, который сохраняет данные между запусками приложения, @SceneStorage предназначен для сохранения состояния между жизненными циклами сцены. Это особенно полезно в iPad приложениях, где пользователь может работать с несколькими окнами.
| Swift | 1
2
3
4
5
6
7
8
9
| struct DocumentEditorView: View {
@SceneStorage("documentText") private var documentText = ""
@SceneStorage("cursorPosition") private var cursorPosition = 0
var body: some View {
TextEditor(text: $documentText)
// ... логика для отслеживания и восстановления позиции курсора
}
} |
|
При сворачивании приложения или переключении между сценами, SwiftUI автоматически сохранит эти значения и восстановит их, когда сцена снова станет активной. Это создаёт плавный опыт пользователя, который может продолжить работу с того места, где остановился. Интересно, что @SceneStorage работает даже когда система выгружает неактивные сцены из памяти для экономии ресурсов — состояние все равно будет восстановлено при следующей активации.
В своей практике я заметил, что @SceneStorage идеально подходит для временных рабочих данных, которые важны для текущей сессии пользователя, но не обязательно должны сохраняться долгосрочно.
SwiftUI шибка при получении данных с Апи Всем привет. Понимаю, вопрос простой, но что то не могу найти нормальной информации в инэте. Начал изучать swiftUI и не могу понять как получить... Интегрировать UIKit в SwiftUI или наоборот? Я начинающий разработчик под Apple. Насколько я понимаю, UIKit и SwiftUI – это полностью самостоятельные фреймворки, на каждом из которых можно... Работа с изображениями SwiftUI Всем добрый день!
Вот в продолжении обучения по теме swiftUI столкнулся со следующей проблемой:
extension Landmark {
var image: Image { ... SwiftUI текущий календарь Добрый день.
Изучаю SwiftUI и столкнулся с ошибкой в строке:
let calendar = Calendar.current
Описание ошибки: Type 'Calendar' has no member...
Практические сценарии применения
Теоретическое понимание механизмов передачи данных в SwiftUI полезно, но настоящее мастерство приходит с практикой.
Простые формы и ввод пользователя
Формы — одна из самых распространённых частей мобильных приложений. Организация потока данных здесь критична для правильной работы валидации, сохранения и обновления информации. Представим форму регистрации, где нужно собрать основную информацию о пользователе:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| struct RegistrationForm: View {
@State private var username = ""
@State private var email = ""
@State private var password = ""
@State private var passwordConfirm = ""
@State private var agreedToTerms = false
// Состояние валидации
@State private var isUsernameValid = true
@State private var isEmailValid = true
@State private var doPasswordsMatch = true
var canSubmit: Bool {
!username.isEmpty && isUsernameValid &&
!email.isEmpty && isEmailValid &&
!password.isEmpty && password == passwordConfirm &&
agreedToTerms
}
var body: some View {
Form {
Section(header: Text("Информация профиля")) {
TextField("Имя пользователя", text: $username)
.onChange(of: username) { validateUsername() }
if !isUsernameValid {
Text("Имя должно содержать не менее 3 символов")
.foregroundColor(.red)
}
TextField("Email", text: $email)
.onChange(of: email) { validateEmail() }
if !isEmailValid {
Text("Некорректный формат email")
.foregroundColor(.red)
}
}
Section(header: Text("Безопасность")) {
SecureField("Пароль", text: $password)
.onChange(of: password) { validatePasswords() }
SecureField("Подтвердите пароль", text: $passwordConfirm)
.onChange(of: passwordConfirm) { validatePasswords() }
if !doPasswordsMatch {
Text("Пароли не совпадают")
.foregroundColor(.red)
}
}
Toggle("Я согласен с условиями", isOn: $agreedToTerms)
Button("Зарегистрироваться") {
submitRegistration()
}
.disabled(!canSubmit)
}
}
private func validateUsername() {
isUsernameValid = username.count >= 3
}
private func validateEmail() {
// Упрощенная проверка email
isEmailValid = email.contains("@") && email.contains(".")
}
private func validatePasswords() {
doPasswordsMatch = password == passwordConfirm
}
private func submitRegistration() {
// Отправка данных на сервер
}
} |
|
В этом примере мы используем локальное состояние (@State) для хранения значений полей формы и результатов валидации. Обратите внимание, как вычисляемое свойство canSubmit объединяет несколько условий для определения возможности отправки формы. Для более сложных форм с множеством полей разумно выделить модель формы в отдельный наблюдаемый объект:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| @Observable
class RegistrationModel {
var username = ""
var email = ""
var password = ""
var passwordConfirm = ""
var agreedToTerms = false
var isUsernameValid: Bool {
username.count >= 3
}
var isEmailValid: Bool {
email.contains("@") && email.contains(".")
}
var doPasswordsMatch: Bool {
password == passwordConfirm
}
var canSubmit: Bool {
!username.isEmpty && isUsernameValid &&
!email.isEmpty && isEmailValid &&
!password.isEmpty && doPasswordsMatch &&
agreedToTerms
}
}
struct RegistrationForm: View {
@State private var model = RegistrationModel()
var body: some View {
Form {
TextField("Имя пользователя", text: $model.username)
if !model.isUsernameValid && !model.username.isEmpty {
Text("Имя должно содержать не менее 3 символов")
.foregroundColor(.red)
}
// Остальные поля...
Button("Зарегистрироваться") {
// Отправка данных
}
.disabled(!model.canSubmit)
}
}
} |
|
Такой подход изолирует логику валидации от представления, что упрощает тестирование и поддержку. Это особенно важно для сложных форм с большим количеством взаимосвязанных полей.
Сложные иерархии представлений
По мере роста приложения растёт и сложность его потоков данных. Особенно это заметно в глубоких иерархиях представлений, где информация должна проходить через несколько уровней.
Рассмотрим пример приложения для учёта расходов, где пользователь может просматривать, добавлять и редактировать транзакции:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| @Observable
class ExpenseModel {
var transactions: [Transaction] = []
var categories: [Category] = []
func addTransaction(_ transaction: Transaction) {
transactions.append(transaction)
}
func updateTransaction(_ transaction: Transaction, at index: Int) {
transactions[index] = transaction
}
func deleteTransaction(at index: Int) {
transactions.remove(at: index)
}
}
struct ExpensesApp: View {
@State private var expenseModel = ExpenseModel()
@State private var showingAddForm = false
var body: some View {
NavigationStack {
TransactionListView(transactions: $expenseModel.transactions)
.navigationTitle("Мои расходы")
.toolbar {
Button("Добавить") {
showingAddForm = true
}
}
.sheet(isPresented: $showingAddForm) {
TransactionFormView(
categories: expenseModel.categories,
onSave: { transaction in
expenseModel.addTransaction(transaction)
showingAddForm = false
}
)
}
}
.environment(expenseModel)
}
}
struct TransactionListView: View {
@Binding var transactions: [Transaction]
var body: some View {
List {
ForEach(transactions.indices, id: \.self) { index in
NavigationLink {
TransactionDetailView(transaction: $transactions[index])
} label: {
TransactionRow(transaction: transactions[index])
}
}
.onDelete { indexSet in
transactions.remove(atOffsets: indexSet)
}
}
}
} |
|
Здесь мы комбинируем несколько механизмов передачи данных:- Используем
@State для хранения модели верхнего уровня.
- Передаём биндинги к массиву транзакций в список.
- Размещаем модель в окружении для доступа из любой части приложения.
- При навигации к деталям передаём биндинг к конкретной транзакции.
Обратите внимание на интересный момент: когда мы создаем биндинг к элементу массива с помощью $transactions[index], SwiftUI автоматически обновит представление при изменении этого элемента. Это избавляет нас от необходимости вручную искать и обновлять элементы.
В одном из проектов я столкнулся с неожиданной проблемой: при использовании такого подхода получал странные краши со "смещением индекса". Проблема была в том, что при удалении элементов массива индексы могли измениться во время отображения детального представления. Решение — передавать идентификатор элемента вместо индекса и искать элемент по этому идентификатору:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| struct TransactionListView: View {
@Environment(ExpenseModel.self) private var expenseModel
var body: some View {
List {
ForEach(expenseModel.transactions) { transaction in
NavigationLink {
TransactionDetailView(transactionId: transaction.id)
} label: {
TransactionRow(transaction: transaction)
}
}
.onDelete { indexSet in
for index in indexSet {
expenseModel.deleteTransaction(at: index)
}
}
}
}
}
struct TransactionDetailView: View {
let transactionId: UUID
@Environment(ExpenseModel.self) private var expenseModel
private var transactionBinding: Binding<Transaction> {
Binding(
get: {
expenseModel.transactions.first { $0.id == transactionId }!
},
set: { newValue in
if let index = expenseModel.transactions.firstIndex(where: { $0.id == transactionId }) {
expenseModel.updateTransaction(newValue, at: index)
}
}
)
}
var body: some View {
TransactionForm(transaction: transactionBinding)
.navigationTitle("Детали транзакции")
}
} |
|
Этот подход более устойчив к изменениям в коллекции, так как не зависит от индексов.
Асинхронные операции и обновление UI
Одна из распространённых проблем в разработке приложений — как правильно обновлять интерфейс после асинхронных операций, например, сетевых запросов. В SwiftUI есть несколько подходов к этому:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| @Observable
class WeatherViewModel {
var temperature: Double?
var humidity: Double?
var isLoading = false
var errorMessage: String?
func fetchWeather(for city: String) async {
isLoading = true
errorMessage = nil
do {
// Имитация сетевого запроса
try await Task.sleep(for: .seconds(1))
// В реальном приложении здесь был бы API-запрос
let weatherData = try await WeatherService.fetchWeather(for: city)
// Обновляем UI в основном потоке
await MainActor.run {
temperature = weatherData.temperature
humidity = weatherData.humidity
isLoading = false
}
} catch {
await MainActor.run {
errorMessage = "Ошибка загрузки: \(error.localizedDescription)"
isLoading = false
}
}
}
}
struct WeatherView: View {
@State private var city = ""
@State private var viewModel = WeatherViewModel()
var body: some View {
VStack {
TextField("Введите город", text: $city)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button("Посмотреть погоду") {
Task {
await viewModel.fetchWeather(for: city)
}
}
.disabled(city.isEmpty || viewModel.isLoading)
.padding()
if viewModel.isLoading {
ProgressView()
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
.foregroundColor(.red)
} else if let temperature = viewModel.temperature,
let humidity = viewModel.humidity {
VStack(spacing: 10) {
Text("Температура: \(temperature, specifier: "%.1f")°C")
Text("Влажность: \(humidity, specifier: "%.0f")%")
}
}
Spacer()
}
.padding()
}
} |
|
В этом примере мы используем современный подход с async/await для выполнения асинхронной операции. Обратите внимание на несколько ключевых моментов:
1. Мы отслеживаем состояние загрузки с помощью флага isLoading.
2. Мы обрабатываем возможные ошибки и отображаем сообщение пользователю.
3. Мы явно переключаемся на главный поток для обновления UI с помощью MainActor.
Этот паттерн предпочтительнее старого подхода с completion handlers, так как делает асинхронный код намного более читаемым и уменьшает вероятность ошибок, связанных с циклами захвата или неправильными переключениями между потоками. Если вам приходится работать с API, использующими completion handlers, вы можете преобразовать их в async/await с помощью продолжений:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| func fetchWeatherData(for city: String) async throws -> WeatherData {
return try await withCheckedThrowingContinuation { continuation in
weatherService.fetchWeather(for: city) { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
} |
|
Избегание распространённых ошибок
За годы работы со SwiftUI я наблюдал несколько типичных ошибок при организации потоков данных, которые приводили к багам и проблемам с производительностью.
Копирование источников истины
Одна из самых распространённых ошибок — создание копий данных вместо использования биндингов или среды:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Неправильно
struct DetailView: View {
let item: TodoItem // Копия, изменения не будут синхронизированы
var body: some View {
// ...
}
}
// Правильно
struct DetailView: View {
@Binding var item: TodoItem // Ссылка на оригинальные данные
var body: some View {
// ...
}
} |
|
Когда вы передаете значение напрямую, без биндинга, любые изменения, сделанные в дочернем представлении, не будут отражаться в родительском. Это может привести к непоследовательному состоянию UI и трудноотслеживаемым багам.
Избыточные обновления
Другая распространённая проблема — слишком частые обновления UI из-за неправильной структуры данных:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Неэффективно
@Observable
class UserPreferences {
var allSettings: [String: Any] = [:] // Изменение любого значения обновит весь UI
}
// Более эффективно
@Observable
class UserPreferences {
var appearance: AppearanceSettings = .default
var notifications: NotificationSettings = .default
var privacy: PrivacySettings = .default
} |
|
Разделение данных на логические группы позволяет SwiftUI более точно определять, какие части UI нуждаются в обновлении, что повышает производительность.
Ошибки идентичности
SwiftUI полагается на идентичность объектов для определения того, какие части UI нужно обновить. Неправильное обращение с идентичностью может вызвать неожиданные перерисовки или, наоборот, отсутствие обновлений:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| struct ContentView: View {
@State private var tasks: [Task] = []
var body: some View {
List {
// Плохо: ID основан на индексе, который может измениться
ForEach(0..<tasks.count, id: \.self) { index in
TaskRow(task: tasks[index])
}
// Хорошо: ID основан на уникальном идентификаторе
ForEach(tasks) { task in
TaskRow(task: task)
}
}
}
} |
|
В первом случае, если задача будет удалена из середины списка, ID всех последующих элементов изменятся, что приведёт к ненужным обновлениям UI. Во втором случае SwiftUI может точно определить, какой элемент был изменён.
Передача данных между независимыми модулями приложения
В крупных приложениях часто возникает необходимость передачи данных между модулями, которые не связаны напрямую в иерархии представлений. Для таких случаев традиционные механизмы передачи данных могут быть недостаточными.
Один из подходов — использование синглтонов или глобальных объектов. Однако этот метод имеет свои недостатки: он создаёт скрытые зависимости, затрудняет тестирование и может привести к проблемам с многопоточностью. Более элегантное решение — использование сервисов и зависимостей, размещённых в окружении:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // Определение сервиса
protocol AuthenticationService {
var currentUser: User? { get }
func signIn(username: String, password: String) async throws -> User
func signOut() async throws
}
// Реализация
class DefaultAuthService: AuthenticationService {
@Published var currentUser: User?
func signIn(username: String, password: String) async throws -> User {
// Реализация
}
func signOut() async throws {
// Реализация
}
}
// Ключ для доступа к сервису через окружение
struct AuthServiceKey: EnvironmentKey {
static let defaultValue: AuthenticationService = DefaultAuthService()
}
// Расширение для удобного доступа
extension EnvironmentValues {
var authService: AuthenticationService {
get { self[AuthServiceKey.self] }
set { self[AuthServiceKey.self] = newValue }
}
}
// Использование в приложении
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.authService, DefaultAuthService())
}
}
}
// Доступ к сервису
struct LoginView: View {
@Environment(\.authService) private var authService
@State private var username = ""
@State private var password = ""
var body: some View {
// ...
}
} |
|
Этот подход имеет несколько преимуществ:- Сервисы можно заменить на мок-версии для тестирования.
- Зависимости явные и управляемые.
- Разные части приложения могут использовать разные реализации сервисов.
Тестирование компонентов с разными потоками данных
Тестирование представлений, использующих различные механизмы передачи данных, требует особого подхода. SwiftUI предлагает несколько инструментов для упрощения этой задачи:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Тестируемое представление
struct ProfileEditor: View {
@Binding var profile: UserProfile
var body: some View {
Form {
TextField("Имя", text: $profile.name)
TextField("Фамилия", text: $profile.surname)
DatePicker("Дата рождения", selection: $profile.birthDate)
}
}
}
// Тестирование с помощью PreviewProvider
struct ProfileEditor_Previews: PreviewProvider {
static var previews: some View {
@State var profile = UserProfile.sample
return ProfileEditor(profile: $profile)
}
} |
|
Для модульного тестирования представлений, использующих сложные потоки данных, можно воспользоваться новыми API из iOS 17:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| func testProfileEditor() throws {
// Создаем тестовый профиль
let profile = UserProfile.sample
// Создаем тестовое представление
let profileEditor = ProfileEditor(
profile: .constant(profile)
)
// Проверяем текстовые поля
let namePredicate = "TextField where label == 'Имя' && text == '\(profile.name)'"
XCTAssert(try viewController.inspect().find(viewWithId: namePredicate).isTextField())
// Имитируем редактирование поля
try viewController.inspect().find(viewWithId: namePredicate).textField().setInput("Новое имя")
// Проверяем обновление модели
XCTAssertEqual(profile.name, "Новое имя")
} |
|
Для представлений, использующих @EnvironmentObject или @Environment, необходимо явно предоставить эти зависимости:
| Swift | 1
2
3
| let testView = MyView()
.environment(\.locale, Locale(identifier: "ru"))
.environmentObject(MockDataModel()) |
|
Хороший подход к тестированию — создание фабрик представлений, которые инкапсулируют все необходимые зависимости:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| struct ViewFactory {
let dataModel: DataModel
let settings: AppSettings
func makeHomeView() -> some View {
HomeView()
.environment(\.appSettings, settings)
.environmentObject(dataModel)
}
// Другие фабричные методы
}
// В тестах
let factory = ViewFactory(
dataModel: MockDataModel(...),
settings: TestSettings(...)
)
let view = factory.makeHomeView() |
|
Продвинутые техники
За пределами основных механизмов передачи данных SwiftUI существует целый мир более сложных стратегий, которые могут оказаться спасением в нестандартных ситуациях. Эти подходы не всегда очевидны из документации, но часто оказываются необходимыми по мере роста сложности приложения.
Кастомные решения для сложных кейсов
Иногда встроенных механизмов SwiftUI недостаточно для решения специфических проблем. В таких случаях разработчики создают собственные реализации на основе существующих инструментов. Одна из наиболее востребованных кастомных техник — создание зависимой от контекста среды выполнения. Например, вам может понадобиться, чтобы некоторые части приложения имели доступ только к определенным данным или функциям:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| struct ViewContextKey: EnvironmentKey {
static let defaultValue: ViewContext = .global
}
extension EnvironmentValues {
var viewContext: ViewContext {
get { self[ViewContextKey.self] }
set { self[ViewContextKey.self] = newValue }
}
}
enum ViewContext {
case global
case limited(permissions: [Permission])
case preview
var canEditContent: Bool {
switch self {
case .global:
return true
case .limited(let permissions):
return permissions.contains(.edit)
case .preview:
return false
}
}
}
struct ContentEditor: View {
@Environment(\.viewContext) private var context
@Binding var content: String
var body: some View {
if context.canEditContent {
TextField("Редактировать", text: $content)
} else {
Text(content)
}
}
} |
|
Этот паттерн позволяет гибко настраивать доступные функции в зависимости от того, где представление используется, без необходимости явно передавать дополнительные параметры. Другой полезный кастомный механизм — объединённое сохранение и отмена изменений:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| class EditableModel<T>: ObservableObject where T: Equatable {
@Published private(set) var original: T
@Published var draft: T
var hasChanges: Bool {
original != draft
}
init(value: T) {
self.original = value
self.draft = value
}
func commit() {
original = draft
}
func discard() {
draft = original
}
}
struct EditView<Content: View, T: Equatable>: View {
@StateObject private var model: EditableModel<T>
@Environment(\.dismiss) private var dismiss
private let content: (Binding<T>) -> Content
init(value: T, @ViewBuilder content: @escaping (Binding<T>) -> Content) {
self._model = StateObject(wrappedValue: EditableModel(value: value))
self.content = content
}
var body: some View {
NavigationStack {
content($model.draft)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Отмена") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Сохранить") {
model.commit()
dismiss()
}
.disabled(!model.hasChanges)
}
}
}
}
} |
|
Это решение элегантно инкапсулирует логику редактирования, отмены и сохранения изменений, которая часто дублируется в разных частях приложения.
Производительность и оптимизация потоков данных
По мере роста приложения начинают проявляться проблемы с производительностью. Частые обновления UI могут приводить к ощутимым задержкам и повышенному расходу батареи. Одна из ключевых оптимизаций — грамотная декомпозиция наблюдаемых объектов. Вместо одного гигантского объекта, который вызывает обновление всего дерева представлений при любом изменении, лучше разделить данные на логические группы:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Вместо этого
@Observable class AppState {
var user: User = .anonymous
var settings: Settings = .default
var documents: [Document] = []
var networkStatus: NetworkStatus = .disconnected
}
// Лучше использовать
@Observable class UserState {
var user: User = .anonymous
}
@Observable class AppSettings {
var settings: Settings = .default
}
@Observable class DocumentStore {
var documents: [Document] = []
}
@Observable class NetworkMonitor {
var status: NetworkStatus = .disconnected
} |
|
Такой подход позволяет SwiftUI более точно определять, какие представления нуждаются в обновлении при изменении конкретных данных.
Другая важная техника — использование механизма эквивалентности для предотвращения ненужных перерисовок:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| struct ExpensiveView: View {
let data: ComplexData
var body: some View {
// Сложные вычисления на основе data
VStack {
// ... множество дочерних компонентов
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.data.id == rhs.data.id && lhs.data.version == rhs.data.version
}
} |
|
Когда функция == определена для представления, SwiftUI может использовать её для определения, нужно ли перерисовывать представление при изменении его свойств.
В одном из проектов нам пришлось вручную управлять обновлениями для представления, отображающего сложные графики. Мы использовали комбинацию equatable и отложенных обновлений:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| struct ChartView: View, Equatable {
let data: ChartData
@State private var internalData: ProcessedChartData?
var body: some View {
Group {
if let internalData {
// Отрисовка на основе обработанных данных
} else {
ProgressView()
}
}
.task(id: data.id) {
// Отложенная обработка для предотвращения зависаний UI
let processed = await processData(data)
internalData = processed
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.data.id == rhs.data.id
}
private func processData(_ data: ChartData) async -> ProcessedChartData {
// Тяжелые вычисления в фоновом потоке
await Task.yield() // Даем возможность другим задачам выполниться
return // ... результат обработки
}
} |
|
Интеграция с Combine для управления потоками данных
Фреймворк Combine предоставляет мощные инструменты для трансформации, фильтрации и объединения потоков данных. Его интеграция со SwiftUI открывает новые возможности для сложных сценариев обработки данных.
Одно из типичных применений Combine в SwiftUI — дебаунсинг поисковых запросов:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| struct SearchView: View {
@State private var searchText = ""
@State private var results: [SearchResult] = []
@State private var isLoading = false
// Издатель для дебаунсинга поисковых запросов
private let searchSubject = PassthroughSubject<String, Never>()
var body: some View {
VStack {
TextField("Поиск", text: $searchText)
.onChange(of: searchText) {
searchSubject.send(searchText)
}
if isLoading {
ProgressView()
} else {
List(results) { result in
Text(result.title)
}
}
}
.onAppear {
setupSearchPipeline()
}
}
private func setupSearchPipeline() {
searchSubject
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.handleEvents(receiveOutput: { _ in isLoading = true })
.flatMap { term in
Future<[SearchResult], Error> { promise in
Task {
do {
let results = try await performSearch(term)
promise(.success(results))
} catch {
promise(.failure(error))
}
}
}
.catch { error -> Just<[SearchResult]> in
print("Ошибка поиска: \(error)")
return Just([])
}
}
.receive(on: RunLoop.main)
.sink { results in
self.results = results
self.isLoading = false
}
.store(in: &cancellables)
}
} |
|
Combine отлично справляется с координацией нескольких источников данных. Например, если вам нужно собрать информацию из разных API и обновить интерфейс только когда все данные будут готовы:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
| let userDataPublisher = fetchUserData()
let friendsPublisher = fetchFriendsList()
let postsPublisher = fetchRecentPosts()
Publishers.CombineLatest3(userDataPublisher, friendsPublisher, postsPublisher)
.receive(on: RunLoop.main)
.sink { userData, friends, posts in
// Обновление UI с полным набором данных
updateProfileView(userData: userData, friends: friends, posts: posts)
}
.store(in: &cancellables) |
|
Несмотря на появление async/await, Combine остается мощным инструментом для декларативного управления асинхронными потоками данных и отлично интегрируется со SwiftUI.
SwiftUI <Клавиатура> Версия XCode 12.5.1
Упёрся в то что как-то не получается показать клавиатуру в самом простом коде
Хоть сколько <тапай> по... swiftui foreach Скажите пожалуйста где ошибка:
import SwiftUI
struct ContentView: View {
var body: some View {
ForEach(0 ... 5, id: \.self)... SwiftUI как сделать много языковой интерфейс Я новичок, изучаю SwiftUI. Подскажите как сделать много языковой интерфейс. Буду благодарен за ссылки и конкретные совету, куда посмотреть,... Как отфильтровать массив отфильтрованных данных Core Data Отфильтровал массив данных Core Data по одному значению, нужно теперь этот же массив отфильтровать по другому значению. Чтобы в конечном итоге... Trust Flow (MajesticSEO) / Citation Flow (Majestic SEO) что это? Что означает индекс = Trust Flow (MajesticSEO) / Citation Flow (Majestic SEO)? Где его можно использовать?
Добавлено через 4 часа 3 минуты ... Передача данных между частичными представлениями Привет всем!
У меня есть страница, на которой есть некие данные.
Также у этой странице есть 2 частичных представления.
Подскажите, возможно... Передача id между представлениями на web api Здравствуйте! Пишу на web api и возникла ситуация, что у меня есть список, например, пользователей и при клике на одном из них я должен перейти на... Почему Data Flow Analysis это static technique? Согласно ответам на mock test для ISTQB exam, анализ потока данных почему-то статический. Хотя как он может быть статическим, мне непонятно.... PyNoder - data flow programming наткунлся случайно на проект https://github.com/johnroper100/PyNoder
Очень заинтересовало. Кто имеет представление об этом? в сети очень мало... Интеграция таблицы Microsoft Access с Data Flow диаграммой Case Studio Допустим, я создал новый документ .mdb, добавил таблицу, заполнил ее некоторыми данными, и отдельно создал в Case Studio диаграмму с процессами,... Переключение между 4-мя представлениями Всем привет!
Подскажите как организовать цикличное (влево и вправо) переключение между 4-мя UIViewController-ами с CATransition анимацией? ... Как передать класс между представлениями? Подскажите пожалуйста, как можно передать класс между представлениями не сохраняя его в бд?
Задача такова:
1. В одном представлении создается...
|