Validation with SwiftUI & Combine – Part 2

Grey is great … but blue is better!

So this is admittedly a little later than planned but I didn’t expect the world to set on fire immediately after I wrote Part 1! Also I have a puppy now. But enough excuses, let’s talk about code …

In Part 1 (if you didn’t read that you’ll probably want to or this isn’t going to make a lot of sense!) we created a reasonably rich SwiftUI form.

We also used Combine to allow highly dynamic displaying of validation errors for each field.

The missing piece is the Submit button, which we also want to react dynamically based on the combined state of our various form fields. At the moment it will show an error once tapped, but that feels very Windows XP!

So let’s add some new publishers to Models.swift:

// MARK: Combined Publishers

// These are split up by section as CombineLatest only supports
// a maximum of 4 input publishers maximum.

lazy var allNamesValidation: ValidationPublisher = {
    Publishers.CombineLatest3(
        firstNameValidation,
        lastNamesValidation,
        birthdayValidation
    ).map { v1, v2, v3 in
        print("firstNameValidation: \(v1)")
        print("lastNamesValidation: \(v2)")
        print("birthdayValidation: \(v3)")
        return [v1, v2, v3].allSatisfy { $0.isSuccess } ? .success : .failure(message: "")
    }.eraseToAnyPublisher()
}()
    
lazy var allAddressValidation: ValidationPublisher = {
    Publishers.CombineLatest3(
        addressHouseNumberOrNameValidation,
        addressFirstLineValidation,
        postcodeValidation
    ).map { v1, v2, v3 in
        print("addressHouseNumberOrNameValidation: \(v1)")
        print("addressFirstLineValidation: \(v2)")
        print("postcodeValidation: \(v3)")
        return [v1, v2, v3].allSatisfy {
            $0.isSuccess } ? .success : .failure(message: "")
    }.eraseToAnyPublisher()
}()

lazy var allValidation: ValidationPublisher = {
    Publishers.CombineLatest(
        allNamesValidation,
        allAddressValidation
    ).map { v1, v2 in
        return [v1, v2].allSatisfy { $0.isSuccess } ? .success : .failure(message: "")
    }.eraseToAnyPublisher()
}()

As it stands, Combine makes this slightly more complicated than is necessary by limiting how many publishers we can combine (per the comment at the top of the code block).

The important part here is that we are combining the results of all of our individual publishers using CombineLatest.

CombineLatest takes the latest messages from its input publishers and publishes a new value based on the value of each message.

A few things of note about how the publishers function in practice:

  1. It needs at least one message from each publisher before it can publish it’s own message.
  2. Once it’s received at least one message from it’s upstream publishers, it will publish a new message when any single one of those publishers publishes a new message.

This is perfect for computing the validation state of our submit button because:

  1. We don’t want to change the state until we have received at least one validation message from each validation publisher.
  2. Once we have at least one message from each validation publisher, we then want it to update every time that any one of those publishers sends a new message.

The submit button reacts to changes in validation state published by the allValidation publisher. First add a new @State variable to ContentView:

@State var isSaveDisabled = true

Then add a modifier to the save button that will bind the activation state (i.e. whether or not the button is disabled) to the isSaveDisabled variable:

Button(action: {}, label: {
    // ... 
})
.disabled(self.isSaveDisabled)

(Note: you may already some of the above from the first tutorial, if so that’s fine!)

Finally we need to update our state variable when a message is received from the allValidation publisher. Add the following modifier to the Form view:

Form {
    // ...
} // ...
.onReceive(model.allValidation) { validation in
    self.isSaveDisabled = !validation.isSuccess
}

That’s it! Run the app, fill in the form with some valid data, and voila 🎉 The save button will enable and disable as the validity of the form changes.

This is just one small example of how SwiftUI and Combine can be used to create reactive UI.

If you want to take this example a bit further, try extending the example with one/all of the following:

  • Create a publisher that collects all of the available validation messages and then modify the form to print them in a bulleted list under the save button.
  • Try adding a slider or stepper to the form, and a new validation publisher to complement it.
  • For a more advanced problem: Try changing the ‘Country’ text field to a picker, and then change the available address fields based on which country is selected. Here are some tips to help you along:
    • Start off small with a couple of countries that don’t share a specific field e.g. ‘State’ for USA, but not for UK.
    • Modify the postcode validation to validate with a different regular expression depending on the country.
    • Ensure that a missing field  is not included as part of the validation for a given country (e.g. ‘State’ when UK is selected).

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.