Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
114 views
in Technique[技术] by (71.8m points)

swift - Two way binding to multiple instances

I have a view with a ForEach with multiple instances of another view. I want to be able to:

  1. Click a button in the main view and trigger validation on the nested views, and
  2. On the way back, populate an array with the results of the validations

I've simplified the project so it can be reproduced. Here's what I have:


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

When I change the property in the main view, the property doesn't change in the nested views, even though it's a @Published property.

Since this first step is not working, I haven't even been able to test the second part (updating the array of books with the validation results)

I need the setup to be like this because if an item is not valid, that view will show an error message, so the embedded views need to know whether they are valid or not.

UPDATE:

My issue was that you can't seem to be able to store Binding objects in view models, only in views, so I moved my properties to the view, and it works:


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()
    }
}
question from:https://stackoverflow.com/questions/65947373/two-way-binding-to-multiple-instances

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...