coopersita
coopersita

Reputation: 5041

onChange not getting called when @State var is modified

I'm working on a validation routine for a form, but when the validation results come in, the onChange is not being triggered.

So I have a form that has some fields, and some nested items that have some more fields (the number of items may vary). Think of a form for creating teams where you get to add people.

When the form is submitted, it sends a message to each item to validate itself, and the results of the validation of each item are stored in an array of booleans. Once all the booleans of the array are true, the form is submitted.

Every time a change occurs in the array of results, it should change a flag that would check if all items are true, and if they are, submits the form. But whenever I change the flag, the onChange I have for it never gets called:

final class AddEditProjectViewModel: ObservableObject  {
    @Published var array = ["1", "2",  "3",  "hello"]
    // In reality this array would be a collection of objects with many properties
}

struct AddEditItemView: View {
    @State var text : String
    
    @Binding var doValidation: Bool // flag to perform the item validation
    @Binding var isValid : Bool // result of validating all fields in this item
    
    init(text: String, isValid: Binding<Bool>, doValidation: Binding<Bool>) {
        self._text = State(initialValue: text)
        self._isValid = isValid
        self._doValidation = doValidation
    }
    
    func validateAll() {
        // here would be some validation logic for all form fields, 
        //but I'm simulating the result to all items passed validation
        // Validation needs to happen here because there are error message 
        //fields within the item view that get turned on or off
        isValid = true
    }
    
    var body: some View {
            Text(text)
                .onChange(of: doValidation, perform: { value in
                validateAll() // when the flag changes, perform the validation
            })
    }
}

struct ContentView: View {
    @ObservedObject var viewModel : AddEditProjectViewModel
    @State var performValidateItems : Bool = false // flag to perform the validation of all items
    @State var submitFormFlag = false // flag to  detect when validation results come in
    @State var itemsValidationResult = [Bool]() // store the validation results of each item
    {
        didSet {
            print(submitFormFlag) // i.e. false
            submitFormFlag.toggle() // Even though this gets changed, on changed on it won't get called
            print(submitFormFlag) // i.e. true
        }
    }
    
    init(viewModel : AddEditProjectViewModel) {
        self.viewModel = viewModel
        var initialValues = [Bool]()
        for _ in (0..<viewModel.array.count) { // populate the initial validation results all to false
            initialValues.append(false)
        }
        _itemsValidationResult = State(initialValue: initialValues)
    }
    
    //https://stackoverflow.com/questions/56978746/how-do-i-bind-a-swiftui-element-to-a-value-in-a-dictionary
    func binding(for index: Int) -> Binding<Bool> {
        return Binding(get: {
            return self.itemsValidationResult[index]
        }, set: {
            self.itemsValidationResult[index] = $0
        })
    }
        
    var body: some View {
        HStack {
            ForEach(viewModel.array.indices, id: \.self) { i in
                AddEditItemView(
                    text: viewModel.array[i],
                    isValid: binding(for: i),
                    doValidation: $performValidateItems
                )
        }
            Text(itemsValidationResult.description)
            Button(action: {
                performValidateItems.toggle() // triggers the validation of all items
            }) {
                Text("Validate")
            }
            .onChange(of: submitFormFlag, perform: { value in // this never gets called
                print(value, "forced")
                // if all validation results in the array are true, it will submit the form
            })
        }
    }
}

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

Upvotes: 1

Views: 1091

Answers (1)

pawello2222
pawello2222

Reputation: 54601

You shouldn't use didSet on the @State - it's a wrapper and it doesn't behave like standard properties.

See SwiftUI — @State:

Declaring the @State isFilled variable gives access to three different types:

  • isFilled — Bool
  • $isFilled — Binding
  • _isFilled — State

The State type is the wrapper — doing all the extra work for us — that stores an underlying wrappedValue, directly accessible using isFilled property and a projectedValue, directly accessible using $isFilled property.

Try onChange for itemsValidationResult instead:

var body: some View {
    HStack {
        // ...
    }
    .onChange(of: itemsValidationResult) { _ in
        submitFormFlag.toggle()
    }
    .onChange(of: submitFormFlag) { value in
        print(value, "forced")
    }
}

You may also consider putting the code you had in .onChange(of: submitFormFlag) inside the .onChange(of: itemsValidationResult).

Upvotes: 2

Related Questions