MVCは何
MVCというアーキテクチャーは皆さんに熟知されると思います。MVC具体的にはModel、View、Controller三つの要素から構成されます。簡単に言うと、Modelはデーター層、Viewが表示層、ControllerがUIからの入力を担当します。
iOS開発ではViewControllerという存在がありまして、ViewControllerではViewとController両方の役割が持っています。それで、ViewControllerが複雑になったら、ViewController中でのロジックがどんどん増えて、FatViewControllerになります。FatViewControllerを改修することはかなり大変なことです。MVVMはその問題を解決する方法の一つ。他には色々な方法があるけど、今仕事で使ってるのはMVVMので、詳しく説明します。
MVVMは何
MVVMというアーキテクチャーはModel、View、ViewModel三つの要素で構成されます。Modelはデーター層、Viewが表示層、ViewModelがUIからの入力を受けて、ビューの更新を担当します。MVCのControllerと違うのはViewModelでViewの更新はデータバインディングの手法で実現します。iOS開発では、ViewControllerに対して、ViewModelを作って、うまくデータバインディングしたら、もともとFatViewControllerでデータを処理するロジックを簡単にViewModelに移行することができます。そうしたら、ViewControllerがMVCでのVになれます。
開発手法
実装を書く前に、いくつのルールを説明します。
- 一つのViewControllerは一つのViewModelのインスタンスしか持ってません
- ViewがViewModelのインスタンスを持ってないけど、プロトコルを利用してデーターバインディングします
- 実際の実装ではModelがViewModelに含まれるケースもあります
早速実装手法に関して説明します。データーバインディングはReactiveSwiftというライブラリを使ってやります。
class ViewModel {
//データーバインディング必要な変数
let paramA = MutableProperty<Int>(0)
init(data: [AnyHashable: Any]) {
if let a = data["paramA"] as? Int {
paramA.swap(a)
}
}
}
class ViewController: UIViewController {
private let viewModel: ViewModel
init(data: [AnyHashable: Any]) {
viewModel = ViewModel(data: data)
}
override func viewDidLoad() {
super.viewDidLoad()
bind(viewModel)
}
private func bind(_ viewModel: ViewModel) {
//データーバインディング
viewModel.paramA.take(during: reactive.lifetime).observeValue { (value) in
//paramAが変わったら、この変数を使ってるViewが自動的に更新されます。
}
}
}
ViewController初期化する時のデーターを利用してViewModelを初期化します。その後ViewDidLoadでViewModelとViewControllerをバインディングします。
ViewModelのparamAの数値が変わったら、ViewControllerでは自動的にその変化を反映します。
次は子Viewを追加して
protocol AViewModel {
var paramB: MutableProperty<String> { get }
}
class AView: UIView {
func bind(_ viewModel: AViewModel) {
viewModel.paramB.take(during: reactive.lifetime).observeValue{ (value) in
//paramBを使って、AViewを更新します。
}
}
}
まずAViewというViewを定義して、そしてProtocol型のViewModelを声明します。ここのViewModelがクラス型ではなくProtocol型で声明する理由は後で説明します。続いてはViewModelの修正です。
class ViewModel: AViewModel {
let paramA = MutableProperty<Int>(0)
let paramB = MutableProperty<String>("")
init(data: [AnyHashable: Any]) {
if let a = data["paramA"] as? Int {
paramA.swap(a)
}
if let b = data["paramB"] as? String {
paramA.swap(b)
}
}
}
ViewModelをAViewModelのプロトコルを実装して、ViewControllerにAView型の子Viewを追加して、同時にbind関数を修正します。
class ViewController: UIViewController {
private let viewModel: ViewModel
let aView = AView()
init(data: [AnyHashable: Any]) {
viewModel = ViewModel(data: data)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubView(aView)
bind(viewModel)
}
private func bind(_ viewModel: ViewModel) {
//子Viewのデーターバインディング
aView.bind(viewModel)
//データーバインディング
viewModel.paramA.take(during: reactive.lifetime).observeValue { (value) in
//paramAが変わったら、この変数を使ってるViewを更新します。
}
}
}
こうすれば、子ViewとViewModelをデーターバインディングができます。なぜ子ViewのViewModelがクラス型ではなくプロトコル型ですか?このような実装は必ず正しいとは言えませんが、そうする三つの理由を説明します。
-
一つのViewControllerに対してデーター処理のロジックはできるだけ一箇所にまとめます
もし子ViewのViewModelがデーターを処理するロジックを持っていれば、子Viewと子Viewの間にデーターを更新したい時、その実装はちょっと面倒になります。ロジックが一箇所にまとめたら簡単にできます。でもFatViewModel問題も発生かもしれない。 -
子ViewのViewModelがProtocol型にしたら、再利用は簡単です
再利用の場合、この子Viewを持ってるViewControllerのViewModelを子ViewModelを実装すれば完了。Interface向けの考え方はOOP、SwiftではPOP(Protocol Oriented Programing)でもおすすめられます。 -
メモリー管理がわかりやすくなる
データーバインディンに対して、うまく書かないとメモリーリークの恐れがあります*(ViewModelとかViewなど生きたままViewControllerも釈放できません)。一つのViewModelになると、ViewModelのデーターに対して観察の動作をちゃんと終われば、ViewModelを釈放できます。
初めてのqiita投稿、m(..)mよろしくお願いします。