[Swift] HTTP通信部分にCombineを使ってみる #WWDC19
はじめに
こんにちは。モバイルアプリサービス部の平屋です。
WWDC 19で初登場したCombineを使用してHTTP通信を行う実装を紹介します。
以下の記事のCombine版です。
本記事は Apple からベータ版として公開されているドキュメントを情報源としています。 そのため、正式版と異なる情報になる可能性があります。ご留意の上、お読みください。
検証環境
- macOS Mojave 10.14.5
- Xcode Version 11.0 beta (11M336w)
Combine Framework
UIイベント、ネットワーク通信などが非同期に提供するデータを処理するためのFrameworkです。以下のSDKで使用できます。
- iOS 13.0+
- macOS 10.15+
- tvOS 13.0+
- watchOS 6.0+
Combine Frameworkを構成する主な要素は以下の通りです。
- Publisher: データを生成する
- Subscriber: データを受け取る
- Operator: データの合成や加工を行う
- Subscription: PublisherとSubscriberのコネクションを表す
SubscriberはPublisherを購読してデータを受け取ります。データの合成や加工を行いたい場合はOperatorを使用します。
また、購読をやめたい場合はSubscriptionを使って解除します。
Publisherを作る
まずは、データを生成するPublisherを作ります。
ここではPublisher
プロトコルに適合したstructRequestPublisher
を定義して使用します。
(1) Output, Failureの定義
処理成功時に生成する値の型をData
、失敗時に生成する値の型をRequestError
と定義します。
(2) receive(subscriber:)
の実装
Publisher
プロトコルの必須メソッドreceive(subscriber:)
を実装します。このメソッドはSubscriberによって購読されたときに呼ばれます。
このメソッドの中で通信処理と通信結果のハンドリングを処理を行います。
(3) 通信処理
URLSession
のメソッドを使って通信を行います。
(4)〜(6) 通信結果のハンドリング
通信結果のハンドリングを行います。
(4)の場合はAPIから取得した値をSubscriberに渡し、処理完了を伝えます。
(5)と(6)の場合は処理失敗をSubscriberに伝えます。
(7) 購読完了の通知
購読が完了したことをSubscriberに伝えます。
(8) RequestPublisher
の提供
RequestPublisher
を提供するメソッドをURLSession
に追加します。(「Subscribeする」で使用します)
struct RequestPublisher: Publisher { // (1) Output, Failureの定義 typealias Output = Data typealias Failure = RequestError let session: URLSession let request: URLRequest // (2) `receive(subscriber:)`の実装 func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { // (3) 通信処理 let task = session.dataTask(with: request) { data, response, error in DispatchQueue.main.async { let httpReponse = response as? HTTPURLResponse if let data = data, let httpReponse = httpReponse, 200..<300 ~= httpReponse.statusCode { // (4) 通信結果のハンドリング(処理成功) _ = subscriber.receive(data) subscriber.receive(completion: .finished) } else if let httpReponse = httpReponse { // (5) 通信結果のハンドリング(処理失敗、HTTPエラー) let error = RequestError.http(code: httpReponse.statusCode, error: error) subscriber.receive(completion: .failure(error)) } else { // (6) 通信結果のハンドリング(処理失敗、その他のエラー) let error = RequestError.other subscriber.receive(completion: .failure(error)) } } } // (7) 購読完了の通知 let subscription = RequestSubscription(combineIdentifier: CombineIdentifier(), task: task) subscriber.receive(subscription: subscription) task.resume() } } extension URLSession { // (8) `RequestPublisher`の提供 func publisher(for request: URLRequest) -> RequestPublisher { return RequestPublisher(session: self, request: request) } }
Subscribeする
次にSubscribeする処理を見ていきます。
(1) publisher(for:)
の呼び出し
データを要求したいタイミングで、「Publisherを作る」で定義したメソッドpublisher(for:)
を呼びます。
(2) デコード
decode(type:decoder:)
を使用してJSONをデコードします。
(3) Subscribe実行
sink(receiveCompletion:receiveValue:)
を使用してSubscribeします。
receiveCompletion
引数のクロージャは処理完了時に呼ばれます。(成功時と失敗時のどちらでも呼ばれます)
receiveValue
引数のクロージャは値を受け取った場合に呼ばれます。
func request() { let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=swift+combine")!) // (1) `publisher(for:)`の呼び出し requestCancellable = URLSession.shared.publisher(for: request) // (2) デコード .decode(type: Repositories.self, decoder: decorder) // (3) Subscribe実行 .sink(receiveCompletion: { completion in switch completion { case .finished: print("finished") case .failure(let error): print("error:\(error.localizedDescription)") } }, receiveValue: { repositories in print("repositories:\(repositories)") }) }
通信処理をキャンセルできるようにする
最後に通信処理をキャンセルできるようにするための実装を見ていきます。
(1) Subscriptionの定義
URLSessionTask
を保持するSubscriptionを定義し、cancel()
などを実装します。
(2) Subscriptionの提供
RequestPublisher
のreceive(subscriber:)
内でSubscriberにSubscriptionを提供します。
(3) Cancellableの保持
sink(receiveCompletion:receiveValue:)
を呼ぶとCancellable
が返るので保持しておきます。
(4) キャンセル
キャンセルしたいタイミングでcancel()
を呼びます。
// (1) Subscriptionの定義 struct RequestSubscription: Subscription { let combineIdentifier: CombineIdentifier let task: URLSessionTask func request(_ demand: Subscribers.Demand) {} func cancel() { task.cancel() } } struct RequestPublisher: Publisher { // ... func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { // ... // (2) Subscriptionの提供 let subscription = RequestSubscription(combineIdentifier: CombineIdentifier(), task: task) subscriber.receive(subscription: subscription) // ... } } class ViewController: UIViewController { // ... var requestCancellable: Cancellable? deinit { // (4) キャンセル requestCancellable?.cancel() } // ... func request() { // ... // (3) Cancellableの保持 requestCancellable = URLSession.shared.publisher(for: request) .decode(type: Repositories.self, decoder: decorder) .sink(receiveCompletion: { completion in // ... }, receiveValue: { repositories in // ... }) } }
さいごに
本記事ではCombineを使用してHTTP通信を行う実装を紹介しました。
紹介したサンプルコードは単体の非同期処理なので、必要以上に大がかりな感じになってしまいましたが、UIイベントと組み合わせたり、複数の通信を束ねたりするようになれば旨味が出てきそうだなと思いました。
サンプルコードは以下のgistで公開していますので参考にしてみてください!
参考
Apple Developer
- Combine | Apple Developer Documentation
- Introducing Combine - WWDC 2019
- Combine in Practice - WWDC 2019