Урок по треку Swift и iOS 8 мин

SwiftUI: состояние, идентичность, Observation

Декларативный UI, идентичность view, @State, @Binding, @Observable, @Environment, навигация и списки.

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 для нового проекта, как их совмещать?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.