Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

RxCocoa 4 の Signal と Relay のまとめ

Mercari Advent Calendar 2017 の4日目はソウゾウiOSエンジニアのorakaroがお送りします。

ソウゾウ社はメルカリグループの新規プロダクトを多数開発していますが、ほとんどのiOS版アプリでリアクティブライブラリのRxSwiftを採用しています。RxSwift 4 / RxCocoa 4にいくつか新しいクラスが実装されましたので、そのクラスの実装を覗きながら紹介します。

今回紹介するクラスは Signal, PublishRelay, BehaviorRelayです。

Signal

RxCocoa をよく使っている方はご存知だと思いますが、UIレイヤーのリアクティブプログラミングのためにDriverという Trait が提供されています。

SignalDriverに近い物ですが、SharingStrategyだけが異なります。サブスクライブされる時にDriverは一回replayしますがSignalはreplayしないです。

DriverSignalのストリームは共に、

  • エラーを流さない
  • メインスレッドの保証

という性質を持つので、ViewControllerのUIバインディングで推奨されています。

DriverSignalのサブスクライブする時の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

Signalreplayされると困るストリームに便利だと言われていますが、個人の経験ではreplayされないと困る場面の方が多かったため、これまでずっとDriverを使ってきました。

例えばViewModel、ViewControllerのバインディングと最初のトリガーのタイミングが違う時、トリガーがバインディングの前になってしまうと最初のイベントが流れてこないバグが多くありました。 この場合Driverを使うとトリガーとバインディングの前後順序を気にしなくても大丈夫です。

Relayクラス

RxCocoa 4 からRelayクラスが実装され、下記の性質が保証されます。

  • エラーを流さない
  • completeを流さない

今回PublishRelayBehaviorRelayが実装されましたが、PublishRelayPublishSubjectのwrapperで、BehaviorRelayBehaviorSubjectのwrapperになります。

RelayクラスはObservableTypeプロトコルに準拠していますが、注意すべきなのは、Subjectクラスと違ってObserverType準拠していないことです。Observableからcomplete / errorを受け付けたくないので、bind(to:)メソッドは利用しないように推奨されています

PublishRelay

PublishRelayPublishSubjectのwrapperです。

public final class PublishRelay<Element>: ObservableType {
    private let _subject: PublishSubject<Element>
    public init() {
        _subject = PublishSubject()
    }
}

BehaviorRelay

BehaviorRelayBehaviorSubjectの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

BehaviorRelayVariableと同じくvalueを持っていますが、BehaviorRelayvalueは読み込み専用です。Variableのように.value =でアサインすることができません。

そもそもVariablevalueのアサインは命令形プログラミングのコーディングスタイルなので、 Reactive の宣言型プログラミング環境の中には存在すべきではないと思っています。将来時にはVariabledeprecateして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)

PublishRelayPublishSubjectのwrapperなのでreplayしない、BehaviorRelayBehaviorSubjectのwrapperなので最後のイベントだけreplayするようになっています。

SignalDriverの中で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イベントが流れるかを意識しながら使い分けることができるようになりました。更にバインド先にPublishRelayBehaviorRelayを使うことによって、エラーと終了イベントが流されないことをコンパイル時に保証できます。実行時のクラッシュだけではなく、思わぬ挙動の不具合も検知しやすくなるでしょう。

ドメインレイヤーやネットワークレイヤーにRaw Observableを使っても全然大丈夫だと思いますし、むしろエラーや終了イベントがちゃんとあった方が明示的なると思いますが、UIレイヤーからは RxCocoa の Trait であるDriverSignal, Relay を意識して使うべきだと思います。

Mercari Advent Calendar 2017次回は5日目、tenntennさんです。お楽しみに。