30
23

More than 1 year has passed since last update.

ViewModel終わった説(SwiftUI)

Last updated at Posted at 2023-08-14

※先に断っておきますが、この記事には特定のアーキテクチャや手法について、批判したり推奨する意図は一切ありません。あくまでも個人的な意見を多く含み、試験的に行なっている手法を紹介するものです。

こんにちは、フエルマネー開発者です。
今日はViewModelを使わなくなってきた私の小話をします。

ViewModelとは

SwiftUIではよく、MVVMアーキテクチャというプログラムの構成が用いられています。MVVMとは、以下の3要素に分割する考え方ですね。

  • Model(データ構成や処理全般)
  • View(実際の画面構成)
  • ViewModel(Viewの状態管理とModelへの伝達)

それぞれの要素を分けて開発することで、開発生産性あるいは保守性の向上が期待できます。

3要素の意味を簡単にまとめると、Viewは実際に表示される画面の配置、ViewModelはViewの状態やイベントをハンドリングする人、Modelはそれ以外の内部プログラム全般ということになります。

SwiftUIあるある

SwiftUIを学び始めると、アノテーションの話がすぐに出てきます。@State@Bindingなどアットマークがつくやつです。

そしてこれらは、Viewで以下のように記述します。

ContentView.swift
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にしっかりと準拠している人は、以下のように書くと思います。

ContentView.swift
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を作ると、状態管理を分けて考えられるので、以下のようなコーディングができます。

ContentView.swift
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から計算した値を直接代入するのが筋です。

ContentView.swift
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 * 2count * 3といったシンプルな計算ですが、もしこの計算が長かったらどうすれば良いでしょうか。

最もらしい方法は、ViewModelで計算することです。

ContentView.swift
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にも関数を持たせることはできます。

ContentView.swift
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を記述して、その外に@Statefuncを定義してViewModelのように管理すれば良いということです。

View自体のボリュームは大きくなりますが、わざわざViewModelに分けたところで可読性は変わらないと思いますし、むしろクラスが増える方が負担になるのではないでしょうか。

まとめ

今回伝えたかったことをまとめると2つです。

一つは、ViewModelをあえてクラスで作らなくても、SwiftUIのViewにはViewModelの要素も含まれているということ。

二つ目は、変化の源泉となる状態だけを状態として管理し、それに追随して変化する値は関数から計算して直接代入するべきということ。

ViewModelがないとできないことを知っている方は是非コメント等で教えてください!

30
23
5

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
30
23