首页 > 解决方案 > 两种方式绑定到多个实例

问题描述

我有一个带有 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()
    }
}

标签: swiftswiftui

解决方案


I'm having trouble reconciling the question with the example code and figuring out what's supposed to be happening. Think that there are a few issues going on.

  1. didSet will not get called on @Published properties. You can (SwiftUI - is it possible to get didSet to fire when changing a @Published struct?) but the gist is that it's not a normal property, because of the @propertyWrapper around it

  2. You say in your question that you want a "binding", but you never in fact us a Binding. If you did want to bind the properties together, you should look into using either @Binding or creating a binding without the property wrapper. Here's some additional reading on that: https://swiftwithmajid.com/2020/04/08/binding-in-swiftui/

  3. You have some circular logic in your example code. Like I said, it's a little hard to figure out what's a symptom of the code and what you're really trying to achieve. Here's an example that strips away a lot of the extraneous stuff going on and functions:


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)")
        }
    }
}

Note that it your array, if you include the "nope" item, not everything validates, since there's a non-number, and if you omit it, everything validates.

In your case, there really wasn't the need for that second view model on the detail view. And, if you did have it, at least the way you had things written, it would have gotten you into a recursive loop, as it would've validated, then refreshed the @Published property on the parent view, which would've triggered the list to be refreshed, etc.

If you did get in a situation where you needed to communicate between two view models, you can do that by passing a Binding to the parent's @Published property by using the $ operator:

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")
            }
        }
    }
}

推荐阅读