Extracted View in XCode

Глава 7. Обмен данными между структурами Swift

Оглавление

Введение

Обмен данными с помощью 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 переменную, доступную в других структурах.

Для того, чтобы посмотреть как работают привязки, сделаем следующее:

  1. Создадим новое iOs приложение и назовем его Chapter7Binding
  2. Откроем исходный код файла ContentView в панели навигатора
  3. Изменим структуру 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 в отдельную структуру.

  4. Удерживая клавишу Command нажмем на HStack:

    Обмен данными между структурами
    Рис 7.1 Command клик на графическом элементе показывает всплывающее меню
  5. Выберем 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 и не может этого сделать.

  6. Изменим названием нашей новой структуры ExtractedView на DisplayTextField:

    Extracted View in XCode
    Рис 7.2 Изменение названия новой структуры
  7.  Добавим  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. 

  8. Исправим вывод нашей структуры 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)
            }
        }
    }
  9. Нажмем на иконку Preview на панели Canvas
  10. Нажмем на текстовое поле TextField  и попробуем ввести имя – оно тут же отобразится в текстовой метке. Все работает отлично.

В нашем примере, мы разместили вторую структуру в том же исходном файле ContentView. В реальных приложениях структуры View будут размещены в разных исходных файлах с целью упрощения сопровождения кода. Мы продолжим работу с нашим проектом и перенесем нашу структуру DisplayTextField в отдельный исходный файл. Давайте посмотрим, как это сделать лучше всего:

  1. Выберем FileNewFile и перейдем в группу шаблонов User Interface

    SwiftUI Create new User Interface file
    Рис. 7.3 Создание нового SWiftUI файла
  2. Выберем шаблон SwiftUI View и нажмем кнопку Next.
  3. Назовем наш новый файл SwiftUIView(оригинально да) и откроем его в панели Навигатора
  4. Изменим его исходный код:
    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 по умолчанию – константы пустой строки.

  5. Теперь снова вернемся к нашему исходному файлу 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()
        }
    }

     

  6. Нажмем на иконку Preview в панели Canvas.
  7. Снова протестируем наш код – от переноса структуры в другой исходный файл функциональность не изменилась.

Как мы убедились, размещение структур 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, мы сделаем следующее:

  1. Создадим новое iOs приложение и назовем его Chapter7StateObjectApp
  2. Откроем файл ContentView  в панели навигатора
  3. В самом конце файла добавим следующий класс:
    class SongModel: ObservableObject{
        @Published var name: String = ""
        @Published var artist: String = ""
        @Published var duration: Double = 0.0
        @Published var year: Double = 2000
    }

    Не забывайте, что каждое свойство должно быть помечено аннотацией @Published

  4. Изменим структуру 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

  5. Теперь создадим отдельную структуру в одноименном файле – 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())
        }
    }

     

  6. Нажмите на иконку Preview на панели Canvas.
  7. Введите в верхнем поле TextField наименование композиции.
  8. Во втором поле TextField введите имя исполнителя.
  9. Перетаскиванием слайдера укажем продолжительность трека.
  10. Перетаскиванием второго слайдера укажем год выхода композиции. Теперь наш UI выглядит следующим образом:

    Complete User Interface
    Рис 7.4 Итоговый пользовательский интерфейс

Обмен данными с помощью EnvironmentObject

Когда Вы передаете объекты, используя StateObject и ObservedObject, Вы должны передать StateObject объект ObservedObject объекту, расположенному в другой UI структуре. Для более полной ясности приведем рисунок, как это выглядит:

State object must be passed to ObservedObjectю. Обмен данными между структурами
Рис 7.5 StateObject передается в ObservedObject, расположенный в другой структуре

Как мы видим из нашего скриншота 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 что именно мы делаем и ниже мы укажем полное описание, что и как делать с полным исходным кодом:

  1. Откроем наше приложении Chapter7StateObjectApp
  2. Откроем класс ContentView в панели Навигатора
  3. Класс SongModel не претерпел никаких изменений:

    class SongModel: ObservableObject{
        @Published var name: String = ""
        @Published var artist: String = ""
        @Published var duration: Double = 0.0
        @Published var year: Double = 2000
    }

     

  4. Рассмотрим структуру 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). Этот модификатор позволяет неявно передавать модели данных в другие включаемые нами структуры.

  5. Изменим структуру ContentView_Previews
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView(song: SongModel())
        }
    }

     

  6. Незначительно изменим код структуры 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())
        }
    }
    
    
    
  7. Нажмем на иконку Preview панели Canvas
  8. Введем наименование композиции в первое поле TextField
  9. Введем имя исполнителя во втором поле TextField
  10. Первым слайдером установим продолжительность композиции
  11. Вторым слайдером установим год выхода композиции. Мы убедились, что приложение работает именно так, как и ожидалось. Внешний вид приложения будет полностью совпадать с тем, что приведен на рис. 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, к которому мы получаем доступ.

Используя готовые решения от AppleState, Binding, StateObject, ObservedObject и EnvironmentObject, мы можем  получать доступ или обмениваться данными/объектами между структурами, расположенными как в одном, так и в разных файлах.

Leave a Reply

Please disable your adblocker or whitelist this site!