Swift のリアクティブプログラミングのライブラリといえば RxSwift か ReactiveSwift が有名ですよね。
今まで RxSwift しか使ってこなかったのですが ReactiveSwift を使ってみたらとても良かったのでそれについて書きたいと思います。
ReactiveSwift と RxSwift を比較する形で紹介します。
RxSwift やその他 Rx
ライブラリをある程度自由に使いこなせるという方を対象に書いています。
ライブラリの構成
ReactiveSwift | RxSwift | |
---|---|---|
for Swift | ReactiveSwift | RxSwift |
for Cocoa | ReactiveCocoa | RxCocoa |
依存ライブラリ | Result | – |
ReactiveSwift の構成は RxSwift と似ています。
– xxxSwift がピュア Swift 用
– xxxCocoa が Cocoa 用
のフレームワークです。
1点違うのは ReactiveSwift は Result
というライブラリに依存していることです。
なので ReactiveSwift を使うと自動的に Result がついてきます。
「えー依存ライブラリ増えんのかよ」と思われるかもしれませんが Result はとてもいいライブラリですし、Result があるからこそ ReactiveSwift が便利なのです。
だから大丈夫です👍🏼
ReactiveSwift のここが素晴らしい
Hot / Cold の区別が型から判別できる
ReactiveSwift では Observable
が Hot
か Cold
かを型で判別できます。
ReactiveSwift | RxSwift | |
---|---|---|
Hot | Signal<Int, MyError> |
Observable<Int> |
Cold | SignalProducer<Int, MyError> |
Observable<Int> |
ずっと RxSwift に慣れていたので Hot / Cold
の区別がつくことがどれだけ嬉しいか、正直未知数でしたが使ってみるととても便利です。
RxSwift を使っていると「この Obsevable
って Hot
?Cold
?どっちやったっけ」と考える必要がありますがそれがなくなります。
こういった暗黙の了解がなくなるのは嬉しいですね。
※ Hot / Cold
の違いについては @ukitaka さんの 今日こそ理解するHot / Cold が分かりやすいです。
Error の型を指定できる
Signal / SignalProducer
の 第二型パラメータに Error の型 を渡すようになっています。
ReactiveSwift | RxSwift | |
---|---|---|
Hot | Signal<Int, MyError> |
Observable<Int> |
Cold | SignalProducer<Int, MyError> |
Observable<Int> |
RxSwift は エラーが Error プロトコルとしてしか取り出せない ので、場合によってはキャストする必要があります。
// RxSwift の場合
let myObservable: Observable<Int> = ...
myObservable
.subscribe(onError: { error in
guard let myError = error as? MyError {
// ここに来ることはないという"暗黙の了解"が発生する
return
}
// 何かする
})
.disposed(by: disposeBag)
一方で ReactiveSwift ではエラーの型を明示的に表すので余計なキャストをすること必要はありません。
// ReactiveSwift の場合
let myProducer: SignalProducer<Int, MyError> = ...
myProducer.startWithFailed { (myError: MyError) in
// myError は MyError 型ということがコンパイル時に保証される!
}
「Error の型が分かっててもどうせ localizedDescription しか使わんしな〜」 という方もいらっしゃるかもしれません。
👇🏼しかしこちらをご覧ください。
// エラーの型が NoError というところがポイント👇🏼
let noErrorProducer: SignalProducer<Int, NoError> = ...
Signal / SignalProducer
のエラー型に NoError
を指定するとその名の通り エラーが起きないということを表現できます。
エラーが起きないという 暗黙の了解を型で表現できる のが ReactiveSwift の嬉しいところですね。
NoError について
ところで「NoError
て急に出てきたけど何?」と疑問を抱いた方もいらっしゃると思います。
ちょっと定義を見てみましょう。
/// An “error” that is impossible to construct.
///
/// This can be used to describe `Result`s where failures will never
/// be generated. For example, `Result<Int, NoError>` describes a result that
/// contains an `Int`eger and is guaranteed never to be a `failure`.
public enum NoError: Swift.Error, Equatable {
public static func ==(lhs: NoError, rhs: NoError) -> Bool {
return true
}
}
NoError
の実態は enum
です。しかし case
を一つも持たないので インスタンス化できません 。
これを利用してエラーが発生しないということを表現しています。
Action が便利
ReactiveSwift には Action
というクラスがあります。これもまた便利です。
Action
を使うと
1. ボタンタップ
2. API 通信
3. 成功時: 画面遷移 / 失敗時: アラートを表示
※ただしタップ連打などで通信が二重に走らないように制御する
といった、よくあるパターンが簡単に実装できます。
ただ説明するのが難しすぎるのでまずサンプルコードをご覧ください。
わかりにくかったらごめんなさい。多分わかりにくいので別の記事で詳しく紹介したいです。
サンプルコード
// わかりやすくするためになるべく型を省略せずに書いています
class APIClient {
func searchUsers(name: String) -> SignalProducer<[User], APIError> {
return ...
}
}
let apiClient = APIClient()
// 👇🏼ここで定義しているのが Action
// ここでは String をトリガーにして
// searchUsers を呼び出す Action を定義しています
let action = Action<String, [User], APIError> { input in
return apiClient.searchUsers(name: input) // ②
}
// 👇🏼 searchUsers の値を購読
action.values.observeValues { (users: [User]) in
print(users) // ③-A
}
// 👇🏼 searchUsers のエラーを購読
action.errors.observeValues { (error: APIError) in
print(error) // ③-B
}
// 👇🏼 "John" をトリガーに action を実行
action.apply("John").start() // ①
① → ② → ③-A or ③-B
の順番で実行されます。
Action の嬉しいところ
1. 一度に実行できる Action は 1 つまで
このおかげで ボタンタップ→通信
というよくあるフローを実装するときに、連打されて通信が二重に走るということが無くなります。
2. エラーが発生しても購読が終了しない
Signal / SignalProducer
はエラーが発生するとストリームが終了してしまいますよね。
Action
を介すると エラーの発生を onError ではなく onNext として流してくれます。
なのでエラーが発生してストリームが終了するという事がなくなります。
3. Action が実行中かどうか判定できる
Action
には let isExecuting: Property
というプロパティが生えています。
その名の通り Action
が 実行中かどうかを表すプロパティ です。
こいつを使えば 通信中はローディングを表示する といったよくあるパターンを簡単に実装できます。
let activityIndicator = UIActivityIndicatorView()
activityIndicator.reactive.isAnimating <~ action.isExecuting
最初理解するまで苦労しました Action
はとても便利です。
ちなみに RxSwift にも Action
の概念は存在しています。
→ RxSwiftCommunity/Action
RxSwift をやっている方で「ようわからん!」という方は下の記事がわかりやすいです。
→ Actionを使って快適なViewModel生活を
最終的に「RxSwift でいいや…」となったとしても Action の存在はぜひ知っておきたいところです
CocoaAction も便利
CocoaAction
はボタンのタップなどの UI
のイベントと Action
を仲介してくれるクラスです。
サンプルコード
// 👇🏼 さっき作った Action
let action: Action<String, [User], APIError> = ...
button.reactive.pressed = CocoaAction(action) {
textField.text ?? "" // タップ(=Void) を String へ変換し action への入力とする
}
たったこれだけのコードで
– ボタンのタップが action への入力に変換されて通信のトリガーになる
– action の処理が実行中は button の isEnabled が false になる
ということを実現できます。
よくある実装パターンで当然 RxSwift でもできるのですが Action
/ CocoaAction
を使うとよりスッキリかけます。
ReactiveSwift と RxSwift の対応
RxSwift から ReactiveSwift に乗り換える時に覚えておきたいのが RxSwift と ReactiveSwift のインターフェースの差です。
ここでは一部ですが ReactiveSwift と RxSwift の対応をご紹介します。
ReactiveSwift にも RxSwift にも存在するもの
インターフェースが同じもの
map, withLatestFrom, merge, combineLatest…など
インターフェースが異なるもの
PublishSubject / Signal.pipe
RxSwift でいう PublishSubject
は ReactiveSwift の Signal.pipe
だと個人的に思っています。(合ってる?)
// RxSwift
let subject = PublishSubject<Int>()
subject
.bind {
print($0)
}
.disposed(by: disposeBag)
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
// ReactiveSwift
let (output, input) = Signal<Int, NoError>.pipe()
output.observeValues {
print($0)
}
input.send(value: 1)
input.send(value: 2)
input.send(value: 3)
Signal<Int, NoError>.pipe()
は input と output がそれぞれ分かれています。
Observable.create / SignalProducer.init
Cold
な Obsevable
の作成の違いです。
// RxSwift
let observable = Observable<Int>.create { observer in
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onCompleted()
return Disposables.create()
}
// ReactiveSwift
let producer = SignalProducer<Int, NoError> { observer, _ in
observer.send(value: 1)
observer.send(value: 2)
observer.send(value: 3)
observer.sendCompleted()
}
flatMap
RxSwift の flatMap / flatMapLatest / concat / amb
は ReactiveSwift では flatMap
にまとめられています。
flatMap
の第一引数に FlattenStrategy
を渡すことで挙動を変えます。
in RxSwift | in ReactiveSwift |
---|---|
flatMap |
FlattenStrategy.merge |
flatMapLatest |
FlattenStrategy.latest |
concat |
FlattenStrategy.concat |
amb |
FlattenStrategy.race |
– | FlattenStrategy.concurrent(limit: UInt) |
こんな感じで第一引数に FlattenStrategy
を渡して呼び出します。
producer.flatMap(FlattenStrategy.latest) { ... }
FlattenStrategy.concurrent
は ReactiveSwift にだけあるものです。
concat
の同時起動数を指定できます。
flatMapFirst
的なオペレータはまだないようです。こちらの issue で作られそうな雰囲気があります。
→ [Idea] Add FlattenStrategy.throttle
BehaviorRelay / MutableProperty(と Property)
RxSwift の BehaviorRelay
(旧 Variable
) に対応する ReactiveSwift のクラスは MutableProperty
です。
let count: MutableProperty<Int> = MutableProperty(0)
// 値の更新
state.value = 1
また Property
というクラスもありこちらの value
は immutable
です。
読み取り専用の BehaviorRelay という感じです。
RxSwift にはなかったので便利ですね。
let state: Property<Int> = Property(0)
// 値の更新はできない❌
state.value = 1
MutableProperty から Property へも変換できます。
// MutableProperty から Property への変換
let mutableProperty = MutableProperty(0)
let readOnlyProperty = Property(mutableProperty) // or Property(capturing: mutableProperty)
MutableProperty or Property を別の Property に変換するメソッドもあります。
let userName = MutableProperty("")
let isEnabled: Property<Bool> = userName.map { !$0.isEmpty }
ちなみに BehaviorRelay
は RxCocoa に定義されていますが
Property / MutableProperty
は ReactiveSwift に定義されています。
distinctUntilChanged / skipRepeats
これらは名前が違うだけで同じです。
ReactiveSwift にしか存在しないもの
全部あげるとキリがないので面白いなと思ったものをピックアップします。
skipNil
その名の通り nil
をスキップします。スキップすると同時にアンラップもしてくれます。
let optional: SignalProducer<Int?, NoError> = ...
let skipNil: SignalProducer<Int, NoError> = optional.skipNil() // ← アンラップされている
combinePrevious
直前の値を取得するオペレータです。
let values: SignalProducer<Int, NoError> = ...
values
.combinePrevious()
.map { previous, current in
// 何かする
}
RxSwift でもオペレータを組み合わせたら可能です。
→ Rxで1つ前の値を取得する
RxSwift にしか存在しないもの
こちらも一部だけです。
Driver, Single, Maybe, Completable
ReactiveSwift にはこれらに対応する型がありません。
ただ、現時点ではそんなに困ってないです。
RxCocoa の DelegateProxy
RxCocoa には UIKit の delegate や dataSource などの Rx 拡張があります。
delegate を実装しなくてもいいので便利です。
// UIScrollViewDelegate の didScroll メソッドの Rx 拡張
scrollView.rx.didScroll.bind {
print("didScroll")
}
しかし ReactiveCocoa にはありません。
この点は RxSwift いいなあと思います。
まとめ
ReactiveSwift では RxSwift における暗黙の了解を型として表せるのが一番嬉しいポイントだと思います。
「RxSwift 流行ってるしやってみるか」となる方が多いと思うのですが ReactiveSwift も便利なので RxSwift と ReactiveCocoa 比べてみてからどちらを使うか検討してもいいのではないでしょうか。