Перейти к содержанию
Educora
Продвинутый22 мин11 / 14

Состояние и поток данных

Храни состояние в SwiftUI через `@State`, передавай его дочерним представлениям через `@Binding`, создавай модель макросом `@Observable`, делись ею через `@Environment` и строй приложение в стиле MVVM.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять разницу между @State и @Binding и находить единый источник правды
  • Создавать класс модели макросом @Observable и владеть им через @State
  • Делиться моделью между представлениями через @Environment и @Bindable
  • Описывать слои MVVM в SwiftUI

Нигяр на экране настроек поднимает дневную цель с 15 до 30 минут, а на главном экране по-прежнему написано «15 min». Причина проста: одно и то же значение хранится в двух местах, которые ничего не знают друг о друге. Золотое правило SwiftUI: у каждого фрагмента данных должен быть один владелец — единый источник правды, а остальные представления его читают или привязываются к нему. В этом уроке ты освоишь инструменты для этого.

@State: собственное состояние представления

Структура View неизменяема, и SwiftUI может в любой момент создать её заново. Поэтому меняющееся значение нельзя хранить в обычном свойстве. @State просит SwiftUI хранить значение вне структуры и пересчитывать body при его изменении. Всегда объявляй свойство @State как private: оно принадлежит только этому представлению. Начальное значение используется лишь один раз, при первом появлении представления; дальше SwiftUI берёт сохранённое значение.

Неверно: обычное свойство
struct CounterView: View {
    var count = 0

    var body: some View {
        Button("Taps: \(count)") {
            count += 1
        }
    }
}
Верно: @State
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        Button("Taps: \(count)") {
            count += 1
        }
    }
}
Код слева не компилируется: «Cannot assign to property: 'self' is immutable». Справа каждое нажатие увеличивает счётчик и обновляет экран.

@Binding: связь с чужим состоянием

Если дочернее представление должно менять значение родителя, ему передают не копию, а привязку (binding) — двустороннюю связь. Родитель передаёт значение через $ ($goal), а ребёнок объявляет его как @Binding. Когда ребёнок меняет значение, на самом деле меняется @State родителя, и обе части экрана обновляются одновременно.

Swift
struct GoalStepper: View {
    @Binding var minutes: Int

    var body: some View {
        Stepper("Daily goal: \(minutes) min", value: $minutes, in: 5...60, step: 5)
    }
}

struct SettingsScreen: View {
    @State private var goal = 15

    var body: some View {
        Form {
            GoalStepper(minutes: $goal)
            Text("You will study \(goal) minutes a day")
        }
    }
}
На экране: строка с кнопками +/− и текст под ней; при нажатии число меняется сразу в обеих строках.
Определение
Единый источник правды

Единственное место, где хранятся данные. В примере это @State private var goal в SettingsScreen; GoalStepper лишь привязан к нему и собственной копии не хранит.

Модели @Observable

Когда данных и логики экрана становится много, их переносят в отдельный класс модели. Начиная с iOS 17 достаточно написать перед классом макрос @Observable: SwiftUI сам отслеживает свойства, которые читает body, и обновляет представление только при их изменении. Логику модели можно проверить и без экрана, обычным кодом на Swift:

Swift
import Observation

@Observable
final class QuizModel {
    var question = 1
    var score = 0
    var finished: Bool { question > 5 }

    func answer(correct: Bool) {
        if correct { score += 1 }
        question += 1
    }
}

let quiz = QuizModel()
for correct in [true, false, true, true, true] {
    quiz.answer(correct: correct)
}
print("Score: \(quiz.score) / 5, finished: \(quiz.finished)")
Ожидаемый результат
Score: 4 / 5, finished: true
Swift
struct QuizView: View {
    @State private var quiz = QuizModel()

    var body: some View {
        VStack(spacing: 16) {
            if quiz.finished {
                Text("Your score: \(quiz.score) / 5")
            } else {
                Text("Question \(quiz.question) of 5")
                Button("Correct") { quiz.answer(correct: true) }
                Button("Wrong") { quiz.answer(correct: false) }
            }
        }
        .font(.title2)
    }
}
На экране: «Question 1 of 5» и две кнопки; после пятого ответа — результат вроде «Your score: 4 / 5».
Раньше: ObservableObject (iOS 13+)
final class QuizModel: ObservableObject {
    @Published var score = 0
}

struct QuizView: View {
    @StateObject private var quiz = QuizModel()
    // child views: @ObservedObject, @EnvironmentObject
}
Сейчас: @Observable (iOS 17+)
@Observable
final class QuizModel {
    var score = 0
}

struct QuizView: View {
    @State private var quiz = QuizModel()
    // child views: plain property, @Bindable, @Environment
}
По-старому каждому свойству нужен был @Published, а любое изменение объекта обновляло все наблюдающие за ним представления. @Observable требует меньше кода и реагирует только на реально прочитанные свойства.

@Environment и @Bindable

Передавать модель на пять уровней вниз по цепочке параметров утомительно. Вместо этого её кладут в окружение через .environment(profile), а нужное представление берёт её через @Environment(Profile.self) private var profile. Если нужна привязка к свойству модели (например, для TextField), используют @Bindable. @Environment также даёт системные значения: \.dismiss, \.colorScheme, \.locale.

Swift
@Observable
final class Profile {
    var name = "Nigar"
}

struct ProfileEditor: View {
    @Bindable var profile: Profile

    var body: some View {
        TextField("Name", text: $profile.name)
    }
}

struct GreetingView: View {
    @Environment(Profile.self) private var profile

    var body: some View {
        Text("Hello, \(profile.name)!")
    }
}
Корневое представление хранит @State private var profile = Profile() и вызывает .environment(profile). Если изменить имя в поле, надпись «Hello, …!» сразу обновится.
ИнструментКогда
@Stateпредставление владеет значением или моделью @Observable
@Bindingребёнок читает и меняет значение родителя
@Bindableнужны привязки к свойствам модели @Observable
@Environmentпрочитать системное значение или модель из окружения
обычный letпредставление только показывает значение

Вместе это и есть стиль MVVM: Model — структуры данных и сервисы (сеть, база данных); ViewModel — класс @Observable, который хранит состояние и логику экрана (QuizModel); View — структура SwiftUI, которая показывает состояние и передаёт действия пользователя в методы ViewModel. Поскольку интерфейс можно обновлять только в главном потоке, ViewModel обычно помечают @MainActor.

  1. 1
    Создай файл модели

    Нажми Cmd+N, выбери Swift File в разделе iOS, назови его QuizModel и нажми Create. Напиши там класс @Observable.

  2. 2
    Создай файл представления

    Снова Cmd+N, на этот раз шаблон SwiftUI View; имя — QuizView. Шаблон сам добавит блок #Preview.

  3. 3
    Проверь на холсте

    Понажимай кнопки в превью и проверь, что меняется номер вопроса, а в конце — результат; симулятор запускать не нужно.

создать новый файлCmd+N
перейти к объявлению типа или функции — Jump to DefinitionCtrl+Cmd+J
структура файла: список свойств и методовCtrl+6
показать открытый файл в навигатореCmd+Shift+J

Главное

  • У каждого фрагмента данных должен быть один владелец — единый источник правды.
  • @State private var — собственное состояние представления; при его изменении body пересчитывается.
  • @Binding — двусторонняя связь со значением родителя; её передают через $.
  • Класс @Observable (iOS 17+) сам отслеживает прочитанные свойства; модель храни через @State, делись ею через @Environment и @Bindable.
  • MVVM: Model даёт данные, ViewModel @Observable управляет состоянием, View его показывает.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Как объявить простое изменяемое значение, принадлежащее самому представлению?