Как наилучшим образом передавать данные для редактирования формы в swuiftui (при этом эти данные доступны для родительского представления и других вложенных представлений)?

#ios #swift #swiftui

#iOS #swift #swiftui

Вопрос:

У меня есть форма SwiftUI в MyFormSubView.swift, где у меня есть несколько переменных @State, представляющих отдельные поля, такие как текст и т.д. Моя проблема в том, что моему родительскому представлению «contentView.swift» также необходим доступ к этой информации, а другие вложенные представления «otherView.swift» также получат доступ для отображения или редактирования. Мой текущий подход заключается в том, чтобы изменить все @State на @Binding, что создает головную боль, потому что некоторые формы могут содержать до 20 полей с некоторыми необязательными … каков наилучший способ справиться с этим? Есть ли способ просто передать объект и сделать его «редактируемым»?

Подходы:

  1. (Текущий, проблемный подход) Имейте несколько отдельных переменных, объявленных как @State в contentView.swift, и передавайте каждую отдельную переменную в MyFormSubView.swift с теми переменными, которые имеют @Binding перед ними, которые сопоставляются с элементами swiftui, Чтобы отображаться как «текст-заполнитель» в текстовых полях и т. Д. Это плохо, поскольку у меня потенциально может быть до 30 полей, некоторые из которых являются необязательными.
  2. (То, что я думаю, я желаю) Имейте идентифицируемую модель со всеми полями (и, возможно, передайте эту модель в MyFormSubView.swift, и если это возможно, привяжитесь к ней и просто сделайте так, чтобы каждое поле было $ mymodel .field1, $mymodel.field2 и т. Д. … Что устраняет необходимость передачи более 30 переменных в эту вещь.
  3. (Может быть, лучше?) Используйте @ObservableObject .

Возможно ли # 2? Или есть еще лучший способ? Пример кода был бы отличным!

Комментарии:

1. Роландо, ты можешь создать структуру, которая превратит тебя в объект состояния №2. Затем вы можете передать один объект в из своей формы. Сделать его наблюдаемым объектом № 3 было бы еще лучше. Я постараюсь написать это сегодня и выложу что-нибудь позже.

2. Роландо, опубликовал статью о medium.com со всеми возможными вариантами. Прочтите это здесь. medium.com/codestory /…

Ответ №1:

Существует несколько способов передачи подобных данных между представлениями. Вот краткая реализация, описывающая 4 подхода.

  1. Вы можете использовать an @ObservableObject для ссылки на класс со всеми вашими данными внутри. Переменные есть @Published , что позволяет представлению обновляться таким же образом @State , как и переменная.
  2. Вы можете использовать @StateObject . Это то же самое @ObservableObject , что и, за исключением того, что он будет инициализироваться только один раз, и если представление повторно отобразит переменную, она сохранится (в то время как an @ObservedObject будет повторно инициализироваться). Подробнее о различии читайте здесь.
  3. Вы можете использовать @EnvironmentObject . Это то же самое @ObservedObject , что и, за исключением того, что оно хранится в среде, поэтому вам не нужно вручную передавать его между представлениями. Это лучше всего, когда у вас сложная иерархия представлений и не каждое представление нуждается в ссылке на данные.
  4. Вы можете создать пользовательскую модель и использовать @State переменную.

Все эти методы работают, но, основываясь на вашем описании, я бы сказал, что 2-й метод, вероятно, лучше всего подходит для вашей ситуации.

 class DataViewModel: ObservableObject {
    @Published var text1: String = "One"
    @Published var text2: String = "Two"
    @Published var text3: String = "Three"
}

struct DataModel {
    var text1: String = "Uno"
    var text2: String = "Dos"
    var text3: String = "Tres"
}

struct AppView: View {
    var body: some View {
        MainView()
            .environmentObject(DataViewModel())
    }
}

struct MainView: View {
    
    @StateObject var dataStateViewModel = DataViewModel()
    @ObservedObject var dataObservedViewModel = DataViewModel()
    @EnvironmentObject var dataEnvironmentViewModel: DataViewModel
    @State var dataStateModel = DataModel()
    
    @State var showSheet: Bool = false
    @State var showOtherView: Bool = false

    var body: some View {
        VStack(spacing: 20) {
            
            Text(dataStateViewModel.text1)
                .foregroundColor(.red)
            
            Text(dataObservedViewModel.text2)
                .foregroundColor(.blue)
            
            Text(dataEnvironmentViewModel.text3)
                .foregroundColor(.green)

            Text(dataStateModel.text1)
                .foregroundColor(.purple)

            Button(action: {
                showSheet.toggle()
            }, label: {
                Text("Button 1")
            })
            .sheet(isPresented: $showSheet, content: {
                FormView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
                    .environmentObject(dataEnvironmentViewModel) // Sheet is a new environment
            })
            
            Button(action: {
                showOtherView.toggle()
            }, label: {
                Text("Button 2")
            })
            
            if showOtherView {
                ThirdView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
            }
        }
        
    }
    
}

struct FormView: View {
    @StateObject var dataStateViewModel: DataViewModel
    @ObservedObject var dataObservedViewModel: DataViewModel
    @EnvironmentObject var dataEnvironmentViewModel: DataViewModel
    @Binding var dataStateModel: DataModel
    @Environment(.presentationMode) var presentationMode
    
    var body: some View {
        Form(content: {
            Button(action: {
                presentationMode.wrappedValue.dismiss()
            }, label: {
                Text("BACK")
            })
            Text("EDIT TEXT FIELDS:")

            TextField("Placeholder 1", text: $dataStateViewModel.text1)
                .foregroundColor(.red)

            TextField("Placeholder 2", text: $dataObservedViewModel.text2)
                .foregroundColor(.blue)

            TextField("Placeholder 3", text: $dataEnvironmentViewModel.text3)
                .foregroundColor(.green)
            
            TextField("Placeholder 4", text: $dataStateModel.text1)
                .foregroundColor(.purple)
        })
    }
}

struct ThirdView: View {
    @StateObject var dataStateViewModel: DataViewModel
    @ObservedObject var dataObservedViewModel: DataViewModel
    @EnvironmentObject var dataEnvironmentViewModel: DataViewModel
    @Binding var dataStateModel: DataModel
    var body: some View {
        VStack(spacing: 20) {
            Text(dataStateViewModel.text1)
                .foregroundColor(.red)
            
            Text(dataObservedViewModel.text2)
                .foregroundColor(.blue)
            
            Text(dataEnvironmentViewModel.text3)
                .foregroundColor(.green)

            Text(dataStateModel.text1)
                .foregroundColor(.purple)
        }
    }
}
 

Комментарии:

1. Это действительно полезно, но как справиться с одним элементом вопроса OP: «… при этом некоторые из них являются необязательными «?

Ответ №2:

Используйте ObservalbeObject, вот простой пример того, как вы можете обмениваться данными: Шаг 1 — Добавьте какое-то состояние:

 class AppState: ObservableObject {
    @Published var value: String = ""
}
 

Шаг 2 — Передать состояние в contentView с помощью настройки enviromentObject

     ContentView()
            .environmentObject(AppState())
 

Шаг 3 — Теперь AppState будет доступен во всех дочерних представлениях contentView, поэтому вот код для contentView и otherView. В otherView есть текстовое поле, текст из которого будет сохранен в AppState, и вы сможете его увидеть, когда нажмете «назад» из otherView.

contentView:

 import SwiftUI

struct ContentView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        NavigationView {
            VStack {
                Text("Content View")
                        .padding()

                NavigationLink(
                        destination: OtherView()
                ) {
                    Text("Open other view")
                }

                Text("Value from other view: (appState.value)")
            }
        }
    }
}
 

Другой вид:

 import SwiftUI

struct OtherView: View {
    @EnvironmentObject var appState: AppState
    @State var value: String = ""
    
    var body: some View {
        VStack {
            Text("Other View")
                    .padding()

            TextField("Enter value", text: Binding<String>(
                    get: { self.value },
                    set: {
                        self.value = $0
                        appState.value = $0
                    }
            ))
                    .frame(width: 200)
                    .padding()
        }
    }
}
 

Это всего лишь простой пример, для более сложных случаев вы можете взглянуть на шаблоны VIPER или MVVM в Swift UI. Например, здесь:
https://www.raywenderlich.com/8440907-getting-started-with-the-viper-architecture-pattern
https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios

Комментарии:

1. Является ли «хорошей практикой» поддерживать единый Observable EnvironmentObject «AppState», который поддерживает любые переменные для всего приложения? Я знаю, что возможно иметь несколько ObservableObjects, но лучше ли иметь только один?

2. Это зависит от того, как вы обрабатываете данные и изменения пользовательского интерфейса, какие части пользовательского интерфейса должны меняться, что не должно и т.д. Для небольших приложений с простым интерфейсом и примерами — почему бы и нет. Для сложных приложений существует множество других шаблонов. Например, каждый небольшой модуль приложения может иметь свое собственное «состояние».