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さんです。お楽しみに。




