Edited at

明日からプロジェクトで RxSwift + MVVM を始める人への規約 & ノウハウ集


想定読者

iOS のプロジェクトで初めて RxSwift + MVVM で行くぞ!ってなったけど具体的にどのようにコードを書いていけば良いのかわからない人.


用語の説明


MVVM

MVVM は Model, View, ViewModelのことです. レイヤ構造で見ると以下のような感じになっています.


Model

Model はアプリケーションのデータ構造やそのデータをどのように処理するかが書いてあります. しばしば「ビジネスロジックを扱うもの」と表現されます. 現実には何がビジネスロジックなのか?という問を続けながらコーディングしていくことになります.

アプリケーションの機能が多いと肥大化するのでいい感じに分けながら作っていきましょう.


View

View はユーザーにデータを見せたり、ユーザーから入力を受け取ったりするものです. iOS では UIView や UIViewController のことです.

View は Model や ViewModel にロジックが逃げていくので比較的シンプルに保つことができます.


ViewModel

ViewModel は Model と View の間に入って橋渡しをするものです.「プレゼンテーションロジック」を担うものと表現されることもあります.

具体的には



  • View のあるボタンが押されたら -> Modelの ある処理をする


  • Model のあるデータが生成されたら -> View のある場所に表示する

といった関係を記述するものです.

注意しないとビジネスロジックが入り込んでしまうので気をつけてください.

要素が多い画面(プロフィール画面など)の場合は ViewModel が肥大化しがちなので気をつけましょう. 分割してもいいですが、下手に分割すると見通しが悪くなるので複雑なものは複雑なのだと諦めて Fat ViewModel にしておいてもいいと思います.


RxSwift

RxSwift は MVVM でアプリケーションを使うときには欠かせないライブラリです. この記事では RxSwift の使い方については特に触れませんが、どういう考え方で使えば良いのかを説明します.

そのためにまずは RxSwift 内で分割して定義されている framework を紹介します.


RxSwift

ReactiveX の機能が記述してあるコアな部分.


RxRelay

PublishRelay and BehaviorRelay が記述してある部分.


RxCocoa

Cocoa (UIなど) に関することが記述してある部分.


RxTest, RxBlocking

テストに使うものなのでここでは説明しません.


RxSwift + MVVM なプロジェクトでの規約集

ここから本題です. RxSwift + MVVM なプロジェクトをどのように作っていくのかには様々な意見があると思います. ここでは僕の経験から有用だったものを紹介していこうと思います.


ViewController は View

ViewController は View と割り切ったほうがわかりやすいです. ViewController は概ね以下のようなコードになります.

viewDidLoad が肥大化しますが下手に分割すると見通しが悪くなることもあるので仕方ないと割り切ってしまっても良いと思います(複雑なものは複雑なままにしておきましょう).

class なんちゃらViewController {

public let viewModel: なんちゃらViewModel! // <- ! にしておくと viewModel をセットし忘れたときにクラッシュするので気が付きやすい.(このあたりはお好みで.

public override func viewDidLoad() {
super.viewDidLoad()

// viewDidLoad で viewModel と view をつなげる. inputs, outputs については後述.
let outputs = viewModel.bind(inputs: inputs)
}
}


Model, ViewModel での UIKit, RxCocoa の import 禁止

まずは MVVM の各レイヤでどの framework を import して良いのかの決まりを作ると各レイヤの役割が明確になるのでおすすめです.以下の表を参照してください.

Model, ViewModel 層で UIKit の import を禁止にすることによって View の処理が入り込むのを防ぐことができます. この規約は機械的にレビューできるため費用対効果が大きいです.

レイヤ
UIKit import 可?
RxCocoa import 可?

Model


ViewModel


View


しかしながらこの規約は現実的には厳しすぎるときがあります. UIImage を使って Model 層で何かしたいときもあると思いますのでそういったときには実装の手間などを秤にかけて UIImage の使用を許可するか、あるいは protocol を使って抽象化するかを議論する必要があると思います. import するにしても import class UIKit.UIImage などとすると安心です.


Model の interface は protocol で記述

Model は外部に公開するインターフェースを定義した protocol と実際に処理を記述する class に分けましょう.

また以下の2つの決まり事を作っておくと変化に強い model になります.


  1. Model の変化の伝搬は Observable で行う(property, 関数どちらでも良い)

  2. Model への指示は return void な関数

import RxSwift

import RxRelay

public protocol なんちゃらModelProtocol {
// model のインターフェースを定義
var output: Observable<()> // model の変化を伝えるのは Observable で記述
func outputById(_ id: Int) -> Observable<()> // 引数が必要な場合はこのように
func doSomething() // model になにか処理をさせるのは return void な関数で記述
}

public class なんちゃらModel: なんちゃらModelProtocol {
// ビジネスロジックを記述
}


ViewModel の2つの決まりごと


  1. Inputs/Outputs で入出力を受け取る

  2. イニシャライザで必要な model を受け取る

import RxSwift

import RxRelay

public class なんちゃらViewModel {
struct Inputs {
// UI からの入力を宣言(以下は例)
let tapOk: Observable<()>
}
struct Outputs {
// UI への出力を宣言(以下は例)
let show: Observable<()>
}

let model: なんちゃらModel
// 必要な model をイニシャライザで受け取る. 1つとは限らない.
public init(model: なんちゃらModel) {
self.model = model
}

public func bind(inputs: Inputs) -> Outputs {

// model, inputs, outputs を使って処理を記述

return Outputs(...)
}
}


画面遷移は ViewModel で ViewModel を作って Outputs で渡す

画面 A から画面 B に遷移する場合は A_ViewModel の中で B_ViewModel を作って View に渡してあげる意図しない画面に遷移する状態を記述できなくなるのでおすすめです. 具体的には以下のようになります.

public class B_ViewController: UIViewController {

public let viewModel: B_ViewModel!
// 内容は省略...
}
public class B_ViewModel {
// 内容は省略...
}

public class A_ViewController: UIViewController {
public let viewModel: A_ViewModel!
public override func viewDidLoad() {
super.viewDidLoad()
let outputs = viewModel.bind(inputs: Inputs(tapShowB: showBButton.rx.tap))
// B に遷移.
outputs.showB.asSignal().emit(onNext: { vm in
let vc = B_ViewController()
// 意図しない ViewController を作ってしまっても viewModel の型が合わないのでここでエラーになる
vc.viewModel = vm
self.present(vc)
}).disposed(by: disposeBag)
}
}

public class A_ViewModel {
func bind(inputs: Inputs) -> Outputs {

// B_ViewModel を作って outputs で view にわたす
let showB = inputs.tapShowB.map{ B_ViewModel() }

return Outputs(showB: showB)
}
}


その他にも沢山あるけど疲れたのでこのへんで...