UIViewControllerのライフサイクルとRxSwiftのお話


みなさん、こんにちは。freddiです。

本記事は、iOS Advent Calender 2018の23日目の記事として、UIViewControllerのライフサイクルとRxSwiftのお話をさせていただきます。短い記事ですが、よろしくおねがいします。

本記事では、都合上RxSwiftに関しての入門の説明はほぼ無いので、ご注意ください。1

危険なRxSwiftの使い方

まず先に、UIViewControllerのライフサイクルとRxSwiftが関係するアンチパターンの例を紹介します。

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    someThingObservableValue.flatMap { ... } // someThingObservableValueはObservableな変数
        .subscribe( ... )
        .disposed(by: disposeBag)
}

一見、ちゃんと動くように見えるコードですね、Observableな変数をsubcribeするだけのコードです。

しかし、ここで気にしてほしいのが、subcribeしている関数です。viewWillAppearは、何回も呼ばれる可能性があるライフサイクルの関数です。 2

このコードの例だと、

  • viewWillAppearのような複数回呼ばれる可能性のある関数でsubscribeを行おうとしている3
  • 複数回同じようなストリームをsubcribeをしてしまうことになる
  • 呼ばれた分だけsubscribe処理が働くことになり、subscribeで重い処理などを行っていると大変なことになる

という重大な問題が出てきます。これは、Multiple Subscribingのような名前で呼ばれていることがあります。subscribeで重い処理をするしない関係なく、Multiple Subscribingは避けるべきです

対処法

その1、無難にviewDidLoadに入れる

override func viewDidLoad() {
    super.viewDidLoad()
    someThingObservableValue.flatMap { ... } 
        .subscribe( ... )
        .disposed(by: disposeBag)
}

viewDidLoadは一回しか呼ばれないので、subscribeするものはviewDidLoadに入れたほうが良いです。
ただ、これもこれで、viewDidLoadが非常に大きくなる問題もあるのでご注意ください4

その2、Completeを即流す

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    Observable.just(())
        .withLatestFrom(someThingObservableValue)
        .flatMap { ... } 
        .subscribe( ... )
        .disposed(by: disposeBag)
}

多分ですが、viewDidLoadに入れることがそもそも解決策ではないこともあると思います(viewWillAppearでどうしてもやりたいこととか)。

そんな時、Observableから使える.just(:)などを使えば、引数で与えられた値をストリームに流したあとにCompletedを流します。5
これによりsubscribe後に即ストリームを開放するという事もでき、subscribeする関数を複数回呼び出してもMultiple Subscribingが起こる心配はありません。

ただ、目的の値を流すとなるとwithLatestFromのような関数を利用しなければならず、場合によってはflatMapのネストが無駄に深くなるなどの厄介な点も出てきます

その3、BehaviorRelayがいい説

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    if someBehaviorRelayValue.value ... {
       ...
    }
}

そもそもBehaviorRelayを使えば、asObservable()せず(ストリームも作らず)に値を見ることができます。何回もsubscribeが呼ばれるような場合で、かつBehaviorRelayに置換できそうなら置換したほうがいいです。BehaviorRelaysubscribeできますし・・・。

その4、そもそもRxがいらない説2

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    ... // Rxを使わない方法でゴニャゴニャ
}

これ一番上に持って行きたかったですが、怒られそうなのであえてここに書きました。Rx使わないのも、それでかなり良い選択です6
RxSwiftがかなり根付いているプロジェクトだと、この検討は多分難しいですが・・・。Rx乱用ダメ絶対。

終わりに

私がRx初心者の頃にやらかしたことを記事にしました。ざっくりとした紹介でしたが、みなさんのご参考になりましたでしょうか?
もしTipsや修正すべき点があれば、気軽にリクエストとコメントをよろしくおねがいします。

では今回は失礼します。皆様ありがとうございました。

次の日はFumiya Sakaiさんの記事です!皆さんもお楽しみに!


  1. これは個人的な意見ですが、RxSwiftを触ろうとする前に、ReactiveXについて調べるほうがいいかもしれません。理由はいくつかありますが、私はReactiveXというものを知ることで、他の言語のRxのコードがRxSwiftでも参考になることを知りました。 

  2. https://qiita.com/sgr-ksmt/items/e23e684c5e46ea3e8d08 

  3. 自分で作った関数を呼び出すとき、その中でsubscribeをすれば、そこでMultiple Subscribeの危険性も出てきます。ですが、シングルトンではないかつライフサイクルの短い(一つのスコープのみに存在する)オブジェクトではこの限りでないです。 

  4. この問題は"Fat viewDidLoad"という名前で呼ばれているのをたまに見ます。 

  5. 他にも、fromも値を流したあとにCompletedを流します。この記事ではjustfromに似ている関数について調べることができます。 

  6. RxSwiftは必要に応じて使うようにして、必要無いならばなるべく使わないという方針が後々圧倒的に楽です。