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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Downloading an Image With Combine

Now that you have the networking logic, it’s time to download some images.

Open the ImageDownloader.swift file and import Combine at the start of the file with the following code:

import Combine

Like randomImage, you don’t need a closure with Combine. Replace download(url:, completion:) with this:

// 1
static func download(url: String) -> AnyPublisher<UIImage, GameError> {
  guard let url = URL(string: url) else {
    return Fail(error: GameError.invalidURL)
      .eraseToAnyPublisher()
  }

  //2
  return URLSession.shared.dataTaskPublisher(for: url)
    //3
    .tryMap { response -> Data in
      guard
        let httpURLResponse = response.response as? HTTPURLResponse,
        httpURLResponse.statusCode == 200
        else {
          throw GameError.statusCode
      }
      
      return response.data
    }
    //4
    .tryMap { data in
      guard let image = UIImage(data: data) else {
        throw GameError.invalidImage
      }
      return image
    }
    //5
    .mapError { GameError.map($0) }
    //6
    .eraseToAnyPublisher()
}

A lot of this code is similar to the previous example. Here’s the step-by-step:

  1. Like before, change the signature so that the method returns a publisher instead of accepting a completion block.
  2. Get a dataTaskPublisher for the image URL.
  3. Use tryMap to check the response code and extract the data if everything is OK.
  4. Use another tryMap operator to change the upstream Data to UIImage, throwing an error if this fails.
  5. Map the error to a GameError.
  6. .eraseToAnyPublisher to return a nice type.

Using Zip

At this point, you’ve changed all of your networking methods to use publishers instead of completion blocks. Now you’re ready to use them.

Open GameViewController.swift. Import Combine at the start of the file:

import Combine

Add the following property at the start of the GameViewController class:

var subscriptions: Set<AnyCancellable> = []

You’ll use this property to store all of your subscriptions. So far you’ve dealt with publishers and operators, but nothing has subscribed yet.

Now, remove all the code in playGame(), right after the call to startLoaders(). Replace it with this:

// 1
let firstImage = UnsplashAPI.randomImage()
  // 2
  .flatMap { randomImageResponse in
    ImageDownloader.download(url: randomImageResponse.urls.regular)
  }

In the code above, you:

  1. Get a publisher that will provide you with a random image value.
  2. Apply the flatMap operator, which transforms the values from one publisher into a new publisher. In this case you’re waiting for the output of the random image call, and then transforming that into a publisher for the image download call.

Next, you’ll use the same logic to retrieve the second image. Add this right after firstImage:

let secondImage = UnsplashAPI.randomImage()
  .flatMap { randomImageResponse in
    ImageDownloader.download(url: randomImageResponse.urls.regular)
  }

At this point, you have downloaded two random images. Now it’s time to, pardon the pun, combine them. You’ll use zip to do this. Add the following code right after secondImage:

// 1
firstImage.zip(secondImage)
  // 2
  .receive(on: DispatchQueue.main)
  // 3
  .sink(receiveCompletion: { [unowned self] completion in
    // 4
    switch completion {
    case .finished: break
    case .failure(let error): 
      print("Error: \(error)")
      self.gameState = .stop
    }
  }, receiveValue: { [unowned self] first, second in
    // 5
    self.gameImages = [first, second, second, second].shuffled()

    self.gameScoreLabel.text = "Score: \(self.gameScore)"

    // TODO: Handling game score

    self.stopLoaders()
    self.setImages()
  })
  // 6
  .store(in: &subscriptions)

Here’s the breakdown:

  1. zip makes a new publisher by combining the outputs of existing ones. It will wait until both publishers have emitted a value, then it will send the combined values downstream.
  2. The receive(on:) operator allows you to specify where you want events from the upstream to be processed. Since you’re operating on the UI, you’ll use the main dispatch queue.
  3. It’s your first subscriber! sink(receiveCompletion:receiveValue:) creates a subscriber for you which will execute those two closures on completion or receipt of a value.
  4. Your publisher can complete in two ways — either it finishes or fails. If there’s a failure, you stop the game.
  5. When you receive your two random images, add them to an array and shuffle, then update the UI.
  6. Store the subscription in subscriptions. Without keeping this reference alive, the subscription will cancel and the publisher will terminate immediately.

Finally, build and run!

Playing the FindOrLose game made with Combine

Congratulations, your app now successfully uses Combine to handle streams of events!

Adding a Score

As you may notice, scoring doesn’t work any more. Before, your score counted down while you were choosing the correct image, now it just sits there. You’re going to rebuild that timer functionality, but with Combine!

First, restore the original timer functionality by replacing // TODO: Handling game score in playGame() with this code:

self.gameTimer = Timer
  .scheduledTimer(withTimeInterval: 0.1, repeats: true) { [unowned self] timer in
  self.gameScoreLabel.text = "Score: \(self.gameScore)"

  self.gameScore -= 10

  if self.gameScore <= 0 {
    self.gameScore = 0

    timer.invalidate()
  }
}

In the code above, you schedule gameTimer to fire every very 0.1 seconds and decrease the score by 10. When the score reaches 0, you invalidate timer.

Now, build and run to confirm that the game score decreases as time elapses.

Game score decreases as time elapses

Using Timers in Combine

Timer is another Foundation type that has had Combine functionality added to it. You're going to migrate across to the Combine version to see the differences.

At the top of GameViewController, change the definition of gameTimer:

var gameTimer: AnyCancellable?

You're now storing a subscription to the timer, rather than the timer itself. This can be represented with AnyCancellable in Combine.

Change the first line ofplayGame() and stopGame() with the following code:

gameTimer?.cancel()

Now, change the gameTimer assignment in playGame() with the following code:

// 1
self.gameTimer = Timer.publish(every: 0.1, on: RunLoop.main, in: .common)
  // 2
  .autoconnect()
  // 3
  .sink { [unowned self] _ in
    self.gameScoreLabel.text = "Score: \(self.gameScore)"
    self.gameScore -= 10

    if self.gameScore < 0 {
      self.gameScore = 0

      self.gameTimer?.cancel()
    }
  }

Here's the breakdown:

  1. You use the new API for vending publishers from Timer. The publisher will repeatedly send the current date at the given interval, on the given run loop.
  2. The publisher is a special type of publisher that needs to be explicitly told to start or stop. The .autoconnect operator takes care of this by connecting or disconnecting as soon as subscriptions start or are canceled.
  3. The publisher can't ever fail, so you don't need to deal with a completion. In this case, sink makes a subscriber that just processes values using the closure you supply.

Build and run and play with your Combine app!

FindOrLose game made with Combine