#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 、つまりこのアプリケーションの実質は、犬と猫の切り替えです。
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 をスイッチします。
import SwiftUI
struct ContentView: View {
var body: some View {
Text("ここに Model の pet を反映する")
.padding()
.onTapGesture {
// ここで model の pet をスイッチする
}
}
}
我々の View は
ユーザーに対する Model 内容の表示と、
ユーザーのタップを受け入れる役割を持っています。
MVVM のパターンを無視すれば、
ここで Model を View で直接保持することも可能です。
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 で持つことも、もちろん可能です。
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 に伝えることです。
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 の値の変更を取得します。
import SwiftUI
struct ContentView: View {
var viewModel = ViewModel()
var body: some View {
ZStack {
Text(viewModel.pet)
.padding()
.onTapGesture {
viewModel.switchPet()
}
}
}
}
これを動かしてみると、 UI が変わりません。
確認のために、 Model のスイッチペットにプリントを入れてみると、
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 が購読する
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 側でこの変更のパブリッシュを購読します。
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 です。
もっと色々あるんでしょうが、とりあえずのパターンの基本要素は入っていると思います。
🐣
フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.com
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。