SwiftUIでアプリ開発を進めているのですが、アーキテクチャーにMVVMを採用しています。
あまり詳しいわけではないため参考になるか分かりませんが、こんな感じで組んでいますというのを紹介したいと思います(誤っている部分がありましたらご指摘いただけると幸いです)。
MVVMを知らない方もいらっしゃると思いますので、まずMVVMアーキテクチャーのおさらいから始めようと思います。
SwiftUIをMVVMで組んでみる
まずは、MVVMの意味から確認していきましょう。
MVVMは以下の頭文字から来ています。
- M:Model
- V:View
- VM:ViewModel
Model
アプリケーションを開発する際に、データという概念は必ずと言っていいほど出てきます。データと一口に言っても、端末の内部データ(UserDefaultsやRealmSwift等)、一般のレンタルサーバ・クラウドサーバーのデータベース(AWS、GCP等)、はたまたモックデータかもしれません。
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()メソッドをコールし読み込んでいます。
Alertの isPresented: はBinding<Bool>として受け取る必要があるため、接頭辞に「$」を付けなければいけません。$viewModel.isShowDetailの「$」は viewModelではなく、isShowDetailに掛かっているという点に注意です。
「$」がAlert表示のイベント発行者であると宣言しているという感覚で良いかと思います。
今回はリストの行(Button)をタップした際に、self.viewModel.showDetailメソッドをコールすると、isShowDetailとmessageがセットされ、Alertが表示されるという仕組みになっています。
最後に実行画面を確認します。
以上、SwiftUIをMVVMで構築する簡単な例を示させて頂きました。
SwiftUIやMVVMの入門者の(筆者もまだまだ入門レベルですが)参考になれば幸いです。
以上
記事拝見いたしました。
「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を参照し表示する
[…] SwiftUi MVVM 入門 【SwiftUI】SwiftUIをMVVMフレームワークで実装しよう SwiftUIをMVVMで組んでみる DPI設定もう一度確認する […]