One of the biggest announcements at WWDC 2019 was SwiftUI — its declarative approach makes building UIs a breeze, and it’s easy to see why people are so excited about it. The hidden gem, however, was the Combine framework, which didn’t get as much fanfare as I think it deserves.
Apple have put their sign of approval on Functional Reactive Programming, and soon it will no longer be a technique that only a few development teams use.
In this article, we will take a closer look at how to use SwiftUI and Combine together, build better apps, and have more fun along the way.
Ready? Let’s go!
What you are going to learn
- What the Combine framework is, and how to integrate it with SwiftUI
- What publishers, subscribers, and operators are, and how to use them
- How to organize your code
To help us reason about SwiftUI and Combine, we’re going to use a simple sign-up screen which lets users enter a username and password to create a new account in an application. In a later article, we will add a login screen to demonstrate some of the additional benefits of using Combine.
The data model for the sign-up screen is simple enough:
- Users need to enter their desired username
- They also need to pick a password
The requirements for username and password are pretty straight forward:
- The username must contain at least 3 characters
- The password must be non-empty and strong enough
- Also, to make sure the user didn’t accidentally mistype, they need to type their password a second time, and both of these passwords need to match up
Let’s put this down in code!
I’ve decided to use an MVVM architecture — this results in a clean code base and will make it easier to add new functionality to the app. First, let’s define the ViewModel which has a couple of properties taking the user’s input (such as the username and passwords), and — for the time being — a property to expose the result of any business logic we’re going to implement shortly.
For the sign-up screen, we use a
Form with several
Section s for the various input fields, which gives us a clean look and feel. It gets the job done, but doesn't look very exciting. In the next episode, we're going to brush it up to demonstrate how SwiftUI and Combine make it possible to make changes to your UI without having to modify the underlying business logic.
Notice how we’re using SwiftUI bindings to access the properties of our view model. The Sign up button is bound to the
isValid output property of the view model. As this defaults to
false, the button is initially disabled, which is what we want - after all, the user shouldn't be able to create an account with an empty username and password!
This is how the UI looks so far:
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 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, 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
Failure types must conform to the publisher's associated
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
filteroperator 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
debounceoperator to wait until the user stops typing
mapoperator 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
Bool using the
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
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).
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.
removeDuplicates operator will publish events only if they are different from any previous events. For example, if the user first types
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
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
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
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
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
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:
Building UIs with SwiftUI is a breeze. Apple has sweated the details to give us the tools that make building UIs more productive than ever before. On top of that, SwiftUI follows Apple’s Human Interface Guidelines , automatically adapts to dark mode and has accessibility built right in. All of this helps to build better, and more inclusive apps in less time — what’s not to like?
Using Combine results in a cleaner and more modular code that (as we will see in the next episode) is more maintainable and easier to extend.
Of course, like every new paradigm, there is a learning curve, and it will take some time to get to grips with functional reactive programming. But I am convinced it’s worth the effort. By releasing SwiftUI and Combine, Apple have put their sign of approval on Functional Reactive Programming, and soon it will no longer be a technique that only a few development teams use.
We will see more and more learning resources to help people get started. Also (and this has been a bit of a sore point in the latest beta releases of Xcode), tooling will get better over time, helping developers to be more productive.
Now is a great time to get started with SwiftUI and Combine — try using them in one of your next projects to get a head start!