MVVMに関して


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よろしくお願いします。