LoginSignup
15
19

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-20

最近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ではないかと教えていただいたので、追記しておきます。

15
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
19