もっともシンプルな SwiftUI - MVVM


SwiftUI - MVVM が理解できます

「 MVVM って、よく聞くけど、なんかわかったようなわからないような。。」
「 流石にそろそろ SwiftUI いじっときたいけど、書く手順とかコードの配置とかややこしそう」
と思っているそこのあなた、 この記事を読めば、 SwiftUI - MVVM のデザインの骨子がわかります。

MVVM とは?

MVVM はコードデザインのパターンで、
Model と View を分離する
ことに主眼をおいています。

Model はアプリが何をするかの実質内容
View はアプリをユーザーにどのように提示するかの方法

そして、両者の変更を

ViewModel が翻訳してつたえあう

ことにより、
Model はアプリの実質内容をわかりやすく一意に保つことができ、
View は Model の反映を遅滞なくユーザーに提示できます。

MVVM は必須?

SwiftUI をつかってアプリをつくるために、 MVVM のパターンは必須ではありませんが、
MVVM をもちいることにより、
「アプリの内容変更をドバッと View にわたしてしまえば、 View がよろしく表現してくれる」宣言型の手法がとれ、スムースにアプリがかけるようです。
(ちなみに、「内容の更新ごとに View に『あれやってこれやって』と言う」のは命名型)

一番シンプルな例で理解しよう

とはいえ、
「ViewModel ってけっきょくなにを書くねん」、「 SwiftUI って @ObservedObject とか @Published とかいろいろ新キャラがでてきてこわそう」
、と感じるのが人情だと思うので(ぼくがそうなので)、
シンプルなケーススタディーで SwiftUI と MVVM の組み合わせを書いてみました。

最低限の Model View ViewModel で MVVM のパターンをつくります。

ケーススタディーは、タップすると犬 ⇄ 猫が切り替わるスイッチです。

【画像:タップすると切り替わるシンプルなスイッチ】

タップ ⇄

(わかりやすいように背景緑色にしてます)

シンプルな MVVM

Model を書く

このサンプルの Model 、つまりこのアプリケーションの実質は、犬と猫の切り替えです。

Model.swift
import Foundation // Model は SwiftUI をインポートしない

struct Model {

    enum Pet:String { // ケースは犬か猫か
        case 🐶
        case 🐱
    }

    var pet: Pet = .🐶 // 初期値は犬

    mutating func switchPet() { // 犬と猫を切り替える関数
        if pet == .🐶 {
            pet = .🐱
        } else {
            pet = .🐶
        }
    }

}

Model は SwiftUI をインポートしません。 UI から独立したアプリの実質だからです。

このアプリは犬なのか、猫なのか、の切り替えが実質なので、
Model は、犬か猫かの pet という変数と、 犬と猫を切り替える switchPet という関数でできています。
(struct が自身を変更するには mutating func をもちいます)

我々のアプリの Model はこれだけです。

View を書く

View は Model の pet を Text View にして表示します。
また、 Text View がタップされたら、 Model の pet をスイッチします。

ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        Text("ここに Model の pet を反映する")
            .padding()
            .onTapGesture {
                // ここで model の pet をスイッチする
        }
    }

}

我々の View は
ユーザーに対する Model 内容の表示と、
ユーザーのタップを受け入れる役割を持っています。

MVVM のパターンを無視すれば、
ここで Model を View で直接保持することも可能です。

ダイレクトにModelをもつContentView.swift
import SwiftUI

struct ContentView: View {
    @State var model = Model()
     // @State をつけることで、 View の状態に関する値を変更、即時反映できる

    var body: some View {
        Text(model.pet.rawValue)
            .padding()
            .onTapGesture {
                model.switchPet()
        }
    }

}

もっといえば、pet 変数と切り替え関数を View で持つことも、もちろん可能です。

ダイレクトにpetとswitchPetをもつContentView.swift
import SwiftUI

struct ContentView: View {

    enum Pet:String {
        case 🐶
        case 🐱
    }

    @State var pet: Pet = .🐶
     // @State をつけることで、 View の状態に関する値を変更、即時反映できる

    mutating func switchPet() {
        if pet == .🐶 {
            pet = .🐱
        } else {
            pet = .🐶
        }
    }

    var body: some View {
        Text(pet.rawValue)
            .padding()
            .onTapGesture {
                switchPet()
        }
    }

}

このペットという変数が View の一時的な状態をあらわすだけなら、これでいいのかもしれません。

しかしそうすると、たとえば、いくつも View があったときに Model の状態を一意に保つのが大変になったりします。
それをやらないのが MVVM です。

ViewModel を書く

ViewModel の役割は、 View と Model のコミュニケーションの通訳です。
View からユーザーのタップがあったときに Model に伝え
Model の状態を View に伝えることです。

ViewModel.swift
import Foundation
import SwiftUI

class ViewModel {
    var model:Model = Model() // Model をもつ

    var pet: String { 
        return model.pet.rawValue // Model の pet を View が必要とする String にして返す
    }

    func switchPet() {
        model.switchPet() // Model の switchPet を呼ぶ
    }
}

View から ViewModel にアクセスする

View から ViewModel にユーザーのタップを伝え、
ViewModel から Model の切り替え関数を呼び、
ViewModel から View は Model の pet の値の変更を取得します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    var viewModel = ViewModel()

    var body: some View {
        ZStack {
            Text(viewModel.pet)
                .padding()
                .onTapGesture {
                    viewModel.switchPet()
                }
        }
    }
}

これを動かしてみると、 UI が変わりません

確認のために、 Model のスイッチペットにプリントを入れてみると、

Model.swift
    mutating func switchPet() {
        if pet == .🐶 {
            pet = .🐱
        } else {
            pet = .🐶
        }
        print(pet)
    }

🐱
🐶
🐱
🐶

Model の pet は切り替わっていますが、 UI は更新されていません。
先ほどの情報遷移のフローで言うと、

View から ViewModel にユーザーのタップを伝え、(できた)
ViewModel から Model の切り替え関数を呼び、(できた)
ViewModel から View は Model の pet の値の変更を取得します。(ここが届いていない)

MVVM では、 ViewModel は全体に向けて Model の変更をパブリッシュ(公開)し、 View は自分の知りたい情報を任意に購読(subscribe)する、というかたちで Model の更新情報を取得します。

ここで、SwiftUI のプロパティ・ラッパーが登場します。

ViewModel が変更を公開し、 View が購読する

ViewModel.swift
import Foundation
import SwiftUI

class ViewModel:ObservableObject { // @ObservableObject をつける
    @Published var model:Model = Model() // @Published をつける

    var pet: String {
        return model.pet.rawValue
    }

    func switchPet() {
        model.switchPet()
    }
}

@ObservableObject (観察可能なオブジェクト)を継承することで、 ViewModel は観察対象になることができ、アプリ全体(の観察意思がある対象)に向けて情報を発信できるようになります。

@Published をつけることで、 この Model に変更があったとき即座に、 ViewModel (@ObservableObject) は全体にパブリッシュ(公開)できます。

そして、 View 側でこの変更のパブリッシュを購読します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel() // @ObservedObject をつける

    var body: some View {
        ZStack {
            Text(viewModel.pet)
                .padding()
                .onTapGesture {
                    viewModel.switchPet()
                }
        }
    }
}

@ObservedObject (観察されるオブジェクト)を viewModel 変数につけることで、 @ObservableObject である ViewModel の公開する変更があったときに、 View は即時に body var から関連する UI を変更できます。

これで、 「ViewModel から View は Model の pet の値を取得する」部分ができあがり、タップによって UI も更新されるようになりました。

【画像:タップで更新される犬猫】

これがもっともシンプルな MVVM です。
もっと色々あるんでしょうが、とりあえずのパターンの基本要素は入っていると思います。

🐣


フリーランスエンジニアです。
お仕事のご相談こちらまで
[email protected]

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium