SwiftUI спрашивают всё чаще: обёртки состояния, @Observable, идентичность view и навигация. Хорошая новость: если понять три идеи (view это описание, у состояния один владелец, идентичность определяет время жизни), остальное складывается.
View это описание
struct ProfileView: View {
let user: User
var body: some View {
VStack(alignment: .leading) {
Text(user.name).font(.title)
Text(user.city).foregroundStyle(.secondary)
}
}
}
ProfileView это лёгкая структура. SwiftUI вызывает body заново, когда меняются данные, сравнивает новое описание со старым и обновляет только изменившееся. Поэтому body должен быть быстрым и без побочных эффектов: никаких сетевых запросов, сортировки больших массивов и изменения состояния.
Обёртки состояния
| Инструмент | Для чего |
|---|---|
@State | своё состояние view: флаг шторки, текст поля; также владение @Observable-объектом |
@Binding | двусторонняя ссылка на чужое состояние |
@Bindable | биндинги к свойствам @Observable-объекта ($model.title) |
@Environment | значения окружения (локаль, схема) и объекты, переданные через .environment |
struct SettingsView: View {
@State private var notificationsOn = false
var body: some View {
Toggle("Уведомления", isOn: $notificationsOn) // $ даёт Binding
}
}
Если в дочерней view сделать свой @State вместо @Binding, она скопирует начальное значение и дальше будет жить отдельно.
Observation (iOS 17)
@Observable @MainActor
final class ProfileModel {
var user: User?
var isLoading = false
func load() async { ... }
}
struct ProfileScreen: View {
@State private var model = ProfileModel()
var body: some View {
content
.task { await model.load() }
}
}
Макрос @Observable отслеживает, какие свойства читались в body, и перерисовывает только те view, которые читали изменённое свойство. Старый ObservableObject с @Published отправлял objectWillChange на любое изменение, и обновлялись все подписчики.
Для старого API действуют правила: владеет объектом @StateObject, а @ObservedObject только подписывается. @ObservedObject var vm = ViewModel() создаёт новую модель при каждом пересоздании структуры родителем, это классическая ошибка с «пропадающими» данными. @EnvironmentObject падает, если предок не передал объект.
Идентичность
SwiftUI связывает состояние, анимации и переходы с идентичностью view:
- структурная: тип и положение в дереве;
- явная:
idвForEachи модификатор.id().
Следствия:
// плохо: две разные view, при переключении состояние CardView сбросится
if isHighlighted { CardView().border(.red) } else { CardView() }
// хорошо: одна view, меняется модификатор
CardView().border(isHighlighted ? .red : .clear)
В ForEach нужны стабильные идентификаторы из данных (Identifiable). Индексы в качестве id ломают анимации и состояние строк при удалении и вставке. Смена .id(value) уничтожает view со всем состоянием, этим пользуются, чтобы сбросить форму.
AnyView стирает статический тип и мешает сравнению деревьев. Вместо него @ViewBuilder, some View и условия в модификаторах.
Побочные эффекты
.task { }: async-работа, привязанная к жизни view, отменяется при исчезновении; сid:перезапускается при смене значения..onChange(of:): реакция на изменение значения..onAppear: синхронная работа при появлении.Task { }внутриonAppearне отменяется сам, поэтому для загрузки лучше.task.
Навигация
С iOS 16 навигация описывается данными:
@State private var path: [Route] = []
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: Route.self) { route in
switch route {
case .product(let id): ProductView(id: id)
case .cart: CartView()
}
}
}
// deep link: path = [.product(42), .cart]
NavigationView и NavigationLink(isActive:) устарели.
SwiftUI и UIKit вместе
- SwiftUI в UIKit:
UIHostingController(rootView:), для ячеекUIHostingConfiguration(iOS 16). - UIKit в SwiftUI:
UIViewRepresentableсmakeUIView,updateUIViewиCoordinatorдля делегатов.
Частые ошибки
@ObservedObjectс созданием объекта на месте.- Индексы как идентификаторы в
ForEach. - Тяжёлые вычисления в
body. if/elseтам, где хватает модификатора.- Изменение состояния в
body(«Modifying state during view update»).
Что спросят на собесе
- Чем
@Stateотличается от@Binding,@StateObjectот@ObservedObject? - Что даёт
@Observableпо сравнению сObservableObject? - Что такое идентичность view и как она влияет на состояние?
- Как сделать программную навигацию и deep link?
- SwiftUI или UIKit для нового проекта, как их совмещать?