【问题标题】:Two way binding to multiple instances两种方式绑定到多个实例
【发布时间】:2021-05-02 22:53:30
【问题描述】:

我有一个带有 ForEach 的视图,其中包含另一个视图的多个实例。我希望能够:

  1. 单击主视图中的按钮并触发嵌套视图的验证,然后
  2. 在返回的路上,用验证结果填充一个数组

我已经简化了项目,以便可以复制它。这是我所拥有的:


import SwiftUI


final class AddEditItemViewModel: ObservableObject  {
    @Published var item : String
    @Published var isValid : Bool 
    @Published var doValidate: Bool {
        didSet{
            print(doValidate) // This is never called
            validate()
        }
    }
    
    init(item : String, isValid : Bool, validate: Bool) {
        self.item = item
        self.isValid  = isValid
        self.doValidate = validate 
    }
    
    func validate() {  // This is never called
        isValid = Int(item) != nil
    }
}

struct AddEditItemView: View {
    @ObservedObject var viewModel : AddEditItemViewModel
    
    var body: some View {
            Text(viewModel.item)
    }
    
}

final class AddEditProjectViewModel: ObservableObject  {
    let array = ["1", "2", "3", "nope"]
    @Published var countersValidationResults = [Bool]()
    @Published var performValidation = false
    init() {
        for _ in array {
            countersValidationResults.append(false)
        }
    }
}

struct ContentView: View {
    @ObservedObject var viewModel : AddEditProjectViewModel
    @State var result : Bool = false
    var body: some View {
        VStack {
            ForEach(
                viewModel.countersValidationResults.indices, id: \.self) { i in
                    AddEditItemView(viewModel: AddEditItemViewModel(
                        item: viewModel.array[i], 
                        isValid: viewModel.countersValidationResults[i], 
                        validate: viewModel.performValidation
                    )
                )
            }
            Button(action: {
                viewModel.performValidation = true
                result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
            }) {
                Text("Validate")
            }
            Text("All is valid: \(result.description)")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(viewModel: AddEditProjectViewModel())
    }
}

当我在主视图中更改属性时,嵌套视图中的属性不会更改,即使它是 @Published 属性。

由于第一步不起作用,我什至无法测试第二部分(使用验证结果更新书籍数组)

我需要这样的设置,因为如果项目无效,该视图将显示错误消息,因此嵌入式视图需要知道它们是否有效。

更新:

我的问题是您似乎无法将绑定对象存储在视图模型中,只能存储在视图中,因此我将属性移到了视图中,并且它可以工作:


import SwiftUI


final class AddEditItemViewModel: ObservableObject  {
    @Published var item : String
    
    init(item : String) {
        self.item = item
        print("item",item)
    }
    
    func validate() -> Bool{
        return Int(item) != nil
    }
}

struct AddEditItemView: View {
    @ObservedObject var viewModel : AddEditItemViewModel
    
    @Binding var doValidate: Bool
    @Binding var isValid : Bool
    
    init(viewModel: AddEditItemViewModel, doValidate:Binding<Bool>, isValid : Binding<Bool>) {
        self.viewModel = viewModel
        self._doValidate = doValidate
        self._isValid  = isValid
    }
    var body: some View {
        Text("\(viewModel.item): \(isValid.description)").onChange(of: doValidate)  {  _ in isValid = viewModel.validate() }
    }
    
}

struct ContentView: View {
    @State var performValidation = false
    @State var countersValidationResults = [false,false,false,false] // had to hard code this here
    @State var result : Bool = false
    let array = ["1", "2", "3", "nope"]
    
//      init() {
//          for _ in array {
//            countersValidationResults.append(false) // For some weird reason this appending doesn't happen!
//          }
//      }
  
    var body: some View {
        VStack {
            ForEach(array.indices, id: \.self) { i in
                AddEditItemView(viewModel: AddEditItemViewModel(item: array[i]), doValidate: $performValidation, isValid: $countersValidationResults[i])
        }
            Button(action: {
                performValidation.toggle()
                result = countersValidationResults.filter{ $0 == false }.count == 0
            }) {
                Text("Validate")
            }
            Text("All is valid: \(result.description)")
            Text(countersValidationResults.description)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    我无法用示例代码协调问题并弄清楚应该发生的事情。认为有一些问题正在发生。

    1. didSet 不会在 @Published 属性上被调用。你可以(SwiftUI - is it possible to get didSet to fire when changing a @Published struct?),但要点是它不是一个普通的属性,因为它周围有@propertyWrapper

    2. 你在你的问题中说你想要一个“绑定”,但实际上你从来没有给我们Binding。如果您确实想将属性绑定在一起,则应该考虑使用 @Binding 或创建没有属性包装器的绑定。这里有一些额外的阅读:https://swiftwithmajid.com/2020/04/08/binding-in-swiftui/

    3. 您的示例代码中有一些循环逻辑。就像我说的,要弄清楚代码的症状是什么以及你真正想要实现的目标有点困难。这是一个去掉了很多无关的东西和功能的例子:

    
    struct AddEditItemView: View {
        var item : String
        var isValid : Bool
        
        var body: some View {
                Text(item)
        }
        
    }
    
    final class AddEditProjectViewModel: ObservableObject  {
        let array = ["1", "2", "3"]// "nope"]
        @Published var countersValidationResults = [Bool]()
        init() {
            for _ in array {
                countersValidationResults.append(false)
            }
        }
        
        func validate(index: Int) {  // This is never called
            countersValidationResults[index] = Int(array[index]) != nil
        }
    }
    
    struct ContentView: View {
        @ObservedObject var viewModel : AddEditProjectViewModel
        @State var result : Bool = false
        var body: some View {
            VStack {
                ForEach(
                    viewModel.countersValidationResults.indices, id: \.self) { i in
                    AddEditItemView(item: viewModel.array[i], isValid: viewModel.countersValidationResults[i])
                }
                Button(action: {
                    viewModel.array.enumerated().forEach { (index,_) in
                        viewModel.validate(index: index)
                    }
                    result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
                }) {
                    Text("Validate")
                }
                Text("All is valid: \(result.description)")
            }
        }
    }
    

    请注意,它是您的 array,如果您包含 "nope" 项目,则并非所有内容都会验证,因为有一个非数字,如果您省略它,则所有内容都会验证。

    在您的情况下,在详细视图上确实不需要第二个视图模型。而且,如果您确实拥有它,至少按照您编写内容的方式,它会让您进入递归循环,因为它会验证,然后刷新父视图上的 @Published 属性,这将触发要刷新的列表等

    如果您确实遇到了需要在两个视图模型之间进行通信的情况,您可以通过使用 $ 运算符将 Binding 传递给父级的 @Published 属性来做到这一点:

    class ViewModel : ObservableObject {
        @Published var isValid = false
    }
    
    struct ContentView : View {
        @ObservedObject var viewModel : ViewModel
        
        var body: some View {
            VStack {
                ChildView(viewModel: ChildViewModel(isValid: $viewModel.isValid))
            }
        }
    }
    
    class ChildViewModel : ObservableObject {
        var isValid : Binding<Bool>
        
        init(isValid: Binding<Bool>) {
            self.isValid = isValid
        }
        
        func toggle() {
            isValid.wrappedValue.toggle()
        }
    }
    
    struct ChildView : View {
        @ObservedObject var viewModel : ChildViewModel
        
        var body: some View {
            VStack {
                Text("Valid: \(viewModel.isValid.wrappedValue ? "true" : "false")")
                Button(action: {
                    viewModel.toggle()
                }) {
                    Text("Toggle")
                }
            }
        }
    }
    

    【讨论】:

    • 谢谢,我认为您不能在视图模型中使用 @Bidning 属性,只能在视图中使用,因此将这些属性移动到视图并将它们设置为绑定解决了我的问题。我确实需要这两个值(一个是触发验证的标志,另一个存储验证的结果)。
    猜你喜欢
    • 1970-01-01
    • 2016-02-27
    • 2017-09-05
    • 2014-02-13
    • 1970-01-01
    • 1970-01-01
    • 2019-06-27
    • 1970-01-01
    • 2011-08-23
    相关资源
    最近更新 更多