Open2
SwiftUI: @AppStorageをもう少し理解する
データの保存をするのに、iOS 14から導入された@AppStorage
プロパティラッパーが便利と聞きました。
これを使って多少複雑なデータを保存してもいいのかな?というのを試す前に、そもそもどういう動きをしているのかWebで調査しました。
参考にしたのは以下の記事です。
Viewとそれ以外で動作が異なる、というので、実際何が起きてそうなのか、もう少し確認してみました。
サンプルコード
struct AppStorageView: View {
@AppStorage("Count") var count = 0
var body: some View {
let text = "AppSorageView \(count)"
Button(text){
count += 1
}.buttonStyle(.bordered)
}
}
struct ObservedObjectView: View {
@ObservedObject var countModel: CountModel
var body: some View {
let text = "ObservedObjectView \(countModel.count)"
Button(text){
countModel.count += 1
}.buttonStyle(.bordered)
}
}
struct ObservableObjectView: View {
var countModel: CountModel = CountModel(timeInterval: 2)
var body: some View {
let text = "ObservableObjectView \(countModel.count)"
Button(text){
countModel.count += 1
}.buttonStyle(.bordered)
}
}
struct NonObservableObjectView: View {
var countModel: CountModelNonObservable = CountModelNonObservable(timeInterval: 5)
var body: some View {
let text = "NonObservableObjectView \(countModel.count)"
Button(text){
countModel.count += 1
}.buttonStyle(.bordered)
}
}
struct CountView: View {
var observedCountModel: CountModel
var body: some View {
VStack {
AppStorageView()
ObservedObjectView(countModel: observedCountModel)
ObservableObjectView()
NonObservableObjectView()
Button("UserDefaults"){
UserDefaults.standard.set(0, forKey: "Count")
}.buttonStyle(.bordered)
}
}
}
class CountModel: ObservableObject{
@AppStorage("Count") var count: Int = 0
var timer: Timer
init(timeInterval: TimeInterval) {
print("CountModel init: \(Date.now)")
self.timer = Timer()
self.timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { _ in
self.count += 1
print("CountModel(\(timeInterval)) count up -> \(self.count)")
}
}
}
class CountModelNonObservable {
@AppStorage("Count") var count = 0
var timer: Timer
init(timeInterval: TimeInterval) {
print("CountModelNonObservable init: \(Date.now)")
self.timer = Timer()
self.timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { _ in
self.count += 1
print("CountModelNonObservable count up -> \(self.count)")
}
}
}
struct ContentView: View {
@StateObject var countModel = CountModel(timeInterval: 3)
init() {
print("init: \(Date.now)")
}
var body: some View {
VStack {
let _ = print("ContentView.body \(Date.now)")
CountView(observedCountModel: countModel)
}
}
}
もと記事と同様、同じ@AppStorage
変数を読み書きします。
タイマーで値を自動更新するようにしました。
結論的なもの
確認した感じでは、以下のように推測しました。
言えること | 推測理由 |
---|---|
@Publish と同様、@AppStorage 変数が更新されるとそれを監視しているView に通知が行って再描画される |
observedCountModel の値が更新されるとObservedObjectView に更新がかかるため |
更新された@AppStorage 変数と同じ@AppStorage 変数を見ているView には通知が行って再描画される |
どのオブジェクトのタイマーも、更新された値がAppStorageView に反映されるため |
@AppStorage 変数があるオブジェクトでも、View 側に@ObservedObject としてマークされてなければView には通知が来ない |
ObservableObjectView には更新がかからないため |
UserDefaults からの読み込みはイニシャライザでのみ行われ、その後の変数の更新は最初に読み込んだ変数値をもとにして行われる(途中で再読込されない) |
タイマーで更新されている変数の値が、それぞれのオブジェクトで異なっているため |
View の@AppStorage 変数が常に最新に見えるのは、単に通知を受け取ったView が毎回イニシャライズされているせい。@AppStorage 変数もイニシャライザで再読込されている |
init のログから判断 |
UserDefaults への書き込み自体は、@AppStorage 変数に更新がかかる都度、毎回行われている |
AppStorageView の値がいったりきたりすることから、最後に更新された値で毎回書き込まれていることがわかる |
簡単に言うと
確かに「View
以外で@AppStorage
を使う場合は注意」なんですけども、どちらかというと「@AppStorage
を読むのはどこで読んでもいいけども、更新は1箇所で行え」という感じかなと思います。
割と普通の話ですね。
よくよく元記事のコードの動きを見ると
View以外にAppStorageを使用した場合、そのインスタンスで一度書き込みを行うまでは即座に反映されます。
一度書き込みを行った後はキャッシュされるようで、他で書き込まれても反映されません。
この動きになっています。単純にイニシャライザで読み込んでいるだけ、というわけでもなさそうです。
-
set
されるまでは、get
のたびに毎回ストレージまで取りにいく - いったん
set
されるとその時点で変数とストレージとを切り離して、それ以降のget
は自分の値を返す
というような感じでしょうか。
これ知らないとヤバい動きですね。