この記事は 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のオペレータの対応表をまとめてくれている人がいます。
これってCombineだとどうやるんだろう?と思ったときに大変便利です。
ただこの表の通りに機械的に置き換えられない部分はあり、たとえば今回ご紹介した Futureのイニシャライザ や、 Publishers.Zip
など基本的な部分に関しても若干ひねりを加える必要がありました。
また、個人的に intervalオペレータを使いたいときがあり、 Timer.publish で代用できないかと試行錯誤してみたのですが、良い案が思いつきませんでした。もしアイデアのある方いらっしゃいましたらコメントくださると幸いです。
※ コメントで実装をアドバイスしていただきました https://qiita.com/kumamotone/items/15e5a580a9eba6be189d#comment-e9b4d7cdd8c38adc3b3f
おわりに
この記事ではよく使われる RxSwift の Single と、Combine による非同期処理の実装例を紹介しました。
Combine は外部ライブラリの導入なしに書き始めることができる素晴らしい公式ライブラリですが、RxSwift とは細かいところが色々違うので慣れるのに時間がかかったり、Combine で提供されていないオペレータはどう実現するか悩む部分もありました。
もし導入や置き換えを迷ってらっしゃる方は、RxSwiftで実装している部分が、どう置き換えることができそうか、アタリをつけてから導入しはめるのがおすすめかもしれません。
以上 iOS Advent Calendar 2019 の 2日目の記事でした。 明日は @fromkk さんの記事です。