Edited at

MVVMに関して

More than 1 year has passed since last update.


MVCは何

MVCというアーキテクチャーは皆さんに熟知されると思います。MVC具体的にはModel、View、Controller三つの要素から構成されます。簡単に言うと、Modelはデーター層、Viewが表示層、ControllerがUIからの入力を担当します。

iOS開発ではViewControllerという存在がありまして、ViewControllerではViewとController両方の役割が持っています。それで、ViewControllerが複雑になったら、ViewController中でのロジックがどんどん増えて、FatViewControllerになります。FatViewControllerを改修することはかなり大変なことです。MVVMはその問題を解決する方法の一つ。他には色々な方法があるけど、今仕事で使ってるのはMVVMので、詳しく説明します。


MVVMは何

MVVMというアーキテクチャーはModel、View、ViewModel三つの要素で構成されます。Modelはデーター層、Viewが表示層、ViewModelがUIからの入力を受けて、ビューの更新を担当します。MVCのControllerと違うのはViewModelでViewの更新はデータバインディングの手法で実現します。iOS開発では、ViewControllerに対して、ViewModelを作って、うまくデータバインディングしたら、もともとFatViewControllerでデータを処理するロジックを簡単にViewModelに移行することができます。そうしたら、ViewControllerがMVCでのVになれます。


開発手法

実装を書く前に、いくつのルールを説明します。


  1. 一つのViewControllerは一つのViewModelのインスタンスしか持ってません

  2. ViewがViewModelのインスタンスを持ってないけど、プロトコルを利用してデーターバインディングします

  3. 実際の実装ではModelがViewModelに含まれるケースもあります

早速実装手法に関して説明します。データーバインディングはReactiveSwiftというライブラリを使ってやります。

class ViewModel {

//データーバインディング必要な変数
let paramA = MutableProperty<Int>(0)
init(data: [AnyHashable: Any]) {
if let a = data["paramA"] as? Int {
paramA.swap(a)
}
}
}

class ViewController: UIViewController {
private let viewModel: ViewModel

init(data: [AnyHashable: Any]) {
viewModel = ViewModel(data: data)
}

override func viewDidLoad() {
super.viewDidLoad()
bind(viewModel)
}

private func bind(_ viewModel: ViewModel) {
//データーバインディング
viewModel.paramA.take(during: reactive.lifetime).observeValue { (value) in
//paramAが変わったら、この変数を使ってるViewが自動的に更新されます。
}
}
}

ViewController初期化する時のデーターを利用してViewModelを初期化します。その後ViewDidLoadでViewModelとViewControllerをバインディングします。

ViewModelのparamAの数値が変わったら、ViewControllerでは自動的にその変化を反映します。

次は子Viewを追加して

protocol AViewModel {

var paramB: MutableProperty<String> { get }
}
class AView: UIView {
func bind(_ viewModel: AViewModel) {
viewModel.paramB.take(during: reactive.lifetime).observeValue{ (value) in
//paramBを使って、AViewを更新します。
}
}
}

まずAViewというViewを定義して、そしてProtocol型のViewModelを声明します。ここのViewModelがクラス型ではなくProtocol型で声明する理由は後で説明します。続いてはViewModelの修正です。

class ViewModel: AViewModel {

let paramA = MutableProperty<Int>(0)
let paramB = MutableProperty<String>("")

init(data: [AnyHashable: Any]) {
if let a = data["paramA"] as? Int {
paramA.swap(a)
}
if let b = data["paramB"] as? String {
paramA.swap(b)
}
}
}

ViewModelをAViewModelのプロトコルを実装して、ViewControllerにAView型の子Viewを追加して、同時にbind関数を修正します。

class ViewController: UIViewController {

private let viewModel: ViewModel
let aView = AView()

init(data: [AnyHashable: Any]) {
viewModel = ViewModel(data: data)
}

override func viewDidLoad() {
super.viewDidLoad()
view.addSubView(aView)
bind(viewModel)
}

private func bind(_ viewModel: ViewModel) {
//子Viewのデーターバインディング
aView.bind(viewModel)
//データーバインディング
viewModel.paramA.take(during: reactive.lifetime).observeValue { (value) in
//paramAが変わったら、この変数を使ってるViewを更新します。
}
}
}

こうすれば、子ViewとViewModelをデーターバインディングができます。なぜ子ViewのViewModelがクラス型ではなくプロトコル型ですか?このような実装は必ず正しいとは言えませんが、そうする三つの理由を説明します。



  1. 一つのViewControllerに対してデーター処理のロジックはできるだけ一箇所にまとめます
    もし子ViewのViewModelがデーターを処理するロジックを持っていれば、子Viewと子Viewの間にデーターを更新したい時、その実装はちょっと面倒になります。ロジックが一箇所にまとめたら簡単にできます。でもFatViewModel問題も発生かもしれない。


  2. 子ViewのViewModelがProtocol型にしたら、再利用は簡単です
    再利用の場合、この子Viewを持ってるViewControllerのViewModelを子ViewModelを実装すれば完了。Interface向けの考え方はOOP、SwiftではPOP(Protocol Oriented Programing)でもおすすめられます。


  3. メモリー管理がわかりやすくなる
    データーバインディンに対して、うまく書かないとメモリーリークの恐れがあります*(ViewModelとかViewなど生きたままViewControllerも釈放できません)。一つのViewModelになると、ViewModelのデーターに対して観察の動作をちゃんと終われば、ViewModelを釈放できます。

初めてのqiita投稿、m(..)mよろしくお願いします。