Совместное использование состояния между компонентами

Рассмотрим следующий сценарий: у вас есть ParentView с двумя дочерними представлениями, ChildView1 и ChildView2. На ChildView1 у вас есть кнопка, которая должна запускать действие в ChildView2.

Теперь, нажав эту кнопку, мы хотим изменить текст в этом текстовом поле на что-то более подходящее. Начнем с определения псевдонима для нашего закрытия. Если вы не знаете, что такое закрытие, это, по сути, метод. Подробнее о замыканиях вы можете прочитать в документации.

Давайте добавим следующие псевдонимы типов над нашим объявлением ParentView:

typealias OnClickHandler = (() -> Void)

Так получается:

typealias OnClickHandler = (() -> Void)

struct ParentView: View {
    ...
}

И инициализируем его как свойство @State в нашем ParentView:

struct ParentView: View {
 
   @State var onClick: OnClickHandler = { }
   ...
}

Идея здесь в том, что этот onClick, определенный в ParentView, является нашим единственным источником истины. Мы не хотим, чтобы где-то в стеке вызовов инициализировалось другое замыкание. Мы хотим, чтобы это было передано в оба наших ChildView.

В ChildView2, где находится наша кнопка, мы добавляем ее как @Binding, поскольку она уже инициализирована в нашем ParentView, а ChildView2 на данный момент только управляет ею. Затем мы добавляем его как действие к нашей кнопке:

Вы заметите, что мы удалили старое замыкание, в котором мы печатали наше сообщение, и просто передали наше в качестве параметра. Это не обязательно, но короче и чище.

На этом этапе ваш ParentView уведомляет вас о том, что вам не хватает параметра onClick при инициализации ChildView2, поэтому давайте просто добавим это:

Вы заметите, что мы передали $onClick в ChildView2, поскольку мы определили наше свойство как @Binding, поэтому мы используем $ для передачи привязки, а не значения.

Теперь мы собираемся сделать то же самое с нашим ChildView1 - добавить свойство привязки - но на этот раз мы также собираемся написать функцию, которая вызывается при нажатии кнопки, и мы собираемся назначить эту функцию для наше пройденное закрытие:

Магия здесь вызывает onAppear элемента Text. Это означает, что когда появится это поле (подумайте о viewDidAppear в UIKit), мы собираемся запустить следующий блок кода. В этом блоке кода мы назначаем нашему закрытию onClick функцию, которая изменяет значение нашей строки.

Если вы хотите выглядеть необычно или ваш метод больше, вы даже можете извлечь весь блок кода в другой метод и назначить его onClick:

Теперь, благодаря волшебству SwiftUI и Combine, вам удалось связать два представления, которые ничего не знают друг о друге. Поздравляю!

Бонус

«Что, если я хочу сделать это с помощью представления и UIViewRepresentable, где у меня нет onAppear

Отличный вопрос, Алекс!

В этом случае мы воспользуемся функцией:

Как видите, я отправил его в фоновый поток. Это связано с тем, что компилятор уведомит нас во время выполнения, что «изменение состояния во время обновления представления приведет к неопределенному поведению».

Это способ Apple сказать, что мы обновляем состояние во время перерисовки представления.

Вот и все. Полный код доступен на GitHub. Удачного кодирования!