Networking with Combine and SwiftUI
Getting Started
Not keeping the UI up to date across the different parts of an app can result in an infuriatingly bad user experience, and I am sure we all have at least one or two apps in mind that are notorious for this kind of behaviour.
Writing apps that keep the state in sync across the UI and the underlying data model has traditionally been a difficult task, and the development community has come up with plenty of approaches to address this challenge in more or less developer-friendly ways.
Reactive programming is one such approach, and SwiftUI’s reactive state management makes this a lot easier by introducing the notion of a source of truth that can be shared across your app using SwiftUI’s property wrappers such as @EnvironmentObject
, @ObservedObject
, and @StateObject
.
This source of truth usually is your in-memory data model - but as we all know, no application exists in isolation. Most modern apps need to access the network (or other services) at some point, and this means introducing asynchronous behaviour to your app. There are plenty of ways to deal with asynchronous behaviour in our apps: delegate methods, callback handlers, Combine, and async/await, to name just a few.
In this series, we will look at how to use Combine in the context of SwiftUI to
- access the network,
- map data,
- handle errors
… and deal with some advanced scenarios.
Let’s kick things off by looking into how to use Combine to fetch data from a server and map the result to a Swift struct
.
How to fetch data using URLSession
Let’s assume we’re working on a sign up screen for an app, and one of the requirements is to check if the username the user chose is still available in our user database. This requires us to communicate with our authorization server. Here is a request that shows how we might try to find out if the username sjobs is still available:
The server would then reply with a short JSON document stating if the username is still available:
To perform this request in Swift, we can use URLSession
. The traditional way to fetch data from the network using URLSession
looks like this:
And while this code works fine and nothing is inherently wrong with it, it does have a number of issues:
- It’s not immediately clear what the happy path is - the only location that returns a successful result is pretty hidden (1), and developers who are new to using completion handlers might be confused by the fact that the happy path doesn’t even use a
return
statement to deliver the result of the network call to the caller. - Error handling is scattered all over the place (2, 3, 4, 5).
- There are several exit points, and it’s easy to forget one of the
return
statements in theif let
conditions. - Overall, it is hard to read and maintain, even if you’re an experienced Swift developer.
- It’s easy to forget you have to call
resume()
to actually perform the request (6). I am pretty sure most of us have been frantically looking for bugs, only to find out we forgot to actually kick off the request usingresume
. And yes, I thinkresume
is not a great name for an API that is inteded to send the request.
Running the code samples
You will find all the code samples in the accompanying GitHub repository, in the Networking
folder. To be able to benefit the most, I’ve also provided a demo server (built with Vapor) in the server
subfolder. To run it on your machine, do the following:
$ cd server
$ swift run
How to fetch data using Combine
When they introduced Combine, Apple added publishers for many of their own asynchronous APIs. This is great, as this makes it easier for us to use them in our own Combine pipelines.
Now, let’s take a look at how the code looks like after refactoring it to make use of Combine.
This is a lot easier to read already, and (except for the guard
statement that makes sure we’ve got a valid URL) there is just one exit point.
Let’s walk through the code step by step:
- We use
dataTaskPublisher
to perform the request. This publisher is a one-shot publisher and will emit an event once the requested data has arrived. It’s worth keeping in mind that Combine publishers don’t perform any work if there is no subscriber. This means that this publisher will not perform any call to the given URL unless there is at least one subscriber. I will later show you how to connect this pipeline to the UI and make sure it gets called every time the user enters their preferred username. - Once the request returns, the publisher emits a value that contains both the
data
and theresponse
. In this line, we use themap
operator to transform this result. As you can see, we can reuse most of the data mapping code from the previous version of the code, except for a couple of small changes: - Instead of calling the
completion
closure, we can return aBoolean
value to indicate whether the username is still available or not. This value will be passed down the pipeline. - In case the data mapping fails, we catch the error and just return
false
, which seems to be a good compromise. - We do the same for any errors that might occur when accessing the network. This is a simplification that we might need to revisit in the future.
This looks a lot better and easier to read than the initial version, and we could stop here, and integrate this in out application.
But we can do better. Here are three changes that will make the code more linear and easier to reason about:
Destructuring tuples using key paths
We often find ourselves in a situation where we need to extract a specific attribute from a variable. In our example, we receive a tuple containing the data
and the response
of the URL request we sent. Here is the respective declaration in URLSession
:
Combine provides an overloaded version of the map
operator that allows us to destructure the tuple using a key path, and access just the attribute we care for:
Mapping Data more easily
Since mapping data is such a common task, Combine comes with dedicated operator to make this easier: decode(type:decoder:)
.
This will return decode the data
value from the upstream publisher and decode it into a UserNameAvailableMessage
instance.
And finally, we can use the map
operator again to destructure the UserNameAvailableMessage
and access its isAvailable
attribute:
Fetching data using Combine, simplified
With all these changes in place, we now have version of the pipeline that is easy to read, and has a linear flow:
How to connect to SwiftUI
Let’s finish off by looking at how to integrate this new Combine pipeline in our hypothetical sign up form.
Here is a condensed version a sign up form that contains just a username field, a Text
label to display a message, and a sign up button. In a real application, we’d also have some UI elements to provide a password and a password confirmation.
All UI elements are connected to a view model to separate concerns and keep the view clean and easy to read:
Since @Published
properties are Combine publishers, we can subscribe to them to receive updates whenever their value changes. This allows us to call the checkUserNameAvailable
pipeline we created above.
Let’s create a reusable publisher that we can use to drive the parts of our UI that need to display information that depends on whether the username is available or not. One way to do this is to create a lazy computed property. This makes sure the pipeline will only be set up once it is needed, and there will be only one instance of the pipeline.
To call another pipeline and then use its result, we can make use of the flatMap
operator. This will take all input events from an upstream publisher (i.e., the values emitted by the $username
published property), and transform them into a new publisher (in our case, the publisher checkUserNameAvailable
in ).
In the next and final step, we will connect the result of the isUsernameAvailablePublisher
to the UI. If you take a look at the view model, you will notice we’ve got two properties in the output section of the view model: one for any message related to the username, and another one that holds the overall validation state of the form (remember, in a real sign up form, we might need to validate the password fields as well).
Combine publishers can be connected to more than one subscriber, so we can connect both isValid
and usernameMessage
to the isUsernameAvailablePublisher
:
Using this approach allows us to reuse the isUsernameAvailablePublisher
and use it to drive both the overall isValid
state of the form (which will enable / disable the Submit button, and the error message label which informs the user whether their chosen username is still available or not.
How to handle Publishing changes from background threads is not allowed
When you run this code, you will notice a couple of issues:
- The API endpoint gets called several times for each character you type
- Xcode tells you that you shouldn’t update the UI from a background thread
We are going to dive deeper into the reasons for these issues in the next episodes, but for now, let’s address this error message:
[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
The reason for this error message is that Combine will execute the network request on a background thread. When the request is fulfilled, we assign the result to one of the published properties on the view model. This, in turn, will prompt SwiftUI to update the UI - and this will happen on the foreground thread.
To prevent this from happening, we need to instruct Combine to switch to the foreground thread once it has received the result of the network request, using the receive(on:)
operator:
We will look deeper into threading in one of the next episodes when we talk about Combine schedulers.
Closure
In this post, I showed you how to access the network using Combine, and how this enables you to write straight-line code that should be easier to read and maintain than the respective callback-driven counterpart.
We also looked at how to connect a Combine pipeline that makes network requests to SwiftUI by using a view model, and attaching the pipeline to an @Published
property.
Now, you might be wondering why isUsernameAvailablePublisher
uses Never
as its error type - after all, network errors very much are something that we need to deal with.
We will look into error handling (and custom data mapping) in one of the next episodes. We will also look at ways to optimise our Combine-based networking layer, so stay tuned!
Thanks for reading 🔥
Improve your app's UX with SwiftUI's task view modifier
Mastering the art of the pause
SwiftUI Hero Animations with NavigationTransition
Replicating the App Store Hero Animation
Styling SwiftUI Views
How does view styling work?
Previewing Stateful SwiftUI Views
Interactive Previews for your SwiftUI views
Asynchronous programming with SwiftUI and Combine
The Future of Combine and async/await