Mercari Advent Calendar 2017 の4日目はソウゾウiOSエンジニアのorakaroがお送りします。
ソウゾウ社はメルカリグループの新規プロダクトを多数開発していますが、ほとんどのiOS版アプリでリアクティブライブラリのRxSwiftを採用しています。RxSwift 4 / RxCocoa 4にいくつか新しいクラスが実装されましたので、そのクラスの実装を覗きながら紹介します。
今回紹介するクラスは Signal
, PublishRelay
, BehaviorRelay
です。
Signal
RxCocoa をよく使っている方はご存知だと思いますが、UIレイヤーのリアクティブプログラミングのためにDriver
という Trait が提供されています。
Signal
はDriver
に近い物ですが、SharingStrategy
だけが異なります。サブスクライブされる時にDriver
は一回replayしますがSignalはreplayしないです。
Driver
とSignal
のストリームは共に、
- エラーを流さない
- メインスレッドの保証
という性質を持つので、ViewControllerのUIバインディングで推奨されています。
Driver
とSignal
のサブスクライブする時のreplayはこんなイメージです
Driver: Signal: --------a---b------------c--------|-> --------a---b------------c--------|-> Subscribed: (sub) (sub) b-------c--------|-> --------c--------|-> a, b, c, d are events (sub) is subscribe timing | is the 'completed' signal ---> is the timeline
Signal
はreplayされると困るストリームに便利だと言われていますが、個人の経験ではreplayされないと困る場面の方が多かったため、これまでずっとDriver
を使ってきました。
例えばViewModel、ViewControllerのバインディングと最初のトリガーのタイミングが違う時、トリガーがバインディングの前になってしまうと最初のイベントが流れてこないバグが多くありました。
この場合Driver
を使うとトリガーとバインディングの前後順序を気にしなくても大丈夫です。
Relayクラス
RxCocoa 4 からRelay
クラスが実装され、下記の性質が保証されます。
- エラーを流さない
- completeを流さない
今回PublishRelay
とBehaviorRelay
が実装されましたが、PublishRelay
はPublishSubject
のwrapperで、BehaviorRelay
はBehaviorSubject
のwrapperになります。
Relay
クラスはObservableType
プロトコルに準拠していますが、注意すべきなのは、Subject
クラスと違ってObserverType
に準拠していないことです。Observableからcomplete / errorを受け付けたくないので、bind(to:)
メソッドは利用しないように推奨されています。
PublishRelay
PublishRelay
はPublishSubject
のwrapperです。
public final class PublishRelay<Element>: ObservableType { private let _subject: PublishSubject<Element> public init() { _subject = PublishSubject() } }
BehaviorRelay
BehaviorRelay
はBehaviorSubject
のwrapperです。
public final class BehaviorRelay<Element>: ObservableType { private let _subject: BehaviorSubject<Element> public var value: Element { return try! _subject.value() } public init(value: Element) { _subject = BehaviorSubject(value: value) } }
VariableのDeprecated
BehaviorRelay
はVariable
と同じくvalue
を持っていますが、BehaviorRelay
のvalue
は読み込み専用です。Variable
のように.value =
でアサインすることができません。
そもそもVariable
のvalue
のアサインは命令形プログラミングのコーディングスタイルなので、 Reactive の宣言型プログラミング環境の中には存在すべきではないと思っています。将来時にはVariable
をdeprecate
してBehaviorRelay
の alias に定義する予定なので、これから対応し始めた方が良いかと思います。
Relayクラスにイベントを流したい時
Relay
にイベントを流したい時は2つの方法があります。
ひとつはaccept
メソッドを使って直接イベントを受け取ることができます。
someRelay.accept(someEvent)
もう一つの方法はSignal
からPublishRelay
にバインドするか、Driver
からBehaviorRelay
にバインドするかです。
someSignal .emit(to: somePublishRelay) .disposed(by: disposeBag) someDriver .drive(someBehaviorRelay) .disposed(by: disposeBag)
PublishRelay
はPublishSubject
のwrapperなのでreplayしない、BehaviorRelay
はBehaviorSubject
のwrapperなので最後のイベントだけreplayするようになっています。
Signal
とDriver
の中でReplayのStrategyが一致するものだけがバインドできるように作られています。
// Signal+Subscription.swift public func emit(to relay: PublishRelay<E>) -> Disposable { return emit(onNext: { e in relay.accept(e) }) } // Driver+Subscription public func drive(_ relay: BehaviorRelay<E>) -> Disposable { MainScheduler.ensureExecutingOnScheduler(errorMessage: errorMessage) return drive(onNext: { e in relay.accept(e) }) }
completeしないと何が嬉しいのか
当たり前になってしまいますが、completeすると困る場合に嬉しいです。
複数のストリームからアプリ共通のPublishSubject
にバインドした時、どこかで.complete
が流されたらPublishSubject
が終了しまう場面が考えられると思います。
元々のストリームから.complete
を潰したい時に、よく下記のextensionを使います。
extension SharedSequence { public func neverComplete() -> Driver<E> { return asObservable() .concat(Observable.never()) .asDriverIgnoringError() } } postItemButton.rx.tap.asDriver() .map { InterceptedEvent.postOffer(handler: { /*...*/ }) } .neverComplete() .drive(ProfileManager.triggerEvent) .disposed(by: disposeBag)
但し、neverComplete()
が呼ばれることが保証されないため、結局コンパイル時にバグ検知できません。
これを共通のPublishSubject
をやめてPublishRelay
に置き換えたら、.complete
が流れてこないことを保証できます。
postItemButton.rx.tap.asDriver() .map { InterceptedEvent.postOffer(handler: { /*...*/ }) } .drive(ProfileManager.triggerRelay) .disposed(by: disposeBag)
終わりに
Signal
の登場によってUIレイヤーのリアクティブ表現が豊かになり、Replay
イベントが流れるかを意識しながら使い分けることができるようになりました。更にバインド先にPublishRelay
とBehaviorRelay
を使うことによって、エラーと終了イベントが流されないことをコンパイル時に保証できます。実行時のクラッシュだけではなく、思わぬ挙動の不具合も検知しやすくなるでしょう。
ドメインレイヤーやネットワークレイヤーにRaw Observableを使っても全然大丈夫だと思いますし、むしろエラーや終了イベントがちゃんとあった方が明示的なると思いますが、UIレイヤーからは RxCocoa の Trait であるDriver
、Signal
, Relay
を意識して使うべきだと思います。
Mercari Advent Calendar 2017次回は5日目、tenntennさんです。お楽しみに。