Introducing Combine

Before implementing the validation logic for our sign-up form, let’s spend some time understanding how the Combine framework works.

According to the Apple documentation:

The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers. ( source)

Let’s take a closer look at a couple of key concepts here to understand what this means and how it helps us.

Publishers

Publishers send values to one or more subscribers. They conform to the Publisher protocol, and declare the type of output and any error they produce:

A publisher can send any number of values over time, or fail with an error. The associated type Output defines which kinds of values a publisher can send, whereas the associated type Failure defines the type of error it may fail with. A publisher can declare it never fails by specifying the Never associated type.

Subscribers

Subscribers, on the other hand, subscribe to one specific publisher instance, and receive a stream of values, until the subscription is canceled.

They conform to the Subscriber protocol. In order to subscribe to a publisher, the subscriber's associated Input and Failure types must conform to the publisher's associated Output and Failure types.

Operators

Publishers and subscribers are the backbones of SwiftUI’s two-way synchronization between the UI and the underlying model. I think you will agree that it has never been easier to keep your UI and model in sync than with SwiftUI, and this is all thanks to this part of the Combine framework.

Operators, however, are Combine’s superpower. They are methods that operate on a Publisher , perform some computation, and produce another Publisher in return.

For example, you can use the filter operator to ignore values based on certain conditions

operator to ignore values based on certain conditions Or, if you need to perform an expensive task (such as fetching information across the network), you could use the debounce operator to wait until the user stops typing

operator to wait until the user stops typing The map operator allows you to transform input values of a certain type into output values of a different type

Validating the Username

With this in mind, let’s implement a simple validation to make sure the user entered a name that has at least three characters.

All properties on our view model are wrapped with the @Published property wrapper. This means each property has its own publisher, which we can subscribe to.

To indicate whether a username is valid, we transform the user’s input from String to Bool using the map operator:

The result of this transformation is then consumed by the assign subscriber, which - as the name implies - assigns the received value to the valid output property of our view model.

Thanks to the binding we configured earlier in ContentView.swift , SwiftUI will automatically update the UI whenever this property changes. We will later see why this approach is a bit problematic, but for now, it works just fine.

You might wonder what’s this fancy business with the debounce and removeDuplicate operators? Well, these are part of what makes Combine such a useful tool for connecting UIs to the underlying business logic. In all user interfaces, we have to deal with the fact that the user might type faster than we can fetch the information they're requesting. For example, when typing their username, it is not necessary to check whether the username is valid or available for every single letter the user types. It is sufficient to perform this check only once they stop typing (or pause for a moment).

The debounce operator lets us specify that we want to wait for a pause in the delivery of events, for example when the user stops typing.

Similarly, the removeDuplicates operator will publish events only if they are different from any previous events. For example, if the user first types john , then joe , and then john again, we will only receive john once. This helps make our UI work more efficiently.

The result of this chain of calls is a Cancellable , which we can use to cancel processing if required (useful for longer-running chains). We'll store this (and all the others that we will create later on) into a Set<AnyCancellable> , so we can clean up upon deinit .

Validating the Password(s)

Let’s now switch gears and look into how we can perform multi-staged validation logic. This is required as the password fields on our form need to meet multiple requirements: they must not be empty, they must match up, and (most importantly) the chosen password must be strong enough. In addition to transforming the input values into a Bool to indicate whether the passwords meet our requirements, we also want to provide some guidance for the user by returning an appropriate warning message.

Let’s take this one step at a time and begin by implementing the pipeline for validating the passwords the user entered.

Checking whether the password is empty is pretty straightforward, and you will notice this method is very similar to our implementation of the username validation. However, instead of directly assigning the result of the transformation to the isValid output property, we return an AnyPublisher<Bool, Never> . This is so we can later combine multiple publishers into a multi-stage chain before we subscribe to the final result (valid or not).

To verify if two separate properties contain equal strings, we make use of the CombineLatest operator. Remember that the properties bound to the respective SecureField fire each time the user enters a character, and we want to compare the latest value for each of those fields. CombineLatest lets us do that.

To compute the password strength, we use Navajo Swift, a Swift port of @mattt’s excellent Navajo library (2)., and convert the resulting enum into a Bool by chaining on another publisher ( isPasswordStrongEnoughPublisher ). This is the first time we subscribe to a one of our own publishers, and very nicely shows how we can combine multiple publishers to produce the required output.

In case you’re wondering why we need to call eraseToAnyPublisher() at the end of each chain: this performs some type erasure that makes sure we don't end up with some crazy nested return types.

Great — so we now know a lot about the passwords the user entered, let’s boil this down to the single thing we really want to know: is this a valid password?

As you might have guessed, we will need to use the CombineLatest operator, but as this time around we have three parameters, we'll use CombineLatest3 , which takes three input parameters.

The main reason why we map the three booleans to a single enum is that we want to be able to produce a suitable warning message depending on the result of the validation. Telling the user that their password is no good is not very helpful, is it? Much better if we tell them why it’s not valid.

Putting it All Together

To compute the final result of the validation, we need to combine the result of the username validation with the result of the password validation. However, before we can do this, we need to refactor the username validation so it also returns a publisher that we include in our validation chain.

With this done, we can implement the final stage of the form validation:

By now, this should look rather familiar to you.

Updating the UI

None of this would be very useful without connecting it to the UI. To drive the state of the Sign up button, we need to update the isValid output property on our view model.

To do so, we simply subscribe to the isFormValidPublisher and assign the values it publishes to the isValid property:

As this code interfaces with the UI, it needs to run on the UI thread. We can tell SwiftUI to execute this code on the UI thread by calling receive(on: RunLoop.main) .

Let’s finish off with binding the warning message output properties to the UI to help guide the user through filling out the sign-up form.

First, we subscribe to the respective publishers to learn when the username vs. password properties are invalid. Again, we need to make sure this happens on the UI thread, so we'll call receive(on:) and pass the main run loop.

Finally, we need to bind the output properties usernameMessage and passwordMessage to the UI. The section footers are a convenient place to show error messages, and we can make them stand out nicely by colouring them in red:

And here is the result of our hard work in all its glory: