• Combine: Getting Started
  • Fabrizio Brancati
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: chaingangway
  • Proofread by: LSvih

0202 years, it’s time to learn Combine

Learn how to Combine multiple publishers using Publisher and Subscriber in the Combine framework to handle the flow of events over time.

Introduced at WWDC 2019, the Combine framework is Apple’s new “responsive” framework for handling events that change over time. You can use Combine to unify and simplify code like agents, notifications, timers, and completion callbacks. There have been third-party responsive frameworks available on iOS before, but Now Apple has developed its own.

In this tutorial, you will learn:

  • usePublisherSubscriber.
  • Process the event flow.
  • Use in the manner in the Combine frameworkTimer.
  • Determine when to use Combine in a project.

We learned these core concepts by optimizing FindOrLose. FindOrLose is a game where out of four images, one image is different from the other three and you need to quickly identify that image.

Ready to explore the wondrous world of Combine in iOS? It’s time to get started!

An introduction to

You can download the project resources for this tutorial here.

Open the Starter project and take a look at the project files.

Before playing the game, you must register and obtain an API key on the Unsplash Developers Portal. After signing up, create an App on their developer portal. Once created, you should see the following on the screen:

Note: The Unsplash APIs have a maximum of 50 calls per hour. Our games are fun, but don’t play too much :]

Open unsplashapi. swift and add your UnsplashAPI key to unsplashapi. accessToken as follows:

enum UnsplashAPI {
  static let accessToken = "<your key>". }Copy the code

Compile and run. The home screen displays four gray squares and a button to start or stop the game.

Click Play to start the game:

Now, the game is running perfectly fine, but take a look at playGame() in the gameViewController.swift file, and the method ends like this:

            }
          }
        }
      }
    }
  }
Copy the code

There are too many built-in closures. Can you figure out the logic and order? What if you want to change the order of calls or add new functionality? It’s time for Combine to help you.

Combine to introduce

The Combine framework provides a declarative API for calculating values over time. It has three elements:

  1. Publishers: Generate values
  2. Operators: Evaluates a value
  3. Subscribers: Receive value

Let’s take a look at each one in turn:

Publishers

Objects that follow the Publisher protocol can send sequences of values that change over time. There are two association types in the protocol: Output is the type that produces the value; Failure is an exception type.

Each Publisher can send multiple events:

  • OutputType value output
  • Complete the callback
  • FailureType exception output

Some types of functional features, such as Timer and URLSession, have been optimized in the Foundation framework to support Publishers. We’ll use them in this tutorial as well.

Operators

Operators are special methods that can be called by Publishers and return the same or different Publisher. Operator describes the actions of modifying, adding, deleting, or performing other operations on a value. You can combine these operations with chain calls to perform complex operations.

Imagine that the values flow from the original Publisher and are then processed by a series of operators to form a new Publisher. This process is like a river that flows from Publisher upstream to Publisher downstream.

Subscribers

Without listening to these published events, Publishers and Operators have no meaning. So we need Subscriber to monitor.

Subscriber is another agreement. Like the Publisher protocol, it has two association types: Input and Failure. These two types must correspond to Output and Failure types in Publisher.

Subscriber receives a sequence of values from Publisher and normal or abnormal events.

combination

When publisher’s subscribe(_:) method is called, it is ready to pass a value to subscriber. At this point, publisher sends a Subscription to subscriber. Subscriber can use this subscription to request data from publisher.

Once this is done, the publisher is free to send data to the subscriber. During this process, the Publisher may send all of the requested data, or only part of it. If Publisher is a finite event stream, it will end up with either a completion event or an error event. The chart below summarizes the process:

Use Combine at the network layer

The above is an overview of Combine. Now we use it in our project.

First, create a GameError enumeration to handle all Publisher errors. In Xcode’s home directory, go to File ▸ New ▸ File… TAB, then select Template iOS ▸ Source ▸ Swift File.

Name this new file gameError.swift and add it to the Game folder.

To improve the GameError enumeration:

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) } }Copy the code

Enumerations define all errors that can be encountered in a game, as well as a way to handle any type of error that is guaranteed to be of GameError type. We use it when dealing with Publisher.

With this, we can handle errors in HTTP status codes and decoding.

Next, import the Combine framework. Open unsplashapi. swift and add the following to the top of the file:

import Combine
Copy the code

Then change the signature of randomImage(Completion 🙂 to the following:

static func randomImage(a) -> AnyPublisher<RandomImageResponse.GameError> {
Copy the code

Instead of taking the callback closure as an argument, this method now returns a Publisher whose output is of type RandomImageResponse and faliure is of type GameError.

AnyPublisher is a system type that you can use to wrap “any” publisher. This means that you don’t have to change the method signature if you want to use Operators or hide implementation details from callers.

Next, let’s modify the code so that URLSession supports Combine’s new features. Find the line that starts with session.datatask (with:) and replace it from there 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()
Copy the code

This code looks like a lot, but it uses many of Combine’s features. Here’s a step-by-step explanation:

  1. The URL session returns the publisher of the URL request. The publisher isURLSession.DataTaskPublisherType, whose output type is (data: data, response: URLResponse). This is not the correct type of output, so you use a series of operator conversions to do this.
  2. usetryMap. The operator will receive the upstream value and try to map it to another type, possibly throwing an error. There’s another one calledmapOperator can perform the mapping operation, but it does not throw an error.
  3. Check whether the HTTP status is200 OK.
  4. If the HTTP status code is not200 OKThrow customGameError.statusCodeError.
  5. If all is OK, returnresponse.data. This means that the output type of the chained call is nowData.
  6. usedecode, it will try to useJSONDecoderThe upstream value is resolved toRandomImageResponseType. At this point, the output type is correct.
  7. The error type is not exactly correct. If an error occurs during decode, the error type will not be GameError. In the mapError operator, we use the methods defined in GameError to map any error type to the desired error type.
  8. If you look at itmapErrorReturn type, you might be surprised..eraseToAnyPublisherThe operator will clean everything up for you, making the return value more readable.

You could also implement most of the above logic in an operator, but this is clearly not Combine’s idea. You can think of tools in UNIX that do one thing at a time, and then pass the results of each step to the next.

Download images with the Combine framework

With the network layer logic reconfigured, let’s download the image

Open the imageDownloader. swift file and import Combine at the beginning of the file with the following code:

import Combine
Copy the code

Like randomImage, with Combine you don’t have to use closures. Replace the Download (URL :, completion:) method with the following code:

/ / 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()
}
Copy the code

The code here is very similar to the previous example. Here’s a step-by-step explanation:

  1. As before, modify the method signature. Let it return a Publisher instead of receiving closure arguments.
  2. To get the image URLdataTaskPublisher.
  3. usetryMapCheck the response code and extract the data if there are no errors.
  4. With anothertryMapThe operator puts the upstreamDataConverted toUIImageIf it fails, an error is thrown.
  5. Map errors toGameErrorType.
  6. .eraseToAnyPublisherReturns an elegant type

Use the Zip

We have modified all the network-related methods with Publisher instead of callback closure. Now, let’s call these methods.

Open gameViewController.swift and import Combine at the beginning of the file:

import Combine
Copy the code

Add the following properties at the beginning of the GameViewController class:

var subscriptions: Set<AnyCancellable> = []
Copy the code

This property is used to store all subscriptions. So far, we have used publishers and Operators, but have not subscribed.

Delete all the code in playGame() and replace it with the following after the startLoaders() method call:

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

In the code above:

  1. Get a Publisher of random images.
  2. useflatMapMap the value of the previous Publisher to the new Publisher. In this case, you first call randomImage, get the output, and map it to publisher to download the image.

Next, we use the same logic to get the second image. Add the following code to firstImage:

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

So now we’ve downloaded two random images. Combine these operations with ZIP. Add the following code 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)
Copy the code

The following steps are broken down:

  1. zipCreate a new Publisher by combining the outputs of an existing Pulisher. It waits for all publishers to send outputs before sending the combination value downstream.
  2. receive(on:)You can specify where upstream events are handled. If you want to operate on the UI, you must use the main queue.
  3. This is our first subscriber.sink(receiveCompletion:receiveValue:)A subscriber has been created with two closure parameters. The closure is called when a completion event or normal value is received.
  4. Publisher has two ways to end a call — completion or exception. If an exception occurs, the game stops.
  5. Add the data of two random images to the array for randomization, and then update the UI.
  6. Store subscription information tosubscriptionsIs used to remove references. When there is no reference, the subscription is cancelled and publisher stops sending immediately.

Finally, compile and run.

Congratulations, your App has now successfully used Combine to handle event flows.

Join the score

You might notice that the fractional logic doesn’t work. Before the reconstruction, we chose the image while the score was counting down, but now the score is standing still. Now we will use Combine to refactor the functionality of the timer.

First, replace the // TODO: Handling Game Score in the playGame() method with the following code to restore the timer function:

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()
  }
}
Copy the code

In the code above, we are going to have the gameTimer fire every 0.1 second and decrease the score by 10. When the score reaches 0, the timer is terminated.

Now compile and run to determine if your score is decreasing over time.

Use timers in Combine

Timers are another Foundation type that supports Combine. Now let’s migrate timers to Combine’s version to see the difference.

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

var gameTimer: AnyCancellable?
Copy the code

Now you store a subscription in the timer instead of the timer itself. In Combine we use AnyCancellable.

Replace the first line of the playGame() and stopGame() methods with the following:

gameTimer? .cancel()Copy the code

Now modify the gameTimer assignment in the playGame() method 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() } }Copy the code

Here are the decomposing steps:

  1. With this new API, you can create publisher through Timer. This Publisher repeats the current moment at a given time interval and on a given runloop.
  2. This Publisher is a special type of Publisher that needs to explicitly specify when it starts and ends. When a subscription is started or cancelled,.autoconnectManage by connecting or disconnecting.
  3. This Publisher can’t be an exception, so you don’t have to deal with exception callbacks. In this case,sinkCreate subscriber. Only the normal value needs to be processed.

Compile and run and play with your Combine App.

To improve the App

There are still several places to be optimized here, we added multiple subscriber continuously with.store(in: & Subscriptions), but did not remove them. Now let’s improve.

Add the following line at the top of resetImages() :

subscriptions = []
Copy the code

Here, you declare an empty array to remove references to all useless subscription information.

Next, add the following line at the top of the stopGame() method:

subscriptions.forEach { $0.cancel() }
Copy the code

Here, you went through all the subscriptions and then cancelled them.

The last compilation ran.

Do everything with Combine!

Using the Combine framework is a good choice. It’s popular, it’s new, and it’s official, so why not use it now? But before you go all out, there are a few things to consider:

Low iOS version

First, you have to think about your users. If you plan to continue supporting iOS 12, you can’t use Combine. (Combine requires iOS 13 and above to support)

team

Responsive programming is a big shift in thinking and there will be a learning curve, but your team will need to catch up. Is everyone on your team as enthusiastic about changing the way things work as you are?

The rest of the SDK

Before adopting Combine, think about the technologies you already use in your app. If you have other callback-based SDKS, such as Core Bluetooth, you must encapsulate them with Combine.

Gradually integrating

As you gradually master Combine, there are not so many concerns. You can start refactoring with network layer calls and then switch to other modules in your app. You can also use Combine where closures are used.

How do you learn next?

You can download the full version of this project on the original page.

In this tutorial, you learned the basics of Combine: Publisher and Subscriber. You also learned how to use operators and timers. Congratulations, you’ve got the hang of it!

To learn more about how to use Combine, see our book Combine: Asynchronous Programming with Swift!

If you have any questions or comments about this tutorial, feel free to discuss them in the discussion section below!

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.