Combine: Getting Started [FREE]

栏目: IT技术 · 发布时间: 4年前

内容简介:In this tutorial, you’ll learn how to:You’ll see these key concepts in action by enhancing

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:

Combine: Getting Started [FREE]

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:

Combine: Getting Started [FREE]

Tap Play to start the game:

Combine: Getting Started [FREE]

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:

Output
Failure

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:

Combine: Getting Started [FREE]

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.

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:

flatMap

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:

zip
receive(on:)
sink(receiveCompletion:receiveValue:)
subscriptions

Finally, build and run!

Combine: Getting Started [FREE]

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.

Combine: Getting Started [FREE]

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 of playGame() 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:

Timer
.autoconnect
sink

Build and run and play with your Combine app!

Combine: Getting Started [FREE]

Refining the App

There are just a couple of refinements that are missing. You're continuously adding subscribers with .store(in: &subscriptions) without ever removing them. You'll fix that next.

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

subscriptions = []

Here, you assign an empty array that will remove all the references to the unused subscriptions.

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

subscriptions.forEach { $0.cancel() }

Here, you iterate over all subscriptions and cancel them.

Time to build and run one last time!

Combine: Getting Started [FREE]

I want to Combine All The Things Now!

Using Combine may seem like a great choice. It's hot, new, and first party, so why not use it now? Here are some things to think about before you go all-in:

Older iOS Versions

First of all, you need to think about your users. If you want to continue to support iOS 12, you can't use Combine. (Combine requires iOS 13 or above.)

Your Team

Reactive programming is quite a change of mindset, and there is going to be a learning curve while your team gets up to speed. Is everyone on your team as keen as you to change the way things are done?

Other SDKs

Think about the technologies your app already uses before adopting Combine. If you have other callback-based SDKs, like Core Bluetooth, you'll have to build wrappers to use them with Combine.

Gradual Integration

You can mitigate many of these concerns if you start using Combine gradually. Start from the network calls and then move to the other parts of the app. Also, consider using Combine where you currently have closures.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

In this tutorial, you've learned the basics behind Combine's Publisher and Subscriber . You also learned about using operators and timers. Congratulations, you're off to a good start with this technology!

To learn even more about using Combine, check out our book Combine: Asynchronous Programming with Swift !

If you have any questions or comments on this tutorial, please join the forum discussion below!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数字化崇拜

数字化崇拜

[加] 文森特·莫斯可 / 黄典林 / 北京大学出版社 / 2010-1 / 26.00元

与此前的许多技术发展一样,以互联网为标志的数字化时代同样为人们提供了社会根本性变革的许诺:通过电脑,我们可以超越时空和政治。在本书中,文森特·莫斯可透过技术发展和经济泡沫的迷雾,试图探明围绕数字化新技术出现了哪些迷思,以及为何人们对这些迷思坚信不疑。他认为互联网时代投资者如此狂热的动因并不是他们对经济规则的无知,而是对赛博空间开启了一个新世界这样的迷思的坚定信念。 莫斯可指出,迷思并不是一些......一起来看看 《数字化崇拜》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

html转js在线工具
html转js在线工具

html转js在线工具