【RxSwift】DriverとSignalの特徴・違い
公開日:
はじめに
この記事では、RxSwift
では頻出のDriver
とSignal
について解説します。
公式ドキュメント
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
を使った以下の例で説明します
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)
このコードの想定されている動きとしては
- 何かしらのデータを取得してきて、データの数を Label に表示
- データを TableView の Cell に表示
こんな感じ。でも、このコードにはいくつか問題点があります。
問題点 ①: 結果がメインスレッドで帰ってくる保証がない
swift には UI の描画や更新はメインスレッドで行わなければならないというルールがあります。これが原因でアプリがクラッシュしてしまった経験がある人は少なくないのではないでしょうか。なので、メインスレッドで結果が帰ってくる保証がないのは致命傷です。
問題点 ②: エラーが返ってこない保証がない
同じように、UI の描画や更新をするときにエラーが発生してしまうと、アプリがクラッシュしてしまいます。なので、エラーが帰ってこない保証がないのもまずい。
問題点 ③ 二つのストリームが生成されてしまう
コードを見てわかるように今回の例ではresult
というストリームを 2 つの UI パーツの更新に利用しています。すると、2 つのストリームが生成されてしまい、それぞれで値が流れます。
こんなイメージ。理想的には、1 つのストリームの結果を 2 つの UI の更新に利用したいのでこれはあまり良くない。これらの問題点を解決するために、修正したのがこちらのコード
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 に関する処理を書くたびに、これを全て書かなければならない。そこで生まれたのが、Driver
とSignal
さっきのコードを Driver
を使って書くと、このようになります。
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 の処理でめんどくさいことを書かなくても Driver
や Signal
にすることで、自動でやってくれるんです。以上が『Observable』と『Driver』『Signal』の違いでした。
『Driver』と『Signal』の違い
Driver
, Signal
が UI に関することに特化した Observable
だということは理解した。
じゃあ、Driver
とSignal
の違いは??
今回の本題です。正直に言いうと、わかりませんでした。ここから先は自分なりに納得できた解釈を書きます。一般的に、Driver
と Signal
の違いは
- 『Driver』は購読されたときに、1 つ前のイベントをストリームに流す
- 『Signal』は購読されたときに、イベントを流さない
というふうに多くの記事に書かれています。今回、その違いを理解しようとして色々とコードを書いて実験してみましたが、自分で違いを理解することはできませんでした。ですが、色々自分で実験し、インターン先の iOS エンジニアの方々に聞きまくって自分なりに理解はできたので、それを書きます。
実験1
// 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 の特性を持っている。
公式に書いてある
なので Driver・Signal
に関係なく購読したら 1 個前のイベントが流れてくる。なるほど。実験失敗。
実験2
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 のように失敗はしないはず。PublishRelay
を Driver・Signal
に変換して、購読する前にイベントを数回流しておくそして、購読したら Driver
には 1 つ前の処理が流れて、Signal
の場合には流れないはず。結果は全く同じ挙動。マジでなんでかわからん。完全に敗北しました。
結論
Driver
と Signal
を利用した実装のよくあるパターンとしてBehaviorRelay
と PublishRelay
と一緒に使用する方法があります。
参考記事
https://qiita.com/gyama_X/items/1c24bca68a14a92c5ce3
実装については長くなってしまうので説明は割愛します。詳しくは記事を見てください。要するに、
- BehaviorRelay → Driver
- PublishRelay → Signal
こんな感じで変換するのがもはやテンプレになっています。その事実を踏まえると、僕の個人的な Driver
と Signal
の違いの理解としては
BehaviorRelay
と PublishRelay
の違いが、そのまま Driver
と Signal
の違いとして一般的な理解になっているのではないか
という結論に至りました。確かに
- BehaviorRelay → Driver
- PublishRelay → Signal
に変換してそれぞれを監視した時 Driver は購読したときに 1 つ前のイベントを流し、それ以降もイベントを流します。そして、Signal
は購読したときにイベントを流さず、それ以降のイベントを流します。これは、結局 BehaviorRelay
と PublishRelay
の性質なんですが、字面だけ見ると Driver
と Signal
の違いのように思えます。僕は、これが Driver
と Signal
の違いの真実だと本気で思っています。ですが、実際には公式ドキュメントで
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md#signal
Driver と Signal の違いは replay するかしないかであると明記されているので、理解として正しくないかもしれませんが、自分はこのように理解しました。
- BehaviorRelay → Driver
- PublishRelay → Signal
これはなんで変換できるんだ?必要ないんじゃないか??
って思いましたが、これは RxSwift 側も意図せず変換できてしまうだけで本当は必要ないメソッドである。っていう理解で落ち着きました。正直、Driver
と Signal
のよくある実装方法さえ覚えていれば二つの違いはあまり詳しく覚えなくてもいいのかもしれませんね…以上、Driver
と Signal
の個人的な理解でした。僕と同じように悩んでいる人の助けになれば幸いです。
最後に
インターン先の iOS エンジニアの方々に、この話をしたら一緒に真剣に悩んでくれて結局はこの記事のような結論になりました。たくさんの記事に Driver
と Signal
の違いについては書かれていますが簡単そうで、意外と難しい題材なのかもしれません。今回のことを調べるうちに、いろんなことが知れて面白かったです。Driver
と Signal
の違いがわかる方がいらっしゃったら、twitter でもなんでも教えていただけると嬉しいです。
では、Bye