MVVM
Swift

MVVMを2パターン考えてみる(追記:MVPでしたすみません)

最近MVVMの記事がよく目につくような気がしたので、まとめておこうかなという記事です。

MVVMとは

Model-View-ViewModelのことで、デザインアーキテクチャーの一つです。
最近では結構な人が名前くらいは、聞いたことあるのではないかと思います。

各役割について説明して行こうかと思います。
今回はrxを使用しないため、data bindingをしないので、少し違うかもしれませんが、ご了承ください。

Model

主な役割は

  • データの管理

です。

お前らがModelと呼ぶアレをなんと呼ぶべきか。近辺の用語(EntityとかVOとかDTOとか)について整理しつつ考えるでも言われてる通り、定義が曖昧なところがあるので、Modelも2つのパターンがあるのかなと考えたので、紹介して行こうかなと思います。(間違いがあったりしましたらご指摘よろしくお願いします)

View

主な役割は

  • 表示

です。
iOSでは、UIViewControllerクラスもViewに入れられたりします。
そのため、ユーザーからのアクション(ボタンをタップするなど)を受け取るのはこちらになります。
ViewModelから来たデータを表示する・ユーザーからのアクションをViewModelに伝えます。

ViewModel

主な役割は

  • データの操作

です。
データの取得のために通信クラスのメソッドを呼んだり、通信で受け取ったデータをModelに渡したり、Modelから来たデータをViewに渡したりします。

MVVM実装

自分が考える2つのパターンでの実装を紹介しようと思います。

Model_Entity_Pattern

Modelがデータを管理するパターンです。
Entityは、データ構造体を表します。
Modelは、Entityを管理するようにします。

MVVMER-2.jpg

Entity

Modelが管理するデータ構造体に変更があった時は、ViewModelへ伝えます。
データの構造体として、名前と年齢を持つEntityを作り、それをModelで管理します。

Entity.swift
struct Entity {
    let name: String
    let age: Int
}

Model

Modelが管理するデータに変更があった時に、ViewModelへ伝えるためにprotocolを実装します。

Model.swift
protocol ModelToViewModel: class {
    func onChangeEntities(entities: [Entity])
}

class Model {

    var entityies: [Entity] = [] {
        didSet {
            viewModel?.onChangeEntities(entities: entities)
        }
    }

    weak var viewModel: ModelToViewModel?

}

Repository

通信などを行うクラスとしてRepositoryを作成します。
今回は、仮でデータを作成してますが、ご了承ください。

Repository.swift
protocol RepositoryToViewModel: class {
    func onGetEntities(entities: [Entity])
}

class Repository {
    weak var viewModel: RepositoryToViewModel?

    func getEntities() {
        // 通信などで取得する
        // 今回は仮で作成
        var entities = [Entity]()
        entities.append(Entity(name: "tarou", age: 25))
        entities.append(Entity(name: "zirou", age: 20))
        entities.append(Entity(name: "saburou", age: 18))
        viewModel?.onGetEntities(entities: entities)
    }
}

ViewModel

ViewModelはModelとRepositoryを持ちます。
ModelとRepositoryからのdelegateもViewModelに実装します。
変更があったときに、Viewに伝えるためにprotocolを実装しておきます。

ViewModel.swift
protocol ViewModelToView: class {
    func onChangeEntities()
}

class ViewModel {

    let model: Model = Model()
    let repository: Repository = Repository()

    weak var view: ViewModelToView?

    init() {
        model.viewModel = self
        repository.viewModel = self
    }

    func getEntities() {
        repository.getEntities()
    }

}

extension ViewModel: ModelToViewModel {

    // ModelからのDelegate
    func onChangeEntities(entities: [Entity]) {
        view?.onChangeEntities()
    }

}

extension ViewModel: RepositoryToViewModel {

    // RepositoryからのDelegate
    func onGetEntities(entities: [Entity]) {
        model.entities = entities
    }

}

View

Viewは、UIViewControllerクラスを使用しました。
ViewModelクラスを持ちます。
Entityのnameを表示するためにMain.StoryBoardでUITableViewを実装しました。

MVVMViewController.swift
class MVVMViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    let viewModel: ViewModel = ViewModel()

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

}

extension MVVMViewController: UITableViewDelegate, UITableViewDataSource {

    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.model.entities.count
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "MVVMCell", for: indexPath)
        cell.textLabel?.text = viewModel.model.entities[indexPath.row].name
        return cell
    }

}

extension MVVMViewController: ViewModelToView {

    // ViewModelからのdelegate
    func onChangeEntities() {
        tableView.reloadData()
    }

}

思ったこと

Viewで、データを参照する時に、viewModel.model.entitiesとなり、長くなってしまいました。

Model_Pattern

ViewModelがデータを管理するパターンです。
Modelは、データ構造体を表します。
ViewModelが管理するデータ構造体に変更があった時は、Viewへ伝えます。

MVVMER.jpg

コードでしていることは先ほどと同じです。実装をしているクラスが変わっているだけです。

Model

Model.swift
struct Model {
    let name: String
    let age: Int
}

Repository

Repository.swift
protocol RepositoryToViewModel: class {
    func onGetModels(models: [Model])
}

class Repository {
    weak var viewModel: RepositoryToViewModel?

    func getModels() {
        // 通信などで取得する
        // 今回は仮で作成
        var models = [Model]()
        models.append(Model(name: "tarou", age: 25))
        models.append(Model(name: "zirou", age: 20))
        models.append(Model(name: "saburou", age: 18))
        viewModel?.onGetModels(models: models)
    }
}

ViewModel

ViewModelがデータの管理を行います。

ViewModel.swift
protocol ViewModelToView : class {
    func onChangeModels()
}

class ViewModel {
    var models: [Model] = [] {
        didSet {
            view?.onChangeModels()
        }
    }

    let repository: Repository = Repository()

    weak var view: ViewModelToView?

    init() {
        repository.viewModel = self
    }

    func getModels() {
        repository.getModels()
    }

}

extension ViewModel: RepositoryToViewModel {

    // RepositoryからのDelegate
    func onGetModels(models: [Model]) {
        self.models = models
    }

}

View

MVVMViewController.swift
class MVVMViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    let viewModel: ViewModel = ViewModel()

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

}

extension MVVMViewController: UITableViewDelegate, UITableViewDataSource {

    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.models.count
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "MVVMCell", for: indexPath)
        cell.textLabel?.text = viewModel.models[indexPath.row].name
        return cell
    }

}

extension MVVMViewController: ViewModelToView {

    // ViewModelからのdelegate
    func onChangeModels() {
        tableView.reloadData()
    }

}

思ったこと

こちらの方がスッキリかけている気がする。(特にデータの参照viewModel.models

さいごに

MVVMについてまとめてる途中で2パターン行けると思い書いてみましたが、Model_Entity_Patternは微妙だったかなと思いました(笑)
データ参照が長くて毎回引き出すのはつらいなと...

MVVMViewController.swift
extension MVVMViewController: ViewModelToView {

    // ViewModelからのdelegate
    func onChangeModels() {
        tableView.reloadData()
    }

}

onChangeModels(models: [Model])のように与えて使用できたらよかったのですが、UITableViewDataSourceがありしかたなくデータの参照を書くことになりました。
今度はRxSwiftを使ってもっと簡単に書いていきたいですね(笑)

参考

https://qiita.com/mishimay/items/229913a1a0e980f45acc
https://qiita.com/takahirom/items/597c48ece57b4623cdee
https://qiita.com/marty-suzuki/items/5a4f680b10bb82501aa3
https://academy.realm.io/jp/posts/slug-max-alexander-mvvm-rxswift/

追記

ViewModelからデータを取得するときに、viewModel.models.countと直接触るのではなくて、ViewModelにデータ取得メソッドをおいた方がよかったですね。
Model_Patternでの修正してみました。
修正点

  • modelsをprivate変数にした
  • modelの個数を取得するメソッドの追加(getModelCount
  • modelのnameを取得するメソッドの追加(getName()
ViewModel.swift
class ViewModel {
    private var models: [Model] = [] {
        didSet {
            view?.onChangeModels()
        }
    }

    /* 省略 */    

    func getModelCount() -> Int {
        return models.count
    }

    func getName(index: Int) -> String {
        return models[index].name
    }
}
MVVMViewController.swift
extension MVVMViewController: UITableViewDelegate, UITableViewDataSource {

    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.getModelCount()
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "MVVMCell", for: indexPath)
        cell.textLabel?.text = viewModel.getName(index: indexPath.row)
        return cell
    }

}

追記_2

ViewModelがViewへの参照を持ってると、MVVMではなくてMVPではないかと教えていただいたので、追記しておきます。