t__nabe_log

雑多 作業、学習ログ多め。

MVVM, RxSwiftの学習メモ その1 RxExampleのコードを読む

MVVM及びRxSwiftの学習を始めたので勉強したことをメモしています。 RxExampleのうち、GitHubSignup1はRxSwiftでつかわれるDriverなどを使用していないとのこと。全くRxSwiftのこともMVVMのことも理解していない状態で手探りで進めています。何か誤りがあればお知らせください。

GitHubSignup1での各層の役割

ViewController

ViewModelの役割

  • API用のロジックなどをイニシャライズのタイミングで外部から用意
  • イニシャライズで受け取ったObservableを出力に変換

コードを読む

ViewController初期化時に行われていることの概要

GitHubSignupViewController1はViewDidLoadのタイミングでViewModelを初期化しています。viewDidLoad内でのみ使用されていてGitHubSignupViewController1はプロパティとしてViewModelを保持していないです。 ViewModelの初期化時には引数inputにIBOutletで接続した各UIコンポーネントのプロパティをObservableに変換して渡してます。引数outputには依存するモジュールをまとめて渡しています。引数は両方共tupleです。 その後ViewModelの必要なプロパティにバインドして結果をViewに反映させています。

※今この時点でわからないこと - ViewModel破棄しても大丈夫なの?出力のストリームは破棄されないなのだろうか(ViewModel自体は持つべき情報がもうないからすぐに破棄してるんだろうと思います。)

ViewModelの出力をViewにバインドさせる

この処理を各部分でsubscribeとbind(to:)を使い分ける基準んがわかりませんでした。 bind(to:)を使う方が簡潔に書けるようです。 しかし、bind(to:)を使うには条件があるみたいで、

extension ObservableType {
  public func bindTo<O>(_ observer: O) -> Disposable where O : ObserverType, Self.E == O.E {
  //
  }
}

引数OがObserverTypeにconformしていて、かつObservableTypeにconformしているSelfのEとO.Eで==の関係が成り立つ場合に限るとなっています。 今回bind(to:)でバインドされているのを一つ取り出してみると

viewModel.validatedUsername
            .bind(to: usernameValidationOutlet.rx.validationResult)
            .disposed(by: disposeBag)

レシーバーがObservableで引数には usernameValidationOutlet.rx.validationResultが割り当てられています。usernameValidationOutlet.rx.validationResultはBinderで、Binderはpublic struct Binder: ObserverTypeでObserverTypeにconformしているのでbind(to:)メソッドによるバインドが可能になります。値をそのまま反映させるだけならこれで十分ということでしょうか。

Binderについて

  • エラーのバインドは不可
  • バインディングが特定のスケジューラで確実に実行されるようにしてくれる
  • Binderはターゲットを保持しない ターゲットが解放された場合には要素はバインドされない。

DisposeBagについて

disposeBagオブジェクト自身が破棄されるタイミングで登録されたsubscriptionをまとめて破棄する仕組みです。

Observableを変換

validatedUsername = input.username
            .flatMapLatest { username in
                return validationService.validateUsername(username)
                    .observeOn(MainScheduler.instance)
                    .catchErrorJustReturn(.failed(message: "Error contacting server"))
            }
            // ここでHot Observableに変換してる
            .share(replay: 1)  

上のコードを見ると、input.username: Observableからmapで文字列を取り出してvalidatenServiceの処理を適用してその結果をreturnしてストリームに変換されています。

shareが何をしているのか全く想像できなかったため、調べているとHot ObservableとCold Observableという表現が多く目に付きました。これについて解説しているのがRxSwiftのdocumentationにあります。 RxSwift/HotAndColdObservables.md at master · ReactiveX/RxSwift

Hot Observable

  • ストリームの途中に挟むとsubscribeするより前にストリームを稼働させることが出来る
  • 上流のCold Observableの起動と値の発行要求
  • ストリームを分岐させる
  • 自分から値を発行

Cold Observable

  • 自分から値の発行は不可
  • subscribeしてから動作
  • ストリームの分岐不可

share(replay:)はCold ObservableをHot Observableに変換します。subscribeされるたびに動作してしまうのを避けるためにHot Observableに変換しているようです。

shareメソッドを呼び出さずにsubscribeした際にflatMapLatestが2度呼ばれていることを以下のコードをinitializerに挿入することで確認します。

let hoge = input.username
            .flatMapLatest { username -> Observable<ValidationResult> in
                print("called flatMapLatest")
                return validationService.validateUsername(username)
                    .observeOn(MainScheduler.instance)
                    .catchErrorJustReturn(.failed(message: "Error contacting server"))
        }
        
        _ = hoge.subscribe({ _ in
            print("subscribed count 1")
        })
        _ = hoge.subscribe({ _ in
            print("subscribed count 2")
        })

ログには

called flatMapLatest
subscribed count 1
called flatMapLatest
subscribed count 2

と表示されました。shareメソッドを追記すると

called flatMapLatest
subscribed count 1
subscribed count 2

と表示されました。やはりCold ObservableからHot Observableへの変換が行われているようです。そのメリットがあることもわかりました。

flatMapLatestメソッド

initializer入力シーケンスから出力シーケンスへの変換の際、usernameの入力を出力に変換する部分のみmapではなくflatMapLatestを使用しています。

flatMapメソッドは元のObservableのイベントをObservableに変換して、その発行するイベントをマージするのに対して、flatMapLatestは次のイベントが来た時に前のイベントの処理をキャンセルします。

public func flatMapLatest<O>(_ selector: @escaping (Self.E) throws -> O) -> RxSwift.Observable<O.E> where O : ObservableConvertibleType

まとめ

わからないことをそれぞれ章立てしてメモしていきました。初めてコードを読んだ時は全く何をやっているか把握できなかったのですが、一通り読んだ後は何をしているか理解できるようになりました。メモの途中にあげた疑問点が解決したら記事を新たに作り、リンクを貼るようにします。