【RxSwift】DriverとSignalの特徴・違い

公開日: 

はじめに

この記事では、RxSwift では頻出のDriverSignalについて解説します。

公式ドキュメント

https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md#driver

『Driver』 『Signal』とは

RxSwift にはTraitと呼ばれる、さまざまな場面で利用できる Observable のラッパーが用意されています。その中でも、UI に関することに特化しているのがRxCocoa Traits、『Driver』と『Signal』はその中に属するオブジェクトです。少し難しそうなことをつらつらと書きましたが、本当に簡単にいうと

  • 『Driver』と『Signal』は特殊能力を持った『Observable』
  • UI に特化した『Observable』

だいぶ簡略化した認識ですが、この認識でこの先を理解できるかと思います。

『Observable』との違い

『Driver』『Signal』は『Observable』と比べると以下の特徴があります。

『Driver』 『Signal』の特徴

  • エラーを流さない
  • メインスレッドで通知をする
  • スレッドを共有する

詳しく見ていきましょう。Observableを使った以下の例で説明します

sample1.swift
let results = textField.rx.text
  .flatMapLatest { query in
    getItems(query) // textFieldの文字何らかの処理をしてデータを取得してくる
  }

results
  .map { "\($0.count)" }
  .bind(to: label.rx.text) // 取得したデータの数をcountLabelに表示
  .disposed(by: disposeBag)

results
  .bind(to: tableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
    cell.textLabel?.text = "\(result)" // 取得したデータをtableViewのcellの中に表示
  }
  .disposed(by: disposeBag)

このコードの想定されている動きとしては

  1. 何かしらのデータを取得してきて、データの数を Label に表示
  2. データを TableView の Cell に表示

こんな感じ。でも、このコードにはいくつか問題点があります。

問題点 ①: 結果がメインスレッドで帰ってくる保証がない

swift には UI の描画や更新はメインスレッドで行わなければならないというルールがあります。これが原因でアプリがクラッシュしてしまった経験がある人は少なくないのではないでしょうか。なので、メインスレッドで結果が帰ってくる保証がないのは致命傷です。

問題点 ②: エラーが返ってこない保証がない

同じように、UI の描画や更新をするときにエラーが発生してしまうと、アプリがクラッシュしてしまいます。なので、エラーが帰ってこない保証がないのもまずい。

問題点 ③ 二つのストリームが生成されてしまう

コードを見てわかるように今回の例ではresultというストリームを 2 つの UI パーツの更新に利用しています。すると、2 つのストリームが生成されてしまい、それぞれで値が流れます。

こんなイメージ。理想的には、1 つのストリームの結果を 2 つの UI の更新に利用したいのでこれはあまり良くない。これらの問題点を解決するために、修正したのがこちらのコード

sample2.swift
let results = textField.rx.text
  .flatMapLatest { query in
    getItems(query)
      .observeOn(MainScheduler.instance)  // 結果をメインスレッドで通知
      .catchErrorJustReturn([])           // エラーは発生させない(エラーなら空配列を流す)
  }
  .share(replay: 1)                           // ストリームを共有する

results
  .map { "\($0.count)" }
  .bind(to: label.rx.text)
  .disposed(by: disposeBag)

results
  .bind(to: tableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
    cell.textLabel?.text = "\(result)"
  }
  .disposed(by: disposeBag)

これで UI に関するやりとりもバッチリ!しかしまたもや問題が

毎回これ気にしながらかくの流石にめんどくさくないか??

それはそう。このままでは、UI に関する処理を書くたびに、これを全て書かなければならない。そこで生まれたのが、DriverSignalさっきのコードを Driver を使って書くと、このようになります。

sample3.swift
let results = textField.rx.text.asDriver()        // Driverに変換
  .flatMapLatest { query in
    getItems(query)
      .asDriver(onErrorJustReturn: [])
  }

results
  .map { "\($0.count)" }
  .drive(label.rx.text)               // Driverはbindの代わりにdriveを使用
  .disposed(by: disposeBag)

results
  .drive(tableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
    cell.textLabel?.text = "\(result)"
  }
  .disposed(by: disposeBag)

Driver は UI の描画や更新処理に使用されることが想定されているため

  • 結果がメインスレッドで返ってくる
  • エラーが返ってこない
  • 1 つのストリームの結果を共有する

これらの特徴を持ちます。つまり、UI の処理でめんどくさいことを書かなくても DriverSignal にすることで、自動でやってくれるんです。以上が『Observable』と『Driver』『Signal』の違いでした。

『Driver』と『Signal』の違い

Driver, Signalが UI に関することに特化した Observable だということは理解した。

じゃあ、DriverSignalの違いは??

今回の本題です。正直に言いうと、わかりませんでした。ここから先は自分なりに納得できた解釈を書きます。一般的に、DriverSignal の違いは

  • 『Driver』は購読されたときに、1 つ前のイベントをストリームに流す
  • 『Signal』は購読されたときに、イベントを流さない

というふうに多くの記事に書かれています。今回、その違いを理解しようとして色々とコードを書いて実験してみましたが、自分で違いを理解することはできませんでした。ですが、色々自分で実験し、インターン先の iOS エンジニアの方々に聞きまくって自分なりに理解はできたので、それを書きます。

実験1

実験1.swift
// MARK: - Driver
let result = self.textField.rx.text.orEmpty.asDriver()

self.textField.text = "テスト"

result
  .map{ "\($0.count)文字です" }
  .drive(self.label.rx.text)
  .disposed(by: disposeBag)


// MARK: - Signal
let result = self.textField.rx.text.orEmpty.asSignal(onErrorJustReturn: "error")

self.textField.text = "テスト"

result
  .map{ "\($0.count)文字です" }
  .emit(to: self.label.rx.text)
  .disposed(by: disposeBag)

ほとんど同じ 2 種類のコードを用意。違いは Driver か、Signal か、それだけです。Driver は replay するから初めからラベルには、『3 文字です』と表示されるはず!Signal は replay しないから、ラベルには何も表示されないはず!結果は、2 つとも全く同じ挙動。

いやなんで?

調べてみると、Driver・Signal に変換する前のControlPropertyがそもそも replay の特性を持っている。

公式に書いてある

https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md#controlproperty%E2%80%93controlevent

なので Driver・Signal に関係なく購読したら 1 個前のイベントが流れてくる。なるほど。実験失敗。

実験2

実験2.swift
class MainViewController: UIViewController {

    private let disposeBag = DisposeBag()

    private let publishRelay: PublishRelay<Int> = PublishRelay<Int>()
    var driver: Driver<Int>!
    var signal: Signal<Int>!

    var count = 0

    // 購読するボタンアクション
    @IBAction func subscribeAction(_ sender: Any) {
      // MARK: - Driver
      driver
        .drive(onNext: { result in
          print("driver結果: \(result)")
        })
        .disposed(by: disposeBag)

      // MARK: - Signal
      signal
        .emit(onNext: { result in
          print("signal結果: \(result)")
        })
        .disposed(by: disposeBag)
    }

    // PublishRelayにイベントを流すボタンアクション
    @IBAction func acceptAction(_ sender: Any) {
      count+=1
      publishRelay.accept(count)
    }

    override func viewDidLoad() {
      super.viewDidLoad()

      // MARK: - Driver
      driver = publishRelay.asDriver(onErrorJustReturn: 99)

      // MARK: - Signal
      signal = publishRelay.asSignal()

    }
}

今度はPublishRelayでイベントを発火させる。PublishRelay は replay の性質がないので実験 1 のように失敗はしないはず。PublishRelayDriver・Signal に変換して、購読する前にイベントを数回流しておくそして、購読したら Driver には 1 つ前の処理が流れて、Signal の場合には流れないはず。結果は全く同じ挙動。マジでなんでかわからん。完全に敗北しました。

結論

DriverSignal を利用した実装のよくあるパターンとしてBehaviorRelayPublishRelay と一緒に使用する方法があります。

参考記事

https://qiita.com/gyama_X/items/1c24bca68a14a92c5ce3

実装については長くなってしまうので説明は割愛します。詳しくは記事を見てください。要するに、

  • BehaviorRelay → Driver
  • PublishRelay → Signal

こんな感じで変換するのがもはやテンプレになっています。その事実を踏まえると、僕の個人的な DriverSignal の違いの理解としては

BehaviorRelayPublishRelay の違いが、そのまま DriverSignal の違いとして一般的な理解になっているのではないか

という結論に至りました。確かに

  • BehaviorRelay → Driver
  • PublishRelay → Signal

に変換してそれぞれを監視した時 Driver は購読したときに 1 つ前のイベントを流し、それ以降もイベントを流します。そして、Signal は購読したときにイベントを流さず、それ以降のイベントを流します。これは、結局 BehaviorRelayPublishRelay の性質なんですが、字面だけ見ると DriverSignal の違いのように思えます。僕は、これが DriverSignal の違いの真実だと本気で思っています。ですが、実際には公式ドキュメントで

https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md#signal

Driver と Signal の違いは replay するかしないかであると明記されているので、理解として正しくないかもしれませんが、自分はこのように理解しました。

  • BehaviorRelay → Driver
  • PublishRelay → Signal

これはなんで変換できるんだ?必要ないんじゃないか??

って思いましたが、これは RxSwift 側も意図せず変換できてしまうだけで本当は必要ないメソッドである。っていう理解で落ち着きました。正直、DriverSignal のよくある実装方法さえ覚えていれば二つの違いはあまり詳しく覚えなくてもいいのかもしれませんね…以上、DriverSignal の個人的な理解でした。僕と同じように悩んでいる人の助けになれば幸いです。

最後に

インターン先の iOS エンジニアの方々に、この話をしたら一緒に真剣に悩んでくれて結局はこの記事のような結論になりました。たくさんの記事に DriverSignal の違いについては書かれていますが簡単そうで、意外と難しい題材なのかもしれません。今回のことを調べるうちに、いろんなことが知れて面白かったです。DriverSignal の違いがわかる方がいらっしゃったら、twitter でもなんでも教えていただけると嬉しいです。

では、Bye