Sponsor Link
@AppStorage
SwiftUI 2.0 から導入された property wrapper です。動作としては、値を UserDefaults に自動で保存し、自動で読み出してくれます。
これまでは、自分で、アプリケーションの起動時に UserDefaults から読み出して、終了時に 保存していましたが、その部分を行ってくれるものです。
AppStorageのシンプルな使い方
通常の作り方では、カウンターの値は、起動ごとに初期化されてしまいますが、AppStorage を指定しておくことで、記憶してくれるようになります。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2020/11/22
// © 2020 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
// (1)
@AppStorage("countKey") var count:Int = 0
var body: some View {
Text(String(count))
.font(.largeTitle)
HStack {
Button(action: {
count = count - 1
}, label: {
Image(systemName: "minus.circle")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
})
Button(action: {
count = count + 1
}, label: {
Image(systemName: "plus.circle")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
})
}
}
}
- @AppStorage 指定することで、count の値を UserDefaults に キー値 countKey で保存されます
上記コードは、通常(?)のカウンターアプリの変数宣言に @AppStorage をつけているだけです。
それだけで、値がアプリのライフサイクルを超えて保存されるようになるので、非常に使い勝手が良いです。
しかし、この @AppStorage を MVVM に組み入れて使おうとすると少し手間になり始めます。
MVVM の カウンターアプリ
先ほどのカウンターアプリを、MVVM 実装にしました。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2020/11/20
// © 2020 SmallDeskSoftware
//
import SwiftUI
struct CountModel {
var count:Int = 0
}
class CountViewModel:ObservableObject {
@Published var countModel: CountModel
init(model: CountModel) {
self.countModel = model
}
var countAsString : String{
return String(countModel.count)
}
func increment() {
self.countModel.count = self.countModel.count + 1
}
func decrement() {
self.countModel.count = self.countModel.count - 1
}
}
struct ContentView: View {
@ObservedObject var countViewModel: CountViewModel
var body: some View {
VStack {
Text(countViewModel.countAsString)
.font(.largeTitle)
HStack {
Button(action: {
countViewModel.decrement()
}, label: {
Image(systemName: "minus.circle")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
})
Button(action: {
countViewModel.increment()
}, label: {
Image(systemName: "plus.circle")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
})
}
}
}
}
MVVM の M(Model) に組み入れる
Model に組み入れようとすると、CountModel が以下のようなコードになります。
// with AppStorage
struct CountModel {
@AppStorage("countKey") var count:Int = 0
}
コンパイルは通りますが、動かしてみると、数値の変更で画面が更新されないことに気づきます。
理由としては、CountModel のさらに奥の count が変更されているために、CountViewModel がその変更を検知できないからです。
しかし、Model を ObservableObject に準拠させるのは、ViewModel の中に さらに ViewModel を作ることになってしまいます。
Model は、View や ViewModel からは独立しているべきですので、ViewModel で変更を検知して画面更新を依頼する必要があります。
ということで、ViewModel は、以下のようになります。
class CountViewModel:ObservableObject {
// (1)
@Published var countModel: CountModel
init(model: CountModel) {
self.countModel = model
}
var countAsString : String{
return String(countModel.count)
}
func increment() {
self.countModel.count = self.countModel.count + 1
// (2)
self.objectWillChange.send()
}
func decrement() {
self.countModel.count = self.countModel.count - 1
// (3)
self.objectWillChange.send()
}
}
- CountModel に AppStorage 指定された変数が定義されています
- CoutModel 内の AppStorage 指定された変数を変更した場合に、ViewModel から変更を通知する必要があります。
- 同様に、ViewModel から変更を通知しています。
MVVM の責務の分担という意味では、ViewModel が変更を検知して通知、Model は、データの保存 と分かれてはいますが、
AppStorage 導入前と見比べると、複雑化してしまっているように見えます。
MVVM の VM に組み入れる
では、AppStorage 対象の変数を直接 ViewModel に入れてしまうのはどうでしょうか?
AppStorage を Model とみなすことになります。実は、このように実装しても、AppStorage 対象の変数変更を検知することはできません。ですので、ViewModel から 変更の通知 を行う必要があります。
ViewModel のコードは以下となります。
class CountViewModel:ObservableObject {
@AppStorage("countKey") var count:Int = 0
var countAsString : String{
return String(count)
}
func increment() {
count = count + 1
self.objectWillChange.send()
}
func decrement() {
count = count - 1
self.objectWillChange.send()
}
}
AppStorage に @Publised をつけることはできないので、Observed 経由で変更を検知することもできません。
いずれにしても、@AppStorage 指定された変数の検知は、自前で行う必要があることがわかります。
まとめ:@AppStorage を MVVM で使う時の注意点
- View が直接保持している @AppStorage は 変更検知できる
- ViewModel 経由で保持している @AppStorage は、変更検知されないので、自前で通知する
現時点では、AppStorage の対応しているタイプは、Bool, Int, Double, String, URL, Data です。
配列等を保存する時には、Data に変換して保存する必要があります。
そう考えると、MVVM で構築しているアプリでは、@AppStorage を使わずに、直接 UserDefaults を使った方が良いように思えます。
Apple の このドキュメント をよく読むと書いてあります。
説明は以上です。
不明な点やおかしな点ありましたら、Twitter でご連絡いただけるとありがたいです。
Sponsor Link