Help us understand the problem. What is going on with this article?

Combine で RxSwift の Single を置きかえる

この記事は iOS Advent Calendar 2019 の 2日目の記事です。

iOS13から、非同期処理を便利に扱うことができる、 Combine というフレームワークが使えるようになりました。

iOSアプリ開発でよく使われている RxSwift とよく似ているため、今までは RxSwift を使っていたけど、新規開発では Combine を使ってみようかな、と思っている方もいらっしゃるのではないかと思います。

個人的に RxSwift でよく使うのが Single で、 (本当はこれだけならもっと軽量なライブラリでも実現できるのですが) API通信の部分などによく使っています。

この記事では、この Single を使った実装を Combine で実現しようと思ったときに、意外と調べたりハマったりして時間を使ってしまったため、基本的な部分についてまとめてみました。

Single -> Future

Single に対応する Publisher (RxSwift の Observable に対応するものだが、 Observable は class なのに対して Publisher は protocol) は Future になります。

final class Future<Output, Failure> where Failure : Error

Single と同様、 1発だけ値を発行します。また、 Errorを指定する必要があります (Error は Swift言語にある Error 型です)。

RxSwiftの場合:

APIRequester
     .send(number: 1) // Single<String>
     .subscribe { event in
         switch event {
             case .success(let value):
                 print(value)
             case .error(let error):
                 print("error:\(error.localizedDescription)")
         }
     }

Combine の場合:

APIRequester
    .send(number: 1) // Future<String, Error>
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("finished")
        case .failure(let error):
            print("error:\(error.localizedDescription)")
        }
    }) { value in
        print(value)
}

Single.create {} -> Future {}

無名のObservable Future のインスタンスを作って返す関数を定義すれば良いです。 (ただし細かい部分で挙動が異なります。後述)

RxSwiftの場合:

import UIKit
import RxSwift

class APIRequester {
    static func send(number: Int) -> Single<String> {
        return Single.create { single in
            single(.success("\(number)"))
            return Disposables.create()
        }
    }
}

class ViewController: UIViewController {

    var disposeBag: DisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        APIRequester
            .send(number: 1)
            .subscribe { event in
                switch event {
                    case .success(let value):
                        print(value)
                    case .error(let error):
                        print("error:\(error.localizedDescription)")
                }
            }.disposed(by: disposeBag)
    }
}

出力:

1

Combineの場合:

import UIKit
import Combine

class APIRequester {
    static func send(number: Int) -> Future<String, Error> {
        return Future<String, Error> { promise in
            promise(.success("\(number)")) // dummy
        }
    }
}

class ViewController: UIViewController {

    var cancellables: [AnyCancellable] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        APIRequester
            .send(number: 1)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("finished")
                case .failure(let error):
                    print("error:\(error.localizedDescription)")
                }
            }) { value in
                print(value)
        }.store(in: &cancellables)
    }
}

出力:

1
finished

Single.zip -> Publishers.Zip

複数のFutureを実行して、すべてを待ち合わせて何らかの処理がしたい場合は、 Publishers.Zip (先頭が大文字)を使えば良いです。

RxSwiftの場合:

import UIKit
import RxSwift

class APIRequester {
    static func send(number: Int) -> Single<String> {
        return Single.create { single in
            single(.success("\(number)"))
            return Disposables.create()
        }
    }
}

class ViewController: UIViewController {

    var disposeBag: DisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        Single.zip(
            APIRequester.send(number: 1),
            APIRequester.send(number: 2)
        ).subscribe { event in
            switch event {
                case .success(let value):
                    print(value)
                case .error(let error):
                    print("error:\(error.localizedDescription)")
            }
        }.disposed(by: disposeBag)

    }
}

出力:

("1", "2")

Combineの場合:

import UIKit
import Combine

class APIRequester {
    static func send(number: Int) -> Future<String, Error> {
        return Future<String, Error> { promise in
            promise(.success("\(number)")) // dummy
        }
    }
}

class ViewController: UIViewController {

    var cancellables: [AnyCancellable] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        Publishers.Zip(
            APIRequester.send(number: 1),
            APIRequester.send(number: 2)
        )
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("finished")
                case .failure(let error):
                    print("error:\(error.localizedDescription)")
                }
            }) { value in
                print(value)
        }.store(in: &cancellables)
    }
}

出力:

("1", "2")
finished

(追記) Single = Future なのか?

RxSwift の Single と Combine の Future は、ひとつだけ値を emit するという点では同じですが、実際には以下の点が異なります。

Single.create Future {}
購読(subscribe)したタイミングで実行 インスタンスが生成されたタイミングで即実行
複数回購読されると、ストリームが複製される 複数回購読されると内容を共有される
購読側からキャンセルできる 購読側からキャンセルできない

Single が いわゆる Cold な性質を持つ Observable なのに対して、 Future は いわゆる Hot な性質を持つ Observable と言えるかなとも思ったのですが、 Hot な Observable は connect のタイミングで流れこそはすれインスタンス生成時に実行されることはないので、一言で言い表すこともできそうにない挙動になっています。

この記事のような実装でSingleをFutureに置き換える場合、上記の違いを許容できるかどうかでFutureを使うべきか変わってきそうです。(この節はコメントでご指摘いただいたところをもとに追記させていただきました。ありがとうございます。)

RxSwift to Apple’s Combine Cheat Sheet

GitHubに、RxSwiftとCombineのオペレータの対応表をまとめてくれている人がいます。

https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet

これってCombineだとどうやるんだろう?と思ったときに大変便利です。

image.png

image.png

ただこの表の通りに機械的に置き換えられない部分はあり、たとえば今回ご紹介した Futureのイニシャライザ や、 Publishers.Zip など基本的な部分に関しても若干ひねりを加える必要がありました。

image.png

また、個人的に intervalオペレータを使いたいときがあり、 Timer.publish で代用できないかと試行錯誤してみたのですが、良い案が思いつきませんでした。もしアイデアのある方いらっしゃいましたらコメントくださると幸いです。

※ コメントで実装をアドバイスしていただきました https://qiita.com/kumamotone/items/15e5a580a9eba6be189d#comment-e9b4d7cdd8c38adc3b3f

おわりに

この記事ではよく使われる RxSwift の Single と、Combine による非同期処理の実装例を紹介しました。

Combine は外部ライブラリの導入なしに書き始めることができる素晴らしい公式ライブラリですが、RxSwift とは細かいところが色々違うので慣れるのに時間がかかったり、Combine で提供されていないオペレータはどう実現するか悩む部分もありました。

もし導入や置き換えを迷ってらっしゃる方は、RxSwiftで実装している部分が、どう置き換えることができそうか、アタリをつけてから導入しはめるのがおすすめかもしれません。

以上 iOS Advent Calendar 2019 の 2日目の記事でした。 明日は @fromkk さんの記事です。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away