Combine: Getting Started

Learn how to use Combine’s Publisher and Subscriber to handle event streams, merge multiple publishers and more. By Fabrizio Brancati.

4.4 (59) · 4 Reviews

Download materials
Save for later
Share

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 and Subscriber.
  • 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:

Creating Unsplash app to get the API key

Note: Unsplash APIs have a rate limit of 50 calls per hour. Our game is fun, but please avoid playing it too much :]

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:

First screen of FindOrLose with four gray squares

Tap Play to start the game:

First run of FindOrLose with four images

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:

  1. Publishers: Things that produce values.
  2. Operators: Things that do work with values.
  3. 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 Failuretype.

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:

Publisher-Subscriber pattern

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 FileNewFile… and choose the template iOSSourceSwift 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:

  1. 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.
  2. 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 a map operator for mapping operations that can’t throw errors.
  3. Check for 200 OK HTTP status.
  4. Throw the custom GameError.statusCode error if you did not get a 200 OK HTTP status.
  5. Return the response.data if everything is OK. This means the output type of your chain is now Data
  6. Apply the decode operator, which will attempt to create a RandomImageResponse from the upstream value using JSONDecoder. Your output type is now correct!
  7. Your failure type still isn’t quite right. If there was an error during decoding, it won’t be a GameError. The mapError operator lets you deal with and map any errors to your preferred error type, using the function you added to GameError.
  8. 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.