Combine: Getting Started
Learn how to use Combine’s Publisher and Subscriber to handle event streams, merge multiple publishers and more. By Fabrizio Brancati.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Combine: Getting Started
20 mins
- Getting Started
- Introduction to Combine
- Publishers
- Operators
- Subscribers
- Putting it together
- Networking with Combine
- Downloading an Image With Combine
- Using Zip
- Adding a Score
- Using Timers in Combine
- Refining the App
- I want to Combine All The Things Now!
- Older iOS Versions
- Your Team
- Other SDKs
- Gradual Integration
- Where to Go From Here?
Combine, announced at WWDC 2019, is Apple’s new “reactive” framework for handling events over time. You can use Combine to unify and simplify your code for dealing with things like delegates, notifications, timers, completion blocks and callbacks. There have been third-party reactive frameworks available for some time on iOS, but now Apple has made its own.
In this tutorial, you’ll learn how to:
- Use
Publisher
andSubscriber
. - Handle event streams.
- Use
Timer
the Combine way. - Identify when to use Combine in your projects.
You’ll see these key concepts in action by enhancing FindOrLose, a game that challenges you to quickly identify the one image that’s different from the other three.
Ready to explore the magic world of Combine in iOS? Time to dive in!
Getting Started
Download the project materials using the Download Materials button at the top or bottom of this tutorial.
Open the starter project and check out the project files.
Before you can play the game, you must register at Unsplash Developers Portal to get an API key. After registration, you’ll need to create an app on their developer’s portal. Once complete, you’ll see a screen like this:
Open UnsplashAPI.swift and add your Unsplash API key to UnsplashAPI.accessToken
like this:
enum UnsplashAPI {
static let accessToken = "<your key>"
...
}
Build and run. The main screen shows you four gray squares. You’ll also see a button for starting or stopping the game:
Tap Play to start the game:
Right now, this is a fully working game, but take a look at playGame()
in GameViewController.swift. The method ends like this:
}
}
}
}
}
}
That’s too many nested closures. Can you work out what’s happening, and in what order? What if you wanted to change the order things happen in, or bail out, or add new functionality? Time to get some help from Combine!
Introduction to Combine
The Combine framework provides a declarative API to process values over time. There are three main components:
- Publishers: Things that produce values.
- Operators: Things that do work with values.
- Subscribers: Things that care about values.
Taking each component in turn:
Publishers
Objects that conform to Publisher
deliver a sequence of values over time. The protocol has two associated types: Output
, the type of value it produces, and Failure
, the type of error it could encounter.
Every publisher can emit multiple events:
- An output value of
Output
type. - A successful completion.
- A failure with an error of
Failure
type.
Several Foundation types have been enhanced to expose their functionality through publishers, including Timer
and URLSession
, which you’ll use in this tutorial.
Operators
Operators are special methods that are called on publishers and return the same or a different publisher. An operator describes a behavior for changing values, adding values, removing values or many other operations. You can chain multiple operators together to perform complex processing.
Think of values flowing from the original publisher, through a series of operators. Like a river, values come from the upstream publisher and flow to the downstream publisher.
Subscribers
Publishers and operators are pointless unless something is listening to the published events. That something is the Subscriber
.
Subscriber
is another protocol. Like Publisher
, it has two associated types: Input
and Failure
. These must match the Output
and Failure
of the publisher.
A subscriber receives a stream of value, completion or failure events from a publisher.
Putting it together
A publisher starts delivering values when you call subscribe(_:)
on it, passing your subscriber. At that point, the publisher sends a subscription to the subscriber. The subscriber can then use this subscription to make a request from the publisher for a definite or indefinite number of values.
After that, the publisher is free to send values to the Subscriber. It might send the full number of requested values, but it might also send fewer. If the publisher is finite, it will eventually return the completion event or possibly an error. This diagram summarizes the process:
Networking with Combine
That gives you a quick overview of Combine. Time to use it in your own project!
First, you need to create the GameError
enum to handle all Publisher
errors. From Xcode’s main menu, select File ▸ New ▸ File… and choose the template iOS ▸ Source ▸ Swift File.
Name the new file GameError.swift and add it to the Game folder.
Now add the GameError
enum:
enum GameError: Error {
case statusCode
case decoding
case invalidImage
case invalidURL
case other(Error)
static func map(_ error: Error) -> GameError {
return (error as? GameError) ?? .other(error)
}
}
This gives you all of the possible errors you can encounter while running the game, plus a handy function to take an error of any type and make sure it’s a GameError
. You’ll use this when dealing with your publishers.
With that, you’re now ready to handle HTTP status code and decoding errors.
Next, import Combine. Open UnsplashAPI.swift and add the following at the top of the file:
import Combine
Then change the signature of randomImage(completion:)
to the following:
static func randomImage() -> AnyPublisher<RandomImageResponse, GameError> {
Now, the method doesn’t take a completion closure as a parameter. Instead, it returns a publisher, with an output type of RandomImageResponse
and a failure type of GameError
.
AnyPublisher
is a system type that you can use to wrap “any” publisher, which keeps you from needing to update method signatures if you use operators, or if you want to hide implementation details from callers.
Next, you’ll update your code to use URLSession
‘s new Combine functionality. Find the line that begins session.dataTask(with:
. Replace from that line to the end of the method with the following code:
// 1
return session.dataTaskPublisher(for: urlRequest)
// 2
.tryMap { response in
guard
// 3
let httpURLResponse = response.response as? HTTPURLResponse,
httpURLResponse.statusCode == 200
else {
// 4
throw GameError.statusCode
}
// 5
return response.data
}
// 6
.decode(type: RandomImageResponse.self, decoder: JSONDecoder())
// 7
.mapError { GameError.map($0) }
// 8
.eraseToAnyPublisher()
This looks like a lot of code, but it’s using a lot of Combine features. Here’s the step-by-step:
- You get a publisher from the URL session for your URL request. This is a
URLSession.DataTaskPublisher
, which has an output type of(data: Data, response: URLResponse)
. That’s not the right output type, so you’re going to use a series of operators to get to where you need to be. - Apply the
tryMap
operator. This operator takes the upstream value and attempts to convert it to a different type, with the possibility of throwing an error. There is also amap
operator for mapping operations that can’t throw errors. - Check for
200 OK
HTTP status. - Throw the custom
GameError.statusCode
error if you did not get a200 OK
HTTP status. - Return the
response.data
if everything is OK. This means the output type of your chain is nowData
- Apply the
decode
operator, which will attempt to create aRandomImageResponse
from the upstream value usingJSONDecoder
. Your output type is now correct! - Your failure type still isn’t quite right. If there was an error during decoding, it won’t be a
GameError
. ThemapError
operator lets you deal with and map any errors to your preferred error type, using the function you added toGameError
. - If you were to check the return type of
mapError
at this point, you would be greeted with something quite horrific. The.eraseToAnyPublisher
operator tidies all that mess up so you’re returning something more usable.
Now, you could have written almost all of this in a single operator, but that’s not really in the spirit of Combine. Think of it like UNIX tools, each step doing one thing and passing the results on.