1

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())
    }
}
coopersita
  • 5,011
  • 3
  • 27
  • 48

1 Answers1

2

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).

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • Thanks. I was trying to use didSet because potentially items could be already valid, so the array of results wouldn't be changing on validation (so on setting I'd be changing a flag), but I changed things around so that the results array is only changed on submission, and not during editing. – coopersita Feb 10 '21 at 00:11