こんにちは!
dely 株式会社で iOS を担当している nancy こと仲西です。
本記事は dely Advent Calendar の11日目の記事です。
昨日は小林さんが「UI デザイン × PdM で広がるデザインの可能性」というテーマで書いています。
https://dely.design/n/nfbad0dcdec77dely.design
UI デザイナーが PdM をやると何がいいのか、 どんな点を心がけるべきなのかといったことがまとまっているので、気になる方はぜひご覧ください!
ちなみに、記事内でチラッと私も登場しています(笑)
はじめに
本記事では iOS 13以降で使用できる Apple 純正のフレームワークである Combine と、 Combine と似た機能を有する OSS フレームワークである RxSwift を比較してみたいと思います。
現状だと多くの方が RxSwift の方を使用しているかと思います。
いざ Combine に移行する際にどう書き換えたらいいのかなどの点で参考になれば幸いです。
Combine とは
Combine とは、Apple が WWDC 2019 で発表された、 UI イベントやネットワーク通信の非同期イベントなどを処理するためのフレームワークです。
以下の動画で概要が、 developer.apple.com
以下の動画ではコーディング例が紹介されています。
iOS 13から導入されたフレームワークであるため、iOS 12以下では使用できないのでご注意ください。
RxSwift とは
RxSwift とは、Rx という Reactive Programming ができるライブラリの Swift 版で、 こちらも主に UI イベントや非同期のイベントを受け取る際などに使用されています。
元々は .NET 用のフレームワークだったようですが、今では Swift だけでなく Java や JavaScript などにも移植されているオープンソースのフレームワークです。
比較してみる
それでは実際に Combine、RxSwift 両方のコードを書いてみて比較してみたいと思います。
例として、Qiita の記事取得 API(https://qiita.com/api/v2/items)を使用し、記事のタイトル、公開日を取得してみたいと思います。
以下のような struct を定義し、JSON からデコードします。
struct Article: Codable { let title: String let url: String }
Publisher と Observable
Publisher、Observable を用いて API 通信完了後に取得結果を表示するまでの処理を比較してみます。
Combine: Publisher
var cancellables = [AnyCancellable]() func fetchArticles() -> AnyPublisher<[Article], Error> { let url = URL(string: "https://qiita.com/api/v2/items")! let request = URLRequest(url: url) return URLSession.shared .dataTaskPublisher(for: request) .map({ $0.data }) .decode(type: [Article].self, decoder: JSONDecoder()) .eraseToAnyPublisher() } fetchArticles() .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completion in switch completion { case .finished: print("finished") case .failure(let error): print(error.localizedDescription) } }, receiveValue: { response in print(response) }).store(in: &cancellables)
Publisher を購読するために sink
というメソッドを使用しています。
sink
メソッドでは、イベントの購読が完了した際に実行される処理、値が通知された際に実行される処理をクロージャとして渡すことができます。
イベントの購読完了には、正常に終了したのか(.finished)、エラーで終了したのか(.failure)の2パターンあります。
RxSwift: Observable
var disposeBag = DisposeBag() func fetchArticles() -> Observable<[Article]> { let url = URL(string: "https://qiita.com/api/v2/items")! let request = URLRequest(url: url) return Observable<[Article]>.create({ observable in let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { observable.onError(error) } do { if let data = data { let articles = try JSONDecoder().decode([Article].self, from: data) observable.onNext(articles) } } catch(let e) { observable.onError(e) } } task.resume() return Disposables.create() }) } fetchArticles() .subscribe(onNext: { response in print(response.first) }, onError: { error in print(error.localizedDescription) }, onCompleted: { print("Completed") }, onDisposed: { print("Disposed") }).disposed(by: disposeBag)
この辺りは RxSwift を触ったことがある方であれば比較的簡単に理解できそうですね。
RxSwift で言う Dispose は Combine の Cancellable にあたるようです。
var cancellables = [AnyCancellable]()
を定義しておき、 sink
した際に .store(in: &cancellables)
のようにしておくと、
cancellables
が解放されたタイミングで sink
で購読してた処理もキャンセルされるようです。
Future と Single
上記の比較では Observable を用いて Combine と比較しましたが、 Single を Combine で実装するとどうなるのかという比較も行いたいと思います。
Combine: Future
Future は
- 値を1度通知する
- エラーを通知
のどちらかを行うことができます。
RxSwift の Single と同じように API 通信などの1度限りの処理などに使用できそうです。
func fetchArticles() -> Future<[Article], Error> { let url = URL(string: "https://qiita.com/api/v2/items")! let request = URLRequest(url: url) return Future<[Article], Error> { promise in URLSession.shared .dataTaskPublisher(for: request) .map({ $0.data }) .sink(receiveCompletion: { _ in }, receiveValue: { responseData in do { let articles = try JSONDecoder().decode([Article].self, from: responseData) promise(.success(articles)) } catch(let e) { promise(.failure(e)) } }).store(in: &cancellables) } } fetchArticles() .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completion in switch completion { case .finished: print("finished") case .failure(let error): print(error.localizedDescription) } }, receiveValue: { response in print(response) }).store(in: &cancellables)
RxSwift: Single
func fetchArticlesSingle() -> Single<[Article]> { let url = URL(string: "https://qiita.com/api/v2/items")! let request = URLRequest(url: url) return Single<[Article]>.create(subscribe: { single in let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { single(.error(error)) } do { if let data = data { let articles = try JSONDecoder().decode([Article].self, from: data) single(.success(articles)) } } catch(let e) { single(.error(e)) } } task.resume() return Disposables.create() }) } fetchArticlesSingle().subscribe(onSuccess: { response in print(response) }, onError: { error in print(error.localizedDescription) })
上記のようにすると RxSwift で言う Single は Combine の Future を使用すると置き換えられそうです
ただ、こちらの Single と Future の項目で述べられているように、少し動作が異なるようです。
They're only similar in the sense of single emission, but Future shares resources and executes immediately (very strange behavior)
Single と同じようにイベントを1度通知して Finish する動作は同じですが、 Single では初めて Subscribe されたタイミングで処理が実行されるのに対し、 Future ではインスタンスが生成されたタイミングに処理が実行されるようです。
そのため、素直に書き換えるだけでは期待する動作にならない可能性があるため注意が必要です。
終わりに
本記事では Combine と RxSwift の書き方を比較してみました。
これから Combine を使用してみたいと思われている方にとって少しでも参考になれば幸いです。
また、明日は sakura818uuu さんが「初めて PM っぽいことをやって失敗した件」というタイトルで投稿します!
dely について
dely では一緒に働いていただけるエンジニアを募集しています!
dely の開発チームについて詳しく知りたい方はこちらをご覧ください!
CXOとVPoEへのインタビュー記事はこちら!
参考