内容简介: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
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:
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:
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:
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:
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.
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:
- Like before, change the signature so that the method returns a publisher instead of accepting a completion block.
-
Get a
dataTaskPublisher
for the image URL. -
Use
tryMap
to check the response code and extract the data if everything is OK. -
Use another
tryMap
operator to change the upstreamData
toUIImage
, throwing an error if this fails. -
Map the error to a
GameError
. -
.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!
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.
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!
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!
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!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。