iOS開発におけるMVVMモデル——実用進級編(整理)

4431 ワード

この記事では、MVVMを実際に適用する際のいくつかの問題と解決策について説明します.
MVVM(Model View ViewModel)はMVC(Model View Control)の変形であり、MVCにおける膨大で複雑なControllerのメンテナンスが困難な問題を解決する.概ねMVVMにはいくつかの要件があります.
  • Modelsは、任意のオブジェクトに対してアクティブにコミュニケーションすることはできません(これはMVCと同じです)
  • ViewModelはModelsに対して
  • しか積極的に交流できない.
  • ViewControllerはModelsと直接交流せず、ViewModelsとViewsだけと交流
  • Viewsは、ViewControllersとのみコミュニケーションをとり、ユーザが対話するイベント(これもMVCと同様)
  • を通知する.
    MVVMとMVCには似たような特徴がたくさんあります.主な違いは次のとおりです.
  • には、View Modelクラス
  • があります.
  • ViewControllerはModels
  • に直接接触できません.
    もう一つ、MVVMのデフォルトのViewとView Controlは1対1の関係があり、一般的にはこの2つを全体と見なし、.swiftファイルとStoryboardの形式で現れます.
    View Modelの仕事は、すべての展示データを処理する論理です.1つのモデルにNSDateオブジェクトがある場合、NSDateFormatterはViewモデルで日付を設定するための表示形式に使用されます.
    View Modelはユーザーインタフェースの一部に触れることができません.View Modelファイルにはimport UIKitはありません.View ControlはView Modelがいつ新しいデータを表示するかを観察します(KVOまたはFRP(Functional Reactive Programming))
    MVVMとMVCには共通の弱点があります.ネットワークリクエスト部分をどこに置くべきかは明確に定義されていません.実際の操作の過程で、私はネットワーク要求をView Modelファイルに入れますが、その後、私はネットワーク要求を自分の独立したクラスに置くつもりで、View Modelファイルはこのオブジェクトを持っています.
    次に、MVVMの実用化における課題について説明します.
    ユーザーインタフェースの構築方法
    例えば、スクリーンの上部にsegment controlがあり、スクリーンの他の部分はcollection viewであり、異なるsegmentを選択すると、異なるスタイルのcollection view、要素の配列順序が表示されます.すべての配列スタイルを列挙するenumを定義しました.
    enum LayoutValues: Int {
        case layout0 = 0
        case layout1
        case layout2
    }
    

    では、このenumはMVVMモードではどこに置けばいいのでしょうか.このenumはデータの配列の順序を決定するため、各cellの中の文字とボタンのtitle、これらはすべて展示の論理に属して、だからこのenumはview modelの中に置くべきに見えます.
    しかし、これらのlayoutは、表示するデータを変更するのではなく、表示するデータの並び方と並び順を決定するだけであり、この観点からenumはview controllerに置くべきである.
    私の解決策はenumをview modelに入れ、view modelに対外的なObservableまたはSignalを加えて、どのlayoutを使用したかを表し、ユーザーが選択したsegmentに基づいてview modelがこの値を更新し、view controllerで対応するlayoutに基づいてcollection viewのスタイルを変更し、view controllerもこの値に基づいてどのcell reuse identifierを使用するかを決定することです.
    View Modelの構築方法
    iOS開発者がMVVMとFRPでアプリケーションを書くときに最もよくある問題は、View ModelがどのようにViewControllerにデータを表示するかかもしれません.Modelレイヤのデータが変更されて更新されると、ViewControllerは通知を得てUIの更新を行う必要があります.一般的には、2つのメカニズムが使用されます.
  • は、ViewModelでpropertyを用いる、KVOでデータの変化を観察することができる(またはFRPで信号またはシーケンスに属性をカプセル化する).信号またはシーケンスをpropertyとしてViewModelに配置し、対応するサードパーティライブラリおよびフレームワーク
  • を使用することができる.
    最初のオプションは、ViewControllerでpropertyを観察する方法を決めることができるので魅力的です.しかし、私はSwiftで最初のオプションを使用することをお勧めしません.SwiftはKVOでタイプチェックがないので、AnyObjectに対してタイプを何度も強制的に変換する必要があります.
    2つ目のオプションは、Swiftのgenericsプロパティ、signals、sequences、observablesに基づいてコンパイル中のタイプチェックをサポートするSwiftを比較する方法です.
    しかし、viewモデルでこれらのSignalsまたはObservablesを増加させることは困難である場合がある.Swiftの初期化方法は,いつpropertyに値を付与するかについて非常に明確な規定がある.SignalsまたはObservablesは、view model内部の状態を使用する必要があるため、super.init()以降に作成する必要がありますが、一方、super.init()を呼び出す前に、Signal/Observable propertyを含むすべてのpropertyが付与されていることを保証します.
    これは鶏が先にいるか卵が先にあるかの問題だ.varの暗黙的なオプションタイプとして定義すると、super.init()以降にpropertyに値を割り当てることができます.これは完璧な解決策ではない.上記の方法の代わりにlazy var propertyの閉パケット付与を用いることができる.Swiftが絶えず改善され、更新される過程で、他のより良い方法を模索することもできます.
    ユーザー・インタラクションの処理方法
    よく使われる例を挙げると、ユーザーはcollection viewのcellをクリックし、詳細ページにジャンプします.ユーザーがクリックした操作はview controllerで処理すべきで、具体的な内容は新しい詳細ページを示すことです.しかしview controllerはmodelsに直接触れることができません.MVVMモードでこのようなユーザーインタラクションを実現するにはどうすればいいのでしょうか.
    私の解決策はSwiftの閉パッケージを利用することです.まずview modelで閉パッケージを定義します.
    typealias ShowDetailsClosure = (Item) -> Void
    

    次にview modelにpropertyを追加します.
    class ViewModel {
      let showDetails: ShowDetailsClosure
      init(...
           showDetails: ShowDetailsClosure,
           ...
    }
    

    次に、閉パッケージを呼び出す必要があります.view modelでview controllerが呼び出すことができる関数を定義します.この関数のパラメータは、どのデータを使用するかを決定することができます.一般的にindex pathがよく使われます.
    func showDetailsForItemAtIndexPath(indexPath: NSIndexPath) { 
        showDetails(Items[indexPath.item])
    }
    

    ユーザーがcellを選択すると、view modelのこの関数が呼び出され、index pathパラメータが入力され、view modelはどのデータを使用するかを決定し、view controllerで定義された閉パッケージを呼び出します.たとえば、次のようにします.
    func showDetailsForItem(item: Item) {
        performSegueWithIdentifier(SegueIdentifier.ShowItemDetails.rawValue, sender: item)
    }
    

    最後の問題は、このview modelをどのように作成するかです.view modelに閉パッケージの初期化関数を渡し、lazy loadingでview modelの初期化関数を呼び出す必要があります.
    lazy var viewModel = {
        return ViewModel(..., showDetails: self.showDetailsForItem, ...)
    }