@Binding в SwiftUI нужен, когда состояние хранится у родителя, а дочерний view должен читать и менять это состояние. В первом SwiftUI-уроке счетчик жил прямо в ContentView. Теперь вынесем кнопки в отдельный компонент и передадим ему связь с родительским @State
К концу урока вы поймете разницу между @State и @Binding на рабочем примере, а не на абстрактной схеме
- Что получится в конце
- @State: источник локального состояния
- @Binding: связь с чужим состоянием
- Почему это полезно
- Preview для дочернего view
- Домашка: переключатель темы
- Mini-check: кто владеет состоянием
- Частые ошибки и порядок проверки
- Как понять направление данных
- Binding для TextField
- Когда @Binding уже не хватает
- Диагностика странного поведения
- Binding для Slider и Stepper
- Binding не должен скрывать смысл
- Домашка: компонент завершения урока
- Что может быть еще интересно по этой теме
- Что почитать дальше по Swift
Что получится в конце
Код:
import SwiftUI
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Clicks: \(count)")
.font(.title)
CounterControls(count: $count)
}
.padding()
}
}
struct CounterControls: View {
@Binding var count: Int
var body: some View {
HStack {
Button("Add") {
count += 1
}
Button("Reset") {
count = 0
}
}
}
}
#Preview {
ContentView()
}
Родитель хранит число, дочерний view меняет его через binding
@State: источник локального состояния
В родителе:
@State private var count = 0
ContentView владеет состоянием. Он решает, где оно хранится и когда экран должен обновиться
Если состояние нужно только внутри одного view, @State достаточно. Но когда часть UI выносится в дочерний view, возникает вопрос: как дать дочернему view право изменить значение
@Binding: связь с чужим состоянием
В дочернем view:
@Binding var count: Int
Это не самостоятельное хранилище. Это связь с состоянием, которое живет где-то выше
Вызов:
CounterControls(count: $count)
Символ $count передает binding, а не само число. Если написать count: count, будет ошибка типов: дочерний view ждет Binding<Int>, а вы передаете Int
Почему это полезно
Без @Binding весь экран быстро превращается в один большой body. Вы хотите вынести кнопки, форму, переключатель или поле ввода в отдельный view, но состояние должно остаться у родителя
@Binding решает эту задачу: родитель владеет состоянием, дочерний view получает управляемый доступ к изменению
Так компоненты становятся переиспользуемыми. CounterControls не знает, где хранится count. Он только знает, что может его менять
Preview для дочернего view
Для preview дочернего view нужен binding. Можно использовать @State в preview-wrapper:
struct CounterControlsPreviewWrapper: View {
@State private var count = 3
var body: some View {
CounterControls(count: $count)
}
}
#Preview {
CounterControlsPreviewWrapper()
}
Это удобный прием: вы проверяете маленький компонент отдельно, но даете ему настоящее состояние для работы
Домашка: переключатель темы
Сделайте родительский view:
@State private var isDark = false
И дочерний view:
struct ThemeToggle: View {
@Binding var isDark: Bool
var body: some View {
Toggle("Dark mode", isOn: $isDark)
}
}
В родителе покажите текст:
Text(isDark ? "Dark" : "Light")
Проверьте, что Toggle меняет текст. Это хороший пример @Binding на boolean-состоянии
Mini-check: кто владеет состоянием
Когда выбираете между @State и @Binding, задайте вопрос: кто владеет значением
Если view сам хранит локальное состояние, это @State. Если значение пришло сверху и view должен его менять, это @Binding
Не создавайте второй @State в дочернем view, если он должен менять родительское значение. Так вы получите две независимые копии и странное поведение UI
Частые ошибки и порядок проверки
Cannot convert value of type Int to expected Binding<Int> Вы передали count, а нужно $count
Дочерний view не обновляет родителя Проверьте, что у дочернего view именно @Binding, а не отдельный @State
Preview дочернего view не компилируется Для preview нужен настоящий binding. Сделайте wrapper с @State
Слишком рано тянете состояние вверх Если значение нужно только одному маленькому view, оставьте @State там. Поднимайте состояние, когда оно нужно нескольким компонентам
Как понять направление данных
В примере со счетчиком данные идут сверху вниз, а изменение возвращается через binding. Родитель хранит @State, дочерний view получает @Binding и меняет родительское значение
Можно проговорить это как правило:
- родитель владеет состоянием
- ребенок получает доступ к конкретному значению
- ребенок не решает, где это значение хранится
- изменение в ребенке обновляет родителя
Если держать эту модель в голове, @Binding перестает казаться магией. Это не глобальная переменная и не копия значения, а управляемая связь
Binding для TextField
Самый практичный пример @Binding — поле ввода:
struct NameField: View {
@Binding var name: String
var body: some View {
TextField("Name", text: $name)
.textFieldStyle(.roundedBorder)
}
}
Родитель:
struct ContentView: View {
@State private var name = ""
var body: some View {
VStack(spacing: 16) {
NameField(name: $name)
Text("Hello, \(name.isEmpty ? "guest" : name)")
}
.padding()
}
}
Когда пользователь печатает в TextField, меняется родительский @State. Текст ниже обновляется из того же источника данных. Это хороший тест на понимание: если вы создали отдельный @State внутри NameField, приветствие у родителя не изменится
Когда @Binding уже не хватает
@Binding отлично подходит для одного значения: число, строка, boolean, выбранный элемент. Но если экрану нужно менять сложную модель, загружать данные, хранить ошибки, состояние загрузки и результаты API, binding может стать слишком узким инструментом
Например, для сетевого экрана вам понадобятся:
isLoadingitemserrorMessage- функция загрузки
- повторная загрузка
Передавать пять bindings в дочерний view можно, но код быстро станет громоздким. На этом этапе обычно вводят отдельную модель состояния или view model. В этом блоке мы пока не уходим туда, но важно видеть границу инструмента
Диагностика странного поведения
Если дочерний view меняет значение, но экран ведет себя странно, проверьте три вещи:
- Нет ли второго независимого
@Stateс тем же смыслом - Передаете ли вы
$value, а неvalue - Не пересоздается ли родитель с другим начальным состоянием
Самая частая ошибка — создать локальную копию:
struct ChildView: View {
@State private var count = 0
}
Такой child живет своей жизнью. Если задача — менять родительское значение, это должен быть @Binding
Binding для Slider и Stepper
@Binding хорошо видно на контролах, которые сами меняют значение. Например, slider:
struct VolumeSlider: View {
@Binding var volume: Double
var body: some View {
Slider(value: $volume, in: 0...100)
}
}
Родитель:
struct ContentView: View {
@State private var volume = 50.0
var body: some View {
VStack {
VolumeSlider(volume: $volume)
Text("Volume: \(Int(volume))")
}
.padding()
}
}
Slider получает binding и двигает значение у родителя. Текст ниже показывает тот же volume, поэтому экран остается согласованным
Binding не должен скрывать смысл
Если вы передаете binding в компонент, называйте параметр по смыслу, а не просто value:
struct LessonToggle: View {
@Binding var isCompleted: Bool
}
Так при чтении сразу понятно, что меняет компонент. В SwiftUI это особенно важно: маленьких views становится много, и плохие имена быстро превращают код в угадайку
Домашка: компонент завершения урока
Сделайте LessonStatusView, который получает @Binding var isCompleted: Bool, показывает Toggle и текст Done или In progress. Родитель должен хранить @State private var isCompleted = false
Проверка: переключили toggle в дочернем view, текст у родителя или рядом с ним изменился. Если не изменился, значит вы сделали копию состояния, а не binding
Что может быть еще интересно по этой теме
@Binding хранит данные? Нет. Он указывает на состояние, которое хранится где-то еще
Можно ли передавать binding глубоко вниз? Можно, но если цепочка стала длинной, возможно, нужна другая модель состояния
Чем Binding отличается от ObservableObject? Binding хорош для конкретного значения. Observable-модели нужны, когда состояние сложнее и используется в разных местах
Что дальше после @Binding? Async/await и сетевой запрос. Там состояние экрана будет меняться после загрузки данных
Что почитать дальше по Swift
- SwiftUI: первый экран с Text, Button и @State
- async await в Swift: первый сетевой запрос
- Optionals в Swift: ?, ! и безопасное извлечение
- Массивы, словари и циклы в Swift



