※先に断っておきますが、この記事には特定のアーキテクチャや手法について、批判したり推奨する意図は一切ありません。あくまでも個人的な意見を多く含み、試験的に行なっている手法を紹介するものです。
こんにちは、フエルマネー開発者です。
今日はViewModelを使わなくなってきた私の小話をします。
ViewModelとは
SwiftUIではよく、MVVMアーキテクチャというプログラムの構成が用いられています。MVVMとは、以下の3要素に分割する考え方ですね。
- Model(データ構成や処理全般)
- View(実際の画面構成)
- ViewModel(Viewの状態管理とModelへの伝達)
それぞれの要素を分けて開発することで、開発生産性あるいは保守性の向上が期待できます。
3要素の意味を簡単にまとめると、Viewは実際に表示される画面の配置、ViewModelはViewの状態やイベントをハンドリングする人、Modelはそれ以外の内部プログラム全般ということになります。
SwiftUIあるある
SwiftUIを学び始めると、アノテーションの話がすぐに出てきます。@State
、@Binding
などアットマークがつくやつです。
そしてこれらは、Viewで以下のように記述します。
struct ContentView: View{
@State var count: Int = 0
var body: some View{
VStack{
Text(count.description)
Button("+1する"){
count += 1
}
}
}
}
これはとても簡単な例です。ボタンを押すとcountが1増えます。
このcount変数は「状態」で、画面が表示された後も変化し続けることができます。
さて、MVVMの話に戻りましょう。MVVMの前提で行くと、これはアンチパターンです。Viewの状態は、ViewModelがハンドリングします。つまりViewは状態を持たないというのです。
MVVMにしっかりと準拠している人は、以下のように書くと思います。
struct ContentView: View{
@StateObject var model = ContentViewModel()
var body: some View{
VStack{
Text(model.count.description)
Button("+1する"){
model.count += 1
}
}
}
}
class ContentViewModel: ObservableObject{
@Published var count = 0
}
Viewごとに1:1で対応したViewModelクラスを作り、その中でViewの状態を管理します。ObservableObjectに準拠し、StateObjectで宣言することで、countが変化した時にViewが再描画されるので、Stateと同じように扱うことができます。
が、僕がこれで開発した感想は以下の通りです。
- クラスが増えるので行き来するのが面倒、そして見づらい
- 状態にアクセスするために毎回
model.
から始めなければならない - じゃあ
@State
って何のためにあるの? - ViewModelじゃないとできないことが一つも見つからない
@State
はViewの中でしか使えません。ViewModelで状態管理をするには@Published
を使うので、この時点で文字数が多いですし、ObservableObjectに準拠させて@StateObject
あるいは@ObservedObject
をつけて宣言しなければなりません。記述量が増えすぎです。
それに対し、費用対効果はどうかというと、正直ViewModelがないとできないことを僕は一つも知りません。
なのでViewだけでいいんじゃない?と。
まず@State
というものが用意されている時点で、その想定なんだと思います。
状態を計算してViewに反映する
例えばViewModelを作ると、状態管理を分けて考えられるので、以下のようなコーディングができます。
struct ContentView: View{
@StateObject var model = ContentViewModel()
var body: some View{
VStack{
Text(model.count.description)
Text("×2" + model.double.description)
Text("×3" + model.triple.description)
Button("+1する"){
model.count += 1
model.refresh()
}
}
}
}
class ContentViewModel: ObservableObject{
@Published var count = 0
@Published var double = 0
@Published var triple = 0
func refresh(){
double = count * 2
triple = count * 3
}
}
これは、countの値に応じて、2倍した値と3倍した値も同時に更新されるプログラムです。
View:ViewModelは1:1なので、Viewに表示される値ごとにViewModelも値を持たせるのが妥当かと思います。
そうなると、一つ値が更新された時に、他の値も更新(Publish)しなければViewを更新できません。この状態の更新が思うように反映されないというのは、実際に開発しているとよくぶつかる問題だと思います。
こういう時に僕は、refresh()という関数を作って、countの状態が更新されるたびに呼んでいました。
ですが、これはなんだか不自然ですよね。
なぜだかわかりますか?
それは状態ではない
実は、このViewに、doubleやtripleという状態はありません。あくまで状態はcountだけなんです。doubleやtripleは、countの値に応じて一意に決まります。
つまり、countから計算した値を直接代入するのが筋です。
struct ContentView: View{
@StateObject var model = ContentViewModel()
var body: some View{
VStack{
Text(model.count.description)
Text("×2" + (model.count * 2).description)
Text("×3" + (model.count * 3).description)
Button("+1する"){
model.count += 1
}
}
}
}
class ContentViewModel: ObservableObject{
@Published var count = 0
}
今回はcount * 2
やcount * 3
といったシンプルな計算ですが、もしこの計算が長かったらどうすれば良いでしょうか。
最もらしい方法は、ViewModelで計算することです。
struct ContentView: View{
@StateObject var model = ContentViewModel()
var body: some View{
VStack{
Text(model.count.description)
Text("×2" + model.double().description)
Text("×3" + model.triple().description)
Button("+1する"){
model.count += 1
}
}
}
}
class ContentViewModel: ObservableObject{
@Published var count = 0
func double() -> Int{
count * 2
}
func triple() -> Int{
count * 3
}
}
でもここまできたら、もはやViewModelにする意味がわかりません。
Viewにも関数を持たせることはできます。
struct ContentView: View{
@State var count = 0
var body: some View{
VStack{
Text(count.description)
Text("×2" + double().description)
Text("×3" + triple().description)
Button("+1する"){
count += 1
}
}
}
func double() -> Int{
count * 2
}
func triple() -> Int{
count * 3
}
}
なんとなく気づいたことは、Viewの中にViewModelも含んでいるということです。要するにvar body: some View{}
の中にはViewを記述して、その外に@State
やfunc
を定義してViewModelのように管理すれば良いということです。
View自体のボリュームは大きくなりますが、わざわざViewModelに分けたところで可読性は変わらないと思いますし、むしろクラスが増える方が負担になるのではないでしょうか。
まとめ
今回伝えたかったことをまとめると2つです。
一つは、ViewModelをあえてクラスで作らなくても、SwiftUIのViewにはViewModelの要素も含まれているということ。
二つ目は、変化の源泉となる状態だけを状態として管理し、それに追随して変化する値は関数から計算して直接代入するべきということ。
ViewModelがないとできないことを知っている方は是非コメント等で教えてください!