はじめに
普段AngularでRxJSを利用しながらコーディングしているSwift初心者の筆者が
Apple製の非同期フレームワークである、 Combine を利用してプログラミングしてみた記事になります。
どなたかの参考になれば幸いです。
Combineって何?
iOS13 から利用できるようになったフレームワークです。
※ iOS 13以降に使えるフレームワークなのでiOS 12以下は使えないことにご注意ください。
Combineの大事な3つの要素
- Publishers: イベントの発行者
- Subscribers: イベントの購読者です
- Operators: 流れてくる値を加工することができます
新しい言語で新しい概念が出てくると中々理解しづらい部分があるため
慣れ親しんだRxJsとの対比表を書いて整理してみます。
CombineとRxJSとの対比表
Combine | RxJS | |
---|---|---|
イベントの発行者 | Publishers | Observable |
イベントの購読者 | Subscribers | Observer |
流れてくる値の加工 | Operators | Operator |
RxJsと対比してみて、なんとなく理解が進んだところで実際に簡単なサンプルコードを書いてみます。
Publisherを作る (Future)
Futureとは非同期で値を返すことが可能なPublisherです。
値を 1つ 発行してfinish or failsのどちらかをすることができます。
値は Genericsになっていますので、 左辺に返したい値の型、右辺にエラーの型を定義する必要があります。
エラーの型は Error型 でも、自分で定義したErrorプロトコルに準拠した型でもいいです。
enum MyError: Error {
case BadRequestError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
case NotFoundError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
case SystemError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
}
func testFuture(v: Bool) -> Future<String, MyError> {
return Future { promise in
if (v) {
print("hello in test")
promise(.success("hello"))
} else {
promise(.failure(AnError.NotFoundError("", "???", nil)))
}
}
}
testFuture(true)
// 出力
hello in test
注意点としては、処理が実行されるタイミングが、初めてSubscribeされたタイミングではなく
Futureインスタンスが生成されたタイミング になります。
※ RxJsでいうところの .subscribeしたタイミングで実行されるわけではない
そのため上記サンプルを見ていただいたら分かるとおり、 Future型の 関数を購読(Subscribers)していない
ですが、関数内の print()が実行され "hello in test" が出力されていることがわかります。
Futureを遅延実行させる(Deferred)
先ほどのFutureの場合、Subscribeをしていないのに処理が実行されてしまい
思わぬ動作をしてしまう可能性もあります。
そのため購読したタイミングで処理を実行させる Deferred を活用する方法があります。
enum MyError: Error {
case BadRequestError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
case NotFoundError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
case SystemError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
}
func testDeffered(v: Bool) -> Deferred<Future<String, AnError>> {
return Deferred {
Future { promise in
if (v) {
print("hello in test")
promise(.success("hello"))
} else {
promise(.failure(AnError.NotFoundError("", "???", nil)))
}
}
}
}
testDeffered(true)
// 出力
なし
先ほどのFutureと異なり、購読していないため Publisher内の print() が実行されていない事がわかります。
実際に購読してみた場合の動きとコードは以下になります。 (購読については下で紹介)
enum MyError: Error {
case BadRequestError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
case NotFoundError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
case SystemError(_ value: Any, _ cause: String, _ stackTrace: String? = nil)
}
func testDeffered(v: Bool) -> Deferred<Future<String, AnError>> {
return Deferred {
Future { promise in
if (v) {
print("hello in test")
promise(.success("hello"))
} else {
promise(.failure(AnError.NotFoundError("", "???", nil)))
}
}
}
}
testDeffered(true).sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("error \(error)")
}
}, receiveValue: { value in
print(value)
})
// 出力
hello in test
hello
finished
Deferredの動きをRxJSで書いてみた場合は以下が近いような感じに近いと思います。
const helloObservable = of('hello');
helloObservable.subscribe(res => console.log(res)); // subscribeしたタイミングで実行される
また、FutureやDeferred以外にも以下のようなPublisherがあるので、調べてみてもいいかもしれません。
- Empty
- Fail
- Record
Conforming Types
購読する(Subscribers)
Publisherを購読するつ1つの方法として、 Deferredで紹介した sink が使えます。
func test() throws {
Future<String, Error> { promise in
promise(.success("hello"))
}.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("error \(error)")
}
}, receiveValue: { value in
print("value \(value)")
})
}
test()
// 出力
hello in test
hello
finished
RxJSでいうところの Observableを受け取ってsubscribe()を呼び出す処理にあたると思います。
流れてくる値の加工(Operators)
Publisherで流れてきた値を 購読する前に値を加工して渡したい場合は
map などのOperatorsを利用します。
※ RxJSでいうmapやflatMapもCombineにはありますので似たような感覚で使えるかなと思います!
func testOperators() throws {
Future<Int, Error> { promise in
promise(.success(1))
}.map { x in x * 2
}.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("error \(error)")
}
}, receiveValue: { value in
print("value \(value)")
})
}
// 出力
value 2
RxJSと同じく、map以外にもさまざまなOperatorsが標準で用意されているため調べてみてもいいかもしれません。
おわりに
Swiftの非同期周りの処理を書く際に、HydraやRxSwiftなどサードパーティーのライブラリを使うと
言語やOSのバージョンが上がった際にメンテナンスしんどそうだなぁと思っていたのですが
新しめのOSでしか利用できないものの、標準で用意されていて Swiftいいなと思いました。
Rx系のライブラリを触った事がある方なら簡単な処理ならすんなりと書けるところもグッドです。
ぜひ積極的に活用していければなと思います。