#mvvm #binding #swiftui #viewmodel
#mvvm #привязка #свифтуи #viewmodel
Вопрос:
Я создаю приложение SwiftUI с использованием MVVM. Моя модель представления — это наблюдаемый объект, который я объявляю в родительском представлении как @StateObject, и я передаю привязки дочерним компонентам представления, таким как средства выбора. Я понимаю, что при изменении привязок в дочернем представлении это обновляет модель представления, которая как @StateObject обновляет родительское представление и, в свою очередь, обновляет дочернее представление, и это работает нормально.
Однако, если где-то в главном представлении я передаю модель представления через условный оператор, ни один из дочерних представлений не обновляется при изменении их привязок. Даже если они находятся за пределами условного оператора. Это как если бы для условия была создана новая модель представления.
Мой вопрос в том, почему это происходит и есть ли что-то неправильное в моем подходе? Например, является ли плохой практикой помещать условную логику в представление (если да, то какова наилучшая практика для работы с незначительными вариациями)? Или вам всегда нужно передавать всю модель представления дочерним компонентам, а не только привязкам (если да, то как сделать ваши компоненты повторно используемыми в разных контекстах)? Я нашел некоторые обходные пути, но меня больше интересует понимание проблемы и принятие методов, которые позволят избежать ее в будущем.
Я создал минимально воспроизводимую версию приведенного ниже кода, если условие в представлении содержимого имеет значение true при выборе элементов из средства выбора, представление обновляется. Когда условие ложно, это не так.
Большое спасибо за вашу помощь!
import SwiftUI
class ViewModel: ObservableObject {
@Published var bindingArray: [Int] = [0,1]
}
struct ContentView: View {
@StateObject var viewModel : ViewModel
init(){
_viewModel = StateObject(wrappedValue: ViewModel())
}
var body: some View {
NavigationView {
Form {
// bindings break when the if condition is true (changing it to 1 != 1 will make them work again)
if 1 == 1 {
BlankBindingView(bindingArray: $viewModel.bindingArray)
}
NavigationLink(destination: PickerList(bindingArray: $viewModel.bindingArray)){
Text("Tap me")
}
}
}
}
}
struct BlankBindingView: View {
@Binding var bindingArray: [Int]
var body: some View {
Text("Condition is true, bindings will not work")
}
}
struct PickerList: View {
@Binding var bindingArray: [Int]
@State private var allArray: [Int] = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List {
Text("If the bindings are working checkmarks should appear when you tap a row")
ForEach(allArray, id: .self){ i in
Button(action: {self.bindingArray.append(i)}){
HStack {
Text(String(i))
Spacer()
Image(systemName: self.bindingArray.contains(i) ? "checkmark.circle": "circle")
}
}
}
}
}
}
Ответ №1:
Я бы предложил передать ViewModel в дочернее представление вместо привязки
import SwiftUI
class ViewModel: ObservableObject {
@Published var bindingArray: [Int] = [0,1]
}
struct ContentView: View {
@StateObject var viewModel : ViewModel
init(){
_viewModel = StateObject(wrappedValue: ViewModel())
}
var body: some View {
NavigationView {
Form {
// bindings break when the if condition is true (changing it to 1 != 1 will make them work again)
if 1 == 1 {
BlankBindingView(viewModel: viewModel)
}
NavigationLink(destination: PickerList(viewModel: viewModel)){
Text("Tap me")
}
}
}
}
}
struct BlankBindingView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Text("Condition is true, bindings will not work")
}
}
struct PickerList: View {
@ObservedObject var viewModel: ViewModel
@State private var allArray: [Int] = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List {
Text("If the bindings are working checkmarks should appear when you tap a row")
ForEach(allArray, id: .self){ i in
Button(action: {viewModel.bindingArray.append(i)}){
HStack {
Text(String(i))
Spacer()
Image(systemName: viewModel.bindingArray.contains(i) ? "checkmark.circle": "circle")
}
}
}
}
}
}
Комментарии:
1. спасибо @Volker88 — я рассматривал возможность передачи всей модели представления сборщику, но тогда ее сложнее повторно использовать в других контекстах. На самом деле у меня есть несколько моделей просмотра, а средство выбора — это настраиваемое средство выбора с несколькими вариантами выбора с поиском. Я надеялся, что смогу использовать его свободно, передав привязку, точно так же, как я использую встроенный сборщик. Idk может быть, это просто невозможно — не могли бы вы сказать, что передача всей модели представления является стандартной практикой? Я новичок в MVVM, поэтому пытаюсь разобраться в лучших практиках
Ответ №2:
Похоже, это больше связано с тем фактом, что вы отправляете массив модели представления (не происходит с @State
массивом) двум @Binding
, а не самому условию. Если вы поместите тот же код в else
, он тоже не работает.
Если это сделано специально или ошибка, только Apple может вам сказать. Мне было бы любопытно посмотреть, произойдет ли то же самое с объектом CoreData. Я безуспешно пытался вручную активировать viewModel.objectWillChange.send()
, поэтому я склонен сказать, что это ошибка, о которой стоит сообщить.
Список обновляется (нажмите Back
, а затем вернитесь к List
) View
, он просто не получает обновления.
Передача viewModel
и использование @EnviromentObject
— это обходной путь.
import SwiftUI
class ViewModel: ObservableObject {
@Published var bindingArray: [Int] = [0,1]
}
struct ConditoinalView: View {
@State var bindingArray: [Int] = [0,1]
@StateObject var viewModel : ViewModel
init(){
_viewModel = StateObject(wrappedValue: ViewModel())
}
var body: some View {
NavigationView {
Form {
// bindings break when the if condition is true (changing it to 1 != 1 will make them work again)
if 1 == 1{
//viewModel.objectWillChange.send()
//Does not work if the viewModel.bindingArray is shared by 2 @Binding in the same View
BlankBindingView(bindingArray: $viewModel.bindingArray)
BlankBindingViewEO().environmentObject(viewModel)
BlankBindingViewState(bindingArray: $bindingArray)
}else{
//@Binding will not work either
BlankBindingView(bindingArray: $viewModel.bindingArray)
BlankBindingViewEO().environmentObject(viewModel)
BlankBindingViewState(bindingArray: $bindingArray)
}
//Shared Breaks when shared @Binding
NavigationLink(destination: PickerList(bindingArray: $viewModel.bindingArray, action: {//Tell vm there is a change
viewModel.objectWillChange.send()})
){
Text("Tap me")
}
//Does not break
NavigationLink(destination: PickerListEO().environmentObject(viewModel)
){
Text("Tap me EO")
}
//Does not break
NavigationLink(destination: PickerListState(bindingArray: $bindingArray)){
Text("Tap me State")
}
}
}
}
}
struct BlankBindingView: View {
@Binding var bindingArray: [Int]
var body: some View {
Text("BlankBindingView count = (bindingArray.count)")
Text("Condition is true, bindings will not work")
}
}
struct BlankBindingViewState: View {
@Binding var bindingArray: [Int]
var body: some View {
Text("BlankBindingViewState count = (bindingArray.count)")
Text("Condition is true, bindings WILL work")
}
}
struct BlankBindingViewEO: View {
@EnvironmentObject var vm: ViewModel
var body: some View {
Text("BlankBindingViewEO count = (vm.bindingArray.count)")
Text("Condition is true, bindings WILL work")
}
}
struct PickerList: View {
@Binding var bindingArray: [Int]
@State private var allArray: [Int] = [0,1,2,3,4,5,6,7,8,9]
var action: () -> Void
var body: some View {
List {
//The array is being updated (go back and then return) but the View isnt being updated
Text("If the bindings are working checkmarks should appear when you tap a row")
Text("PickerList count = (bindingArray.count)")
ForEach(allArray, id: .self){ i in
Button(action: {self.bindingArray.append(i)
//Tell VM there is a change
action()
}){
HStack {
Text(String(i))
Spacer()
Image(systemName: self.bindingArray.contains(i) ? "checkmark.circle": "circle")
}
}
}
}
}
}
struct PickerListEO: View {
@EnvironmentObject var vm: ViewModel
@State private var allArray: [Int] = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List {
Text("If the bindings are working checkmarks should appear when you tap a row")
Text("PickerListEO count = (vm.bindingArray.count)")
ForEach(allArray, id: .self){ i in
Button(action: {self.vm.bindingArray.append(i)}){
HStack {
Text(String(i))
Spacer()
Image(systemName: self.vm.bindingArray.contains(i) ? "checkmark.circle": "circle")
}
}
}
}
}
}
struct PickerListState: View {
@Binding var bindingArray: [Int]
@State private var allArray: [Int] = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List {
Text("If the bindings are working checkmarks should appear when you tap a row")
Text("PickerListState count = (bindingArray.count)")
ForEach(allArray, id: .self){ i in
Button(action: {self.bindingArray.append(i)}){
HStack {
Text(String(i))
Spacer()
Image(systemName: self.bindingArray.contains(i) ? "checkmark.circle": "circle")
}
}
}
}
}
}
struct ConditoinalView_Previews: PreviewProvider {
static var previews: some View {
ConditoinalView()
}
}
Комментарии:
1. Спасибо loremipsum — я не уверен, что проблема может быть связана с отправкой виртуальной машины в две привязки, потому что, если вы удалите условное предложение из исходного кода и продублируете средство выбора несколько раз, все они будут работать. Интересно, что вы упомянули coredata — в моем исходном коде есть массив объектов coredata вместо массива целых чисел, и это тоже не работает. К сожалению, я не могу использовать объект среды, поскольку мне нужно создать несколько экземпляров модели представления.
2. Это похоже на форму и условное условие, похоже, это не происходит в VStack
3. о, это отличное место — даже просто перенос условного оператора в vstack и сохранение формы работает. очень практичный обходной путь. значит, ты думаешь, что это просто ошибка Apple?
4. Возможно, или что-то связанное с функциями формы