📚

【iOS】MVVMについて

2021/10/03に公開

MVVMについて一からいろいろ調べて自分なりのあるべき姿が
定まってきたので記事としてまとめます。
RxSwiftの要素はできるだけ排除して、純粋にMVVMについての考察をまとめました。
※ただし実装にはRxSwiftの利用を前提としています。→今後はCombineを使うのが一般的になるかなと思います。

前提

この記事では以下の考え方で話を進めます。

  1. ビジネスロジックは Model で実装する
  2. データバインディングには RxSwift を利用する
  3. 可能な限り1クラス1責務にする

1.について
 MVVMに関する記事を見ているとビジネスロジックをViewModelで実装するか、Modelで実装するか
の2パターンに分かれています。ViewModelで状態を管理しているからビジネスロジックもViewModel
で実装した方が分かりやすい、という意見はよく分かるのですがViewModelの元々の役割を考えると
Modelで実装した方がいいのではと思いました。

2.について
 多くのケースでRxSwiftを採用していることに倣い、RxSwiftを採用します。
 RxSwift便利!!

3.について
 再利用性を考えると1クラス1責務となるのが理想的だと考えています。

MVVMの構成要素

Model

  • ビジネスロジック(通信処理やDB操作など)
  • データ型の定義

View

ViewModelのデータを、データバインディングで自動的に描画する

ViewModel

  • ViewとModel間の伝達
  • Viewのための状態保持

MVVMの処理の流れ

下図のようなイメージです。

MVVMイメージ.png

上段の左から右への処理は
ViewはViewModelを参照し、ViewModelはModelを参照することで進んでいき
下段の右から左への処理は
ModelからViewModelに通知し、ViewModelからViewに通知することで進みます
(なんでViewModel→Viewの場合だけを指してDataBindingって言んだろ。)

ViewModelの構成要素

ViewModelの役割やどう実装するか非常に悩みましたが、にわかながら現状以下のように自分の中でまとまってます。

VMの構成要素.png

皆さんはViewModelをどのように実装されているのでしょうか。
是非コメントお願いします!

検索バーに入力した文字列でGitHubのレポジトリを検索して一覧表示するサンプルを作ってみました。
ViewModelはこんな感じに実装しました。

ViewModel.swift

import Foundation
import RxSwift
import RxCocoa

final class RepositoryViewModel {
    
    struct Input {
        // 監視対象(トリガー)
        var repositoryName: Observable<String>
    }
    
    struct Output {
        var rx_repositories: Driver<[RepositoryInfo]>
    }
    
    // MARK: - Properties
    
    // Viewから受け取るトリガー
    private var _input: RepositoryViewModel.Input!
    // Viewにデータをバインドする
    private var _output: RepositoryViewModel.Output!
    // GitHub APIからデータを取得するModel
    private let repositoryModel = RepositoryModel()
    
    // 初期化
    init(trigger: RepositoryViewModel.Input) {
        _input = trigger
        _output = RepositoryViewModel
            .Output.init(rx_repositories: createOutput())
    }
    
    func output() -> RepositoryViewModel.Output {
        return _output
    }
}

extension RepositoryViewModel {
    
    // InputからModel経由でOutputを生成する
    private func createOutput() -> Driver<[RepositoryInfo]> {
        
        return repositoryModel.rx_get(from: _input.repositoryName)
    }
}

プロジェクトにおけるM-V-VMの参照関係

前提の3.「可能な限り1クラス1責務にする」を考えると以下のような関係が良いのではないかと考えます。

M-V-VMの参照関係.png

View : ViewModel = 1 : 1 (間接的に 1 : n はあり得る)
ViewModel : Model = 1 : 1
Model : Model = n : n

まとめ

ここ2、3日で調べたことを基に考えたことをまとめてみました。
まだ書き足りない所はあるけれど、そこは随時更新していきます。
コードも準備できたらリンク貼りますので、良かったら見てやってください。

まだまだ不十分な部分もあると思いますので、ご指摘いただけますと非常に助かります。

以上です。

Discussion