RxSwift MVVM における ViewModel 設計


自分は RxSwift で ViewModel はどうやって設計していくか下記三つ分けて書いていこうと思います。

  • Immutable
  • テストのしやすさ
  • プロトコルの読み取りやすさ

Immutable

  • 可変を避けるため struct を使う。
  • Observable, PublishSubject など値が流れ流けど不可変にする。
    • Protocol には var で定義されるが、struct では let で宣告します。

テストのしやすさ

テストしやすく、 mock しやすくするため、 外部リソースはできれば全て DI できるようにします。
例えばデータ処理、API 叩きなどが書いてある struct や class らはコンストラクタの引数で渡します。
そして、 ViewModel 自身の中身に、全域的なオブジェクトやクラスを直接使用すること、できればしないようにします。

プロトコルの読み取りやすさ

  • インプットとアウトプットを分けて定義する

RxSwift コンポーネントの使い分け

  • インプットは PublishSubject を使う
  • アウトプットは Driver を使う
    • 出口は UI なので Driver の方が良い
    • Driver はコンストラクタで初期化する

実装例

import RxSwift
import RxCocoa

protocol MyViewModelInput {
    var titleChanged: PublishSubject<String?> { get }
    var contentChanged: PublishSubject<String?> { get }
}

protocol MyViewModelOutput {
    var titleLength: Driver<Int> { get }
    var contentLength: Driver<Int> { get }
}

typealias MyViewModelType = MyViewModelInput & MyViewModelOutput

struct MyViewModel: MyViewModel {
    let titleChanged = PublishSubject<String?>()
    let contentChanged = PublishSubject<String?>()

    let titleLength: Driver<Int>
    let contentLength: Driver<Int>

    init() {
        titleLength = titleChanged
          .map { title in
              guard let title = title else { return 0 }
              return title.count
          }
          .asDriver { _ in .empty() }


        contentLength = contentChanged
          .map { content in
              guard let content = content else { return 0 }
              return content.count
          }
          .asDriver { _ in .empty() }
    }
}

ViewModel 以外は?

アウトプットがある場合、相手は UI 出なければ Observable や Single でしておいて大体問題ないと思います。

わかりにくい、説明不足ところや間違えたところあればぜひコメント欄にコメントしてください。