SwiftUIをMVVMで組んでみる

SwiftUIでアプリ開発を進めているのですが、アーキテクチャーにMVVMを採用しています。

あまり詳しいわけではないため参考になるか分かりませんが、こんな感じで組んでいますというのを紹介したいと思います(誤っている部分がありましたらご指摘いただけると幸いです)。

MVVMを知らない方もいらっしゃると思いますので、まずMVVMアーキテクチャーのおさらいから始めようと思います。

SwiftUIをMVVMで組んでみる

まずは、MVVMの意味から確認していきましょう。

MVVMは以下の頭文字から来ています。

  • M:Model
  • V:View
  • VM:ViewModel

Model

アプリケーションを開発する際に、データという概念は必ずと言っていいほど出てきます。データと一口に言っても、端末の内部データ(UserDefaultsRealmSwift等)、一般のレンタルサーバ・クラウドサーバーのデータベース(AWSGCP等)、はたまたモックデータかもしれません。

Modelはこれらのデータ元を問わず、アプリ内で使用できるデータ形式を定義したクラス(または構造体)を表します。

View

Viewは、アプリの見た目を定義する構造体です(SwiftUIではclassではなくstructで定義します)。

UIKitではUIViewController・UIView(またはそれらの子クラス)とxmlで作られたStoryBoardやXibで構成されていましたが、SwiftUIではViewプロトコルを継承したstructで作ります。全てSwift言語で作成できるのが特徴です。

MVVMアーキテクチャでは、基本的に画面要素の構成部分だけを定義し、内部的なロジックや表示データは、ViewModelやModelに処理を移譲します。

ViewModel

最後にViewModelですが、こちらはViewとModelの橋渡し的な役割を担います。Viewからの以来を処理し、必要に応じてModelデータを取得し、その結果をViewに返します(実際は返すというよりかは影響を与えるというイメージです)。

MVVMの基本原則

MVVMは以下のように参照が単一方向です。

View→ViewModel→Model

  • ViewはViewModelへの参照をもつが、その逆はない。
  • ViewModelはModelへの参照をもつが、その逆はない。
  • ViewはModelへの参照をもたず、同様にModelもViewへの参照をもたない

上記3つのいずれか1つでも違反しているとその設計はMVVMとして誤っていることになります。

以上を念頭に置いた上で、早速コード例を見ていきましょう。

SwiftUIでのMVVMコード例

Model

struct PersonModel: Identifiable {
    var id: Int
    var name: String
    var age: Int
    var sex: String
    var location: String
    var job: String
}

まずはModelです。シンプルに人物についての構造体を定義しました。Identifiableプロトコルを継承していますのでidというメンバ入れる必要があります。こうすることでViewのList定義がシンプルにすることが出来ます。Identifiableの詳細についてはこちらをどうぞ。

ViewModel

let personsMock: [PersonModel] = [
    PersonModel(id: 1, name: "佐藤浩介", age: 30, sex: "男性", location: "東京都", job: "会社員"),
    PersonModel(id: 2, name: "鈴木絵里", age: 21, sex: "女性", location: "大阪府", job: "大学生"),
    PersonModel(id: 3, name: "高橋美沙子", age: 33, sex: "女性", location: "静岡県", job: "主婦"),
    PersonModel(id: 4, name: "田中優作", age: 55, sex: "男性", location: "福岡県", job: "会社役員"),
    PersonModel(id: 5, name: "渡辺大毅", age: 17, sex: "男性", location: "埼玉県", job: "高校生"),
    PersonModel(id: 6, name: "中村真衣", age: 27, sex: "女性", location: "秋田県", job: "公務員"),
    PersonModel(id: 7, name: "山崎敏子", age: 72, sex: "女性", location: "広島県", job: "無職"),
    PersonModel(id: 8, name: "山田誠司", age: 46, sex: "男性", location: "北海道", job: "自営業")
]

final class PersonListViewModel: ObservableObject {

    @Published private(set) var persons: [PersonModel] = []
    
    @Published var isShowDetail = false
    
    @Published private(set) var message = ""
    
    func loadPersons() {
        self.persons = personsMock
    }
    
    func showDetail(person: PersonModel) {
        var msg = "【氏名】:\(person.name)\n"
        msg += "【年齢】:\(person.age)歳\n"
        msg += "【性別】:\(person.sex)\n"
        msg += "【出身】:\(person.location)\n"
        msg += "【職業】:\(person.job)"
        self.message = msg
        
        self.isShowDetail = true
    }
}

次にViewModelです。

最初にモックデータとしてPersonモデルの配列(personsMock)を用意しました。モックと言えど通常は別ファイルにまとめておいた方が良いと思います。

ViewModelでのポイントはObservableObjectプロトコル@Publishedプロパティラッパーです。

ObservableObject

observableとは「観測可能な」という意味です。つまり、ObservableObjectを継承したクラスをView側で持つことで、データの変化をView側で監視することができるようになります。

@Published

@PublishedはView側で各View要素に紐づけたい変数に付加する物です。例えば、Listに表示するデータ配列として、

@Published private(set) var persons: [PersonModel]

を宣言していますが、このデータに変更が加えられると、View側で自動的に再描画(リストの更新)を行ってくれます。

@Published var isShowDetail@Published private(set) var message

についても、View側で紐付けされていて、ViewModel内で変更が加えられるとViewに変化をもたらします。

View

struct UserListView: View {
    
    @ObservedObject var viewModel: PersonListViewModel
    
    var body: some View {
        NavigationView {
            List(self.viewModel.persons) { person in
                Button(action: {
                    self.viewModel.showDetail(person: person)
                }, label: {
                    HStack {
                        Text(person.id.description)
                        Text(person.name)
                        Spacer()
                    }
                })
                .foregroundColor(.primary)
                .alert(isPresented: self.$viewModel.isShowDetail) {
                    Alert(title: Text("人物詳細"), message: Text(self.viewModel.message), dismissButton: .default(Text("OK")))
                }
            }
        }
        .onAppear() {
            self.viewModel.loadPersons()
        }
    }
}

struct UserListView_Previews: PreviewProvider {
    static var previews: some View {
        UserListView(viewModel: .init())
    }
}

最後にViewです。

先ほどのViewModelクラスを、@ObservedObjectプロパティラッパーを付加して保持しています。これでViewがViewModelのデータを監視できるようなります。

そして、ViewModelで@Publishedで宣言した変数を以下の箇所で紐付けています。

  • List(self.viewModel.persons) { …
  • .alert(isPresented: self.$viewModel.isShowDetail) {
  • Alert(title: Text(“人物詳細”), message: Text(self.viewModel.message), …

Listに表示する人物(Person)のデータは、ViewのonAppear()のタイミングで、self.viewModel.loadPersons()メソッドをコールし読み込んでいます。

AlertisPresented:Binding<Bool>として受け取る必要があるため、接頭辞に「$」を付けなければいけません。$viewModel.isShowDetailの「$」は viewModelではなく、isShowDetailに掛かっているという点に注意です。

「$」がAlert表示のイベント発行者であると宣言しているという感覚で良いかと思います。

今回はリストの行(Button)をタップした際に、self.viewModel.showDetailメソッドをコールすると、isShowDetailmessageがセットされ、Alertが表示されるという仕組みになっています。

最後に実行画面を確認します。

以上、SwiftUIをMVVMで構築する簡単な例を示させて頂きました。

SwiftUIやMVVMの入門者の(筆者もまだまだ入門レベルですが)参考になれば幸いです。

以上

2件のコメント

記事拝見いたしました。
「ViewはModelへの参照をもたず」というのがMVVMのルールと書かれていますが、記載されているサンプルコードを見るとViewがPersonModel構造体をそのまま扱っており、Modelを参照しているように見えます。
これはMVVMに反していることにはならないのでしょうか?

hogeさん

コメントありがとうございます(お返事が遅くなりすみません)。

MVVMとしておかしいのではというご指摘についてご回答致します。
私のMVVMへの理解が間違っている可能性がございますが、
「ViewはModelへの参照をもたず」という部分については、ViewがModelの参照、つまりメンバ変数として直接持っていない( ViewModelを介している)という点でクリアしているのではという認識でした。

しかし、よりViewとModelを切り離すことを明確化するとしたらView表示用のデータ構造体(仮にViewData構造体とする)を定義し、Modelから取得したデータをViewModelでViewDataに加工してViewに渡すという形を取るとより明確なアーキテクチャになるかもしれません。

ViewModelがModelにデータリクエスト

ModelがViewModelにデータを返す

ViewModelがModelから貰ったデータを元にViewData作成

ViewはViewDataを参照し表示する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です