Оглавление
Обмен данными с помощью Bindings
Обмен данными с помощью StateObject и ObservedObject
Обмен данными с помощью EnvironmentObject
Введение
Когда мы создаем простое iOs приложение, мы обычно полагаемся на один единственный исходный файл SwiftUI интерфейса. В пределах этой View структуры мы определяем State переменные, к которым наши элементы пользовательского интерфейса, такие как слайдеры, текстовые поля, текстовые метки, имеют доступ и могут модифицировать значения этих переменных.
Однако в реальных сложных приложениях используется мультиструктура определения интерфейса. То есть один экран может содержать в себе множество структур, к тому же определенных в разных исходных файлах. Когда мы используем мультиструктурный интерфейс, нам нужно придумать, как например, иметь доступ к State переменным из другой структуры или использовать какой то другой способ обмена данными. В этой главе мы научимся различным способам(всего будет описано три) для обмена данными между структурами, размещающимися как в одном исходном файле, так и в разных.
Обмен данными с помощью Bindings
При разработке структуры UI, мы используем State переменные. Когда Вы объявляете State переменную, Вы можете присвоить ей начальное значение и указать область видимости private. Указывая область видимости как private, Вы предполагаете, что эта переменная может быть изменена только в пределах области видимости этой структуры. Обычное определение State переменной выглядит подобным образом:
@State private var message = ""
Для простых приложений Вы можете разместить весь код экрана в одной структуре. Однако в более сложных приложениях потребуется написание большого количества кода. В конце концов исходный файл, содержащий 4 тысячи строк кода будет весьма сложен в понимании и сопровождении.
Решение заключается в том, чтобы разделить пользовательский интерфейс на несколько логически структурированных частей, с которыми можно будет работать нескольким людям и легко сопровождать и изменять код. Эта мультиструктура может быть определена как в одном файле, так и в нескольких. Однако если мы разместим структуры пользовательского интерфейса в разных файлах, все эти структуры, размещенные в разных файлах не смогут иметь доступ к State переменным из других файлов. Для решения этой проблемы мы будем использовать Привязку – или Bindings.
Bindings – это по сути объявление переменной, содержащей данные, и которая будет “привязана” к State переменной, размещенной в другой структуре. И поэтому State переменная может быть преобразована в Binding переменную, доступную в других структурах.
Для того, чтобы посмотреть как работают привязки, сделаем следующее:
- Создадим новое iOs приложение и назовем его Chapter7Binding
- Откроем исходный код файла ContentView в панели навигатора
- Изменим структуру ContentView: View:
struct ContentView: View { @State private var message = "" var body: some View { VStack { Text("Hello, \(message)") HStack{ Text("Send a greeting") TextField("Type a message here", text: $message) } } } }
В этой структуре отображается текстовое поле, где пользователь может ввести имя. Это текстовое поле сохраняет введеный пользователем текст в State переменной и тут же отображает в текстовой метке с приветствием: Hello.
Хотя наш интерфейс выглядит весьма примитивно, если мы захотим добавить больше элементов и оформления, наш код будет выглядеть весьма загроможденным и неуклюжим. Для решения этой проблемы, мы извлечем HStack в отдельную структуру.
- Удерживая клавишу Command нажмем на HStack:
- Выберем Extract Subview. XCode переместит выбранный нами графический элемент в отдельную структуру и заменит ее название на ExtractedView и отобразит ее в конце файла ContentView
struct ExtractedView: View { var body: some View { HStack{ Text("Send a greeting") TextField("Type a message here", text: $message) } } }
Обратите внимание, что среди XCode подсвечивает ошибку – наша отдельная структура пытается получить доступ к State переменной $message и не может этого сделать.
- Изменим названием нашей новой структуры ExtractedView на DisplayTextField:
- Добавим Binding переменную внутри DisplayTextField структуры, назовем ее newVariable и изменим наименование переменной у текстового поля TextField с message на newVariable
struct DisplayTextField: View { @Binding var newVariable: String var body: some View { HStack{ Text("Send a greeting") TextField("Type a message here", text: $newVariable) } } }
Теперь осталось дело за малым: мы должны передать нашей новой структуре DisplayTextField значение переменной message.
- Исправим вывод нашей структуры DisplayTextField() в главной структуре ContentView:
DisplayTextField(newVariable: $message)
Этот код передает значение message переменной нашей Binding newVariable переменной. Обратите внимание, что теперь XCode не сообщает о наличии ошибки в проекте. Итоговый код выглядит следующим образом:
import SwiftUI struct ContentView: View { @State private var message = "" var body: some View { VStack { Text("Hello, \(message)") DisplayTextField(newVariable: $message) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } struct DisplayTextField: View { @Binding var newVariable: String var body: some View { HStack{ Text("Send a greeting") TextField("Type a message here", text: $newVariable) } } }
- Нажмем на иконку Preview на панели Canvas
- Нажмем на текстовое поле TextField и попробуем ввести имя – оно тут же отобразится в текстовой метке. Все работает отлично.
В нашем примере, мы разместили вторую структуру в том же исходном файле ContentView. В реальных приложениях структуры View будут размещены в разных исходных файлах с целью упрощения сопровождения кода. Мы продолжим работу с нашим проектом и перенесем нашу структуру DisplayTextField в отдельный исходный файл. Давайте посмотрим, как это сделать лучше всего:
- Выберем File – New – File и перейдем в группу шаблонов User Interface
- Выберем шаблон SwiftUI View и нажмем кнопку Next.
- Назовем наш новый файл SwiftUIView(оригинально да) и откроем его в панели Навигатора
- Изменим его исходный код:
import SwiftUI struct SwiftUIView: View { @Binding var newVariable: String var body: some View { HStack{ Text("Send a greeting") TextField("Type a message here", text: $newVariable) } } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView(newVariable: .constant("")) } }
Вы уже прекрасно помните этот код из примера выше. Единственное на что мы хотим обратить ваше внимание, что изменилось, так это передача SwiftUIView_Previews в качестве переменной newVariable по умолчанию – константы пустой строки.
- Теперь снова вернемся к нашему исходному файлу ContentView и проделаем следующие манипуляции:
DisplayTextField мы заменим на SwiftUIView и удалим структуру DisplayTextField за ненадобностью. Обновленный код будет выглядеть следующим образом:
import SwiftUI struct ContentView: View { @State private var message = "" var body: some View { VStack { Text("Hello, \(message)") SwiftUIView(newVariable: $message) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
- Нажмем на иконку Preview в панели Canvas.
- Снова протестируем наш код – от переноса структуры в другой исходный файл функциональность не изменилась.
Как мы убедились, размещение структур View возможно как в одном исходном файле, так и в разных. Единственное о чем необходимо помнить – это передача данных Binding переменной в структуру предпросмотра, как мы это сделали :
SwiftUIView(newVariable: .constant(""))
Обмен данными с помощью StateObject и ObservedObject
Переменные типа State обычно используются для хранения простых данных, такие как числа(Int), вещественные числа(тип Double или Float) или текст(String). А переменные типа Binding используются для получения доступа к State переменной из других структур.
Однако, переменные State и Binding могут работать и с индивидуальными типами данных. Что будет, если мы захотим передать объект, который содержит множество значений? Для этих целей необходимо использовать связку StateObject/ObservedObject.
StateObject – похож на State переменную – даже по своему описанию это и переводится как State объект, а ObservedObject по своему назначению похож на Binding переменную, которые описаны в разделе выше. Разница главным образом в том, что State и Binding переменные могут хранить в себе один тип данных, в то время как StateObject и ObservedObject предназначены для работы с объектами, которые могут содержать произвольное количество полей.
Перед тем как использовать StateObject и ObservedObject, нам необходимо создать класс, который будет использоваться в роли передаваемых данных. Этот класс должен расширять ObservedObject, как указано ниже:
class SongModel: ObservableObject
И потом уже Вы можете создать StateObject переменную, которую необходимо будет передать в другую структуру:
@StateObject var song = SongModel()
В заключении Вы передаете StateObject в другую структуру, которая использует аннотацию ObservableObject:
@ObservableObject var creature: SongModel
Для того, чтобы посмотреть как использовать StateObject и ObservableObject, мы сделаем следующее:
- Создадим новое iOs приложение и назовем его Chapter7StateObjectApp
- Откроем файл ContentView в панели навигатора
- В самом конце файла добавим следующий класс:
class SongModel: ObservableObject{ @Published var name: String = "" @Published var artist: String = "" @Published var duration: Double = 0.0 @Published var year: Double = 2000 }
Не забывайте, что каждое свойство должно быть помечено аннотацией @Published
- Изменим структуру struct ContentView: View
struct ContentView: View { @StateObject var song = SongModel() var body: some View { VStack { Text("Song \(song.name)!") Text("Artist: \(song.artist)") Text("Duration: \(Int(song.duration))") Text("Year: \(Int(song.year))") DisplayTextField(creature: song) } } }
Обратите внимание, что структура создает объект класса SongModel – song. И эта переменная аннотирована как StateObject. Не обращайте внимание на ошибку с DisplayTextField – это будет отдельная структура, которая принимает объект класса SongModel
- Теперь создадим отдельную структуру в одноименном файле – DisplayTextField
struct DisplayTextField: View { @ObservedObject var creature: SongModel var body: some View { VStack{ HStack{ Text("Name: ") TextField("Type a name song here", text: $creature.name) } HStack{ Text("Artist name: ") TextField("Type an artist name here", text:$creature.artist) } HStack{ Text("Duration") Slider(value: $creature.duration, in: 0...1200, step: 1.0) } HStack{ Text("Year: ") Slider(value: $creature.year, in: 1900...2100, step: 1.0) } } } }
Обратите внимание, что в отдельной структуре мы определяем ObservedObject переменную с названием сreature. Теперь в отдельной структуре DisplayTextField мы имеем доступ ко всем полям объекта SongModel структуры ContentView – наименование, имя артиста, длительность трека и год написания песни – name, artist, duration, year. Приведем весь исходный код проекта
import SwiftUI struct ContentView: View { @StateObject var song = SongModel() var body: some View { VStack { Text("Song \(song.name)!") Text("Artist: \(song.artist)") Text("Duration: \(Int(song.duration))") Text("Year: \(Int(song.year))") DisplayTextField(creature: song) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } class SongModel: ObservableObject{ @Published var name: String = "" @Published var artist: String = "" @Published var duration: Double = 0.0 @Published var year: Double = 2000 }
import SwiftUI struct DisplayTextField: View { @ObservedObject var creature: SongModel var body: some View { VStack{ HStack{ Text("Name: ") TextField("Type a name song here", text: $creature.name) } HStack{ Text("Artist name: ") TextField("Type an artist name here", text:$creature.artist) } HStack{ Text("Duration") Slider(value: $creature.duration, in: 0...1200, step: 1.0) } HStack{ Text("Year: ") Slider(value: $creature.year, in: 1900...2100, step: 1.0) } } } } struct DisplayTextField_Previews: PreviewProvider { static var previews: some View { DisplayTextField(creature: SongModel()) } }
- Нажмите на иконку Preview на панели Canvas.
- Введите в верхнем поле TextField наименование композиции.
- Во втором поле TextField введите имя исполнителя.
- Перетаскиванием слайдера укажем продолжительность трека.
- Перетаскиванием второго слайдера укажем год выхода композиции. Теперь наш UI выглядит следующим образом:
Обмен данными с помощью EnvironmentObject
Когда Вы передаете объекты, используя StateObject и ObservedObject, Вы должны передать StateObject объект ObservedObject объекту, расположенному в другой UI структуре. Для более полной ясности приведем рисунок, как это выглядит:
Как мы видим из нашего скриншота 7.5 – небольшим недостатком является необходимость явной передачи объекта в другую структуру, реализующую ObservedObject. В случае использования нами EnvironmentObject, такой необходимости нет, а значит эта функциональность предоставляет более простое решение.
Когда мы используем EnvironmentObject, у нас необходимости передачи объекта типа StateObject в каждую используемую нами структуру. Алгоритм в целом похож на использование StatedObject/ObservedObject. Для начала нам необходимо создать модель данных, производную от ObservableObject:
class SongModel: ObservableObject
Далее Мы создаем объект класса SongModel:
@StateObject var song = SongModel()
Далее в этой же UI структуре, где мы объявили StateObject, мы должны определить передаваемый нами StateObject объект через декларацию:
.environmentObject(song)
И в заключении, каждая структура, которая должна получить доступ к объекту song, должна объявить переменную типа EnvironmentObject:
@EnvironmentObject var creature: SongModel
И… в каждой структуре, которая использует EnvironmentObject также необходимо добавить модификатор
.environmentObject(SongModel())
Это общий алгоритм. Даже если Вы ничего не поняли – Вы всегда можете посмотреть на GitHub что именно мы делаем и ниже мы укажем полное описание, что и как делать с полным исходным кодом:
- Откроем наше приложении Chapter7StateObjectApp
- Откроем класс ContentView в панели Навигатора
-
Класс SongModel не претерпел никаких изменений:
class SongModel: ObservableObject{ @Published var name: String = "" @Published var artist: String = "" @Published var duration: Double = 0.0 @Published var year: Double = 2000 }
- Рассмотрим структуру ContentView:
struct ContentView: View { @StateObject var song = SongModel() var body: some View { VStack { Text("Song \(song.name)!") Text("Artist: \(song.artist)") Text("Duration: \(Int(song.duration))") Text("Year: \(Int(song.year))") DisplayTextField() }.environmentObject(song) } }
На что здесь стоит обратить внимание. Мы здесь также используем @StateObject объект типа SongModel. При использовании EnvironmentObject, мы встраиваем структуру DisplayTextField без явной передачи модели данных song. А также мы добавили модификатор .environmentObject(song). Этот модификатор позволяет неявно передавать модели данных в другие включаемые нами структуры.
- Изменим структуру ContentView_Previews
struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(song: SongModel()) } }
- Незначительно изменим код структуры DisplayTextField:
struct DisplayTextField: View { @EnvironmentObject var creature: SongModel var body: some View { VStack{ HStack{ Text("Name: ") TextField("Type a name song here", text: $creature.name) } HStack{ Text("Artist name: ") TextField("Type an artist name here", text:$creature.artist) } HStack{ Text("Duration") Slider(value: $creature.duration, in: 0...1200, step: 1.0) } HStack{ Text("Year: ") Slider(value: $creature.year, in: 1900...2100, step: 1.0) } }.environmentObject(SongModel()) } }
Здесь всего 2 изменения произошло: в структуре DisplayTextField мы определяем объект типа SongModel, аннотированный @EnvironmentObject. Теперь мы имеет доступ ко всем полям передаваемого нам объекта SongModel. И также добавили модификатор – .environmentObject(SongModel()), указывающий среде XCode, что мы в структуре работаем с EnvironmentObject типа SongModel. Исходный код файла ContentView:
// // ContentView.swift // Chapter7StateObjectApp // // Created by Timur on 27.03.2023. // import SwiftUI struct ContentView: View { @StateObject var song = SongModel() var body: some View { VStack { Text("Song \(song.name)!") Text("Artist: \(song.artist)") Text("Duration: \(Int(song.duration))") Text("Year: \(Int(song.year))") DisplayTextField() }.environmentObject(song) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(song: SongModel()) } } class SongModel: ObservableObject{ @Published var name: String = "" @Published var artist: String = "" @Published var duration: Double = 0.0 @Published var year: Double = 2000 }
И также приведем итоговый исходный код файла DisplayTextField
// // DisplayTextField.swift // Chapter7StateObjectApp // // Created by Timur on 27.03.2023. // import SwiftUI struct DisplayTextField: View { @EnvironmentObject var creature: SongModel var body: some View { VStack{ HStack{ Text("Name: ") TextField("Type a name song here", text: $creature.name) } HStack{ Text("Artist name: ") TextField("Type an artist name here", text:$creature.artist) } HStack{ Text("Duration") Slider(value: $creature.duration, in: 0...1200, step: 1.0) } HStack{ Text("Year: ") Slider(value: $creature.year, in: 1900...2100, step: 1.0) } }.environmentObject(SongModel()) } }
- Нажмем на иконку Preview панели Canvas
- Введем наименование композиции в первое поле TextField
- Введем имя исполнителя во втором поле TextField
- Первым слайдером установим продолжительность композиции
- Вторым слайдером установим год выхода композиции. Мы убедились, что приложение работает именно так, как и ожидалось. Внешний вид приложения будет полностью совпадать с тем, что приведен на рис. 7.4
Заключение
Обмен данными между структурами при разработке UI для ОС iOs/ iPadOs/ macOs весьма простая задача, как мы могли убедиться, читая данную главу и изучая примеры кода.
Обычно данные в виде констант и переменных находятся внутри UI структуры и мы их декларируем как @State переменные. Для обмена данными между структурами, мы создаем в одной структуре State переменную и затем используем в другой структуре Binding переменную для получения значения State переменной. State переменная обычно содержит начальное значение, в то время как Binding переменная просто определяет тот же тип State переменно.
State и Binding переменные могут обмениваться простыми типами данных, такие как Int, Double, Float или String. При возникновении необходимости получения доступа к объекту модели данных, необходимо использовать связку StateObject(вместо State переменной) – ObservedObject(вместо Binding переменной). Также нам необходимо создать класс, расширяющий ObservableObject.
Когда мы используем модель передачи данных StateObject-ObservedObject мы должны явно передавать объект в другую структуру. Если вы хотите создавать более гибкие и способные к расширению структуры, идеальным вариантом будет использование EnvironmentObject. Для этого нам необходимо сначала создать StateObject внутри структуры и затем использовать модификатор .environmentObject для указания возможности передачи нашего StateObject объекта другим структурам.
Далее внутри структуры, из которой мы хотим получить доступ, мы объявляем переменную EnvironmentObject и также используем модификатор .environmentObject для идентификации типа объекта StateObject, к которому мы получаем доступ.
Используя готовые решения от Apple – State, Binding, StateObject, ObservedObject и EnvironmentObject, мы можем получать доступ или обмениваться данными/объектами между структурами, расположенными как в одном, так и в разных файлах.