@Binding и общий доступ к состоянию

@Binding позволяет дочернему вью читать и изменять состояние, которым владеет вью-родитель. Это двусторонняя связь, а не копия.
Суть урока: @State владеет данными, @Binding — лишь ссылка на чужой источник истины. Знак $ перед @State-свойством создаёт binding для передачи вниз по дереву вью.

Часто кнопке или переключателю в дочернем вью нужно изменить данные родителя. Копировать значение нельзя — копия оторвётся от оригинала. Нужна связь с тем же источником истины. Это и есть @Binding:

struct ParentView: View {
    @State private var isOn = false      // владелец данных

    var body: some View {
        VStack {
            Text(isOn ? "ON" : "OFF")
            ChildToggle(isOn: $isOn)     // $ создаёт binding
        }
    }
}

struct ChildToggle: View {
    @Binding var isOn: Bool              // ссылка, не копия
    var body: some View {
        Toggle("Питание", isOn: $isOn)
    }
}

Главная деталь — знак $. Перед @State-свойством он превращает значение в binding: $isOn — это не сам Bool, а двусторонняя связь с ним. Дочернее вью получает @Binding var isOn и, меняя его, меняет состояние родителя. Так один источник истины остаётся единственным, а управлять им могут несколько вью.

ParentView                         ChildToggle
 @State isOn ()  <-- источник истины
      |  $isOn (binding)
      +-------------------------------->  @Binding isOn
                                              |
      изменение здесь  <----------------------+  меняет ТОТ ЖЕ isOn

Попробуй сам ▶ — запусти код прямо в браузере (Pyodide). Здесь нет Swift, но логика та же, что под капотом мобильного кода:

# Binding как общая изменяемая ссылка на одно значение.
class Box:
    def __init__(self, value):
        self.value = value     # единственный источник истины

def child_toggle(binding):     # получает ссылку, не копию
    binding.value = not binding.value

state = Box(False)             # @State у родителя
child_toggle(state)            # передали $state вниз
print('После переключения:', state.value)   # True — оригинал изменён
child_toggle(state)
print('Ещё раз:', state.value)              # False

Как работает под капотом

Binding — это пара функций: получить значение и установить его (get/set), указывающих на чужое хранилище. Когда дочернее вью пишет в @Binding, оно вызывает setter источника истины родителя, и SwiftUI перерисовывает всех, кто зависит от этого значения. Префикс $ обращается к так называемому projected value обёртки @State, которое как раз и является Binding. Поэтому данные не дублируются — есть один владелец и сколько угодно связей.

Частые ошибки

  • Передать значение вместо $значение. Без $ уйдёт копия, и связь не установится.
  • Объявить @State там, где нужен @Binding. Дочернее вью не владеет данными — ему нужен binding.
  • Создавать несколько источников истины для одних данных. Это рассинхронизирует интерфейс.

Best practices

  • Держите один источник истины (@State) и раздавайте доступ через $-binding.
  • Дочерние вью, меняющие чужие данные, объявляйте с @Binding.
  • Для предпросмотра используйте .constant(значение), чтобы быстро подставить binding.

Итоги. @Binding — двусторонняя связь с чужим источником истины, создаваемая знаком $. Она позволяет дочерним вью менять состояние родителя без дублирования данных. Это второй столп управления состоянием после @State.

Шире контекста

Binding решает фундаментальную проблему любого UI: как дать нескольким частям интерфейса работать с одними и теми же данными без дублирования. Без него каждое дочернее вью получало бы копию, копии расходились бы, и интерфейс врал бы пользователю. Binding устанавливает живую двустороннюю связь: дочернее вью читает актуальное значение и пишет прямо в источник истины родителя. Многие встроенные элементы SwiftUI — Toggle, TextField, Slider, Stepper — спроектированы именно вокруг binding: вы передаёте им $value, и они сами двусторонне синхронизируются с вашим состоянием. Это объясняет вездесущий знак доллара в коде SwiftUI. Полезный приём для предпросмотра и тестов — .constant(значение), создающий фиктивный binding с неизменным значением, когда настоящий источник истины не нужен. Освоив связку @State в родителе и @Binding в ребёнке, вы получаете надёжный паттерн распределения состояния по дереву вью без рассинхронизации.

Проверьте себя
1. Что создаёт знак $ перед @State-свойством?
AКопию значения
BBinding — двустороннюю связь с источником истины
CНовый источник истины
DКонстанту
2. Какой property wrapper использует дочернее вью, чтобы менять данные родителя?
A@State
B@Binding
C@Environment
D@Query