最近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を管理するようにします。
Entity
Modelが管理するデータ構造体に変更があった時は、ViewModelへ伝えます。
データの構造体として、名前と年齢を持つEntityを作り、それをModelで管理します。
struct Entity {
let name: String
let age: Int
}
Model
Modelが管理するデータに変更があった時に、ViewModelへ伝えるためにprotocolを実装します。
protocol ModelToViewModel: class {
func onChangeEntities(entities: [Entity])
}
class Model {
var entityies: [Entity] = [] {
didSet {
viewModel?.onChangeEntities(entities: entities)
}
}
weak var viewModel: ModelToViewModel?
}
Repository
通信などを行うクラスとしてRepositoryを作成します。
今回は、仮でデータを作成してますが、ご了承ください。
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を実装しておきます。
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を実装しました。
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へ伝えます。
コードでしていることは先ほどと同じです。実装をしているクラスが変わっているだけです。
Model
struct Model {
let name: String
let age: Int
}
Repository
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がデータの管理を行います。
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
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は微妙だったかなと思いました(笑)
データ参照が長くて毎回引き出すのはつらいなと...
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()
)
class ViewModel {
private var models: [Model] = [] {
didSet {
view?.onChangeModels()
}
}
/* 省略 */
func getModelCount() -> Int {
return models.count
}
func getName(index: Int) -> String {
return models[index].name
}
}
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ではないかと教えていただいたので、追記しておきます。