- Объяснять разницу между
@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
}
}
}struct CounterView: View {
@State private var count = 0
var body: some View {
Button("Taps: \(count)") {
count += 1
}
}
}@Binding: связь с чужим состоянием
Если дочернее представление должно менять значение родителя, ему передают не копию, а привязку (binding) — двустороннюю связь. Родитель передаёт значение через $ ($goal), а ребёнок объявляет его как @Binding. Когда ребёнок меняет значение, на самом деле меняется @State родителя, и обе части экрана обновляются одновременно.
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:
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
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)
}
}final class QuizModel: ObservableObject {
@Published var score = 0
}
struct QuizView: View {
@StateObject private var quiz = QuizModel()
// child views: @ObservedObject, @EnvironmentObject
}@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.
@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Создай файл модели
Нажми
Cmd+N, выбериSwift Fileв разделеiOS, назови егоQuizModelи нажмиCreate. Напиши там класс@Observable. - 2Создай файл представления
Снова
Cmd+N, на этот раз шаблонSwiftUI View; имя —QuizView. Шаблон сам добавит блок#Preview. - 3Проверь на холсте
Понажимай кнопки в превью и проверь, что меняется номер вопроса, а в конце — результат; симулятор запускать не нужно.
Главное
- У каждого фрагмента данных должен быть один владелец — единый источник правды.
@State private var— собственное состояние представления; при его измененииbodyпересчитывается.@Binding— двусторонняя связь со значением родителя; её передают через$.- Класс
@Observable(iOS 17+) сам отслеживает прочитанные свойства; модель храни через@State, делись ею через@Environmentи@Bindable. - MVVM: Model даёт данные, ViewModel
@Observableуправляет состоянием, View его показывает.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.