dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

Combine と RxSwift を比較してみた

 

こんにちは!

dely 株式会社で iOS を担当している nancy こと仲西です。

 

本記事は dely Advent Calendar の11日目の記事です。

 

qiita.com

adventar.org

 

昨日は小林さんが「UI デザイン × PdM で広がるデザインの可能性」というテーマで書いています。

dely.design

UI デザイナーが PdM をやると何がいいのか、 どんな点を心がけるべきなのかといったことがまとまっているので、気になる方はぜひご覧ください!

ちなみに、記事内でチラッと私も登場しています(笑)  

 

はじめに 

本記事では iOS 13以降で使用できる Apple 純正のフレームワークである Combine と、 Combine と似た機能を有する OSS フレームワークである RxSwift を比較してみたいと思います。

 

現状だと多くの方が RxSwift の方を使用しているかと思います。

いざ Combine に移行する際にどう書き換えたらいいのかなどの点で参考になれば幸いです。

Combine とは

f:id:nakanishi-w:20191210102758p:plain
developer.apple.com/wwdc19/722

Combine とは、Apple が WWDC 2019 で発表された、 UI イベントやネットワーク通信の非同期イベントなどを処理するためのフレームワークです。

以下の動画で概要が、 https://developer.apple.com/videos/play/wwdc2019/722/developer.apple.com

以下の動画ではコーディング例が紹介されています。

https://developer.apple.com/videos/play/wwdc2019/721developer.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 の開発チームについて詳しく知りたい方はこちらをご覧ください!

speakerdeck.com

 

CXOとVPoEへのインタビュー記事はこちら!

wevox.io

 

参考