Combine で RxSwift の Single を置きかえる

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

iOS13から、非同期処理を便利に扱うことができる、 [Combine](https://developer.apple.com/documentation/combine) というフレームワークが使えるようになりました。

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

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

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

## Single -> Future

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

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

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

RxSwiftの場合:

```swift
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 の場合:

```swift
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の場合:

```swift
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の場合:

```swift
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の場合:

```swift
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の場合:

```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49010/8f659468-372b-2e15-bb64-22e2ca2cb89d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49010/d592f1f5-2d5f-d246-6ce0-142017451ec4.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49010/633c677f-f484-4f43-8a15-ff42849ab428.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 さんの記事です。