📖

[SwiftUI]UserDefaultsを使用せず@AppStorageでデータを永続的に保存する方法と注意点

2022/02/28に公開

はじめに

この記事では@AppStorageを使用してUserDefaultsのようにデータを永続的に保存する方法とその注意点についてわかった事、試してみた事を紹介したいと思います。

@AppStorage

@AppStorageは値を自動的にUserDefaultsに保存してくれるproperty wrapperです。
公式のドキュメント
UserDefaultsより非常にシンプルに書けるますが、iOS14〜とまだ新しく、少し注意しないといけない点があると感じました。

環境

・ macOS: Monterey
・ Xcode: 13.2
・ iOS: iOS 15.2

基本的な使い方

使用方法は以下の通りです。コードが1行のみと非常にシンプルです。

@AppStorage("testKey") var testAppStorage = ""

@AppStorageの後に書いてある("testKey")がKeyになるのでここが同じならプロパティ名は違っても同じKey名内にデータが上書き保存されていきます。
また以下はTextFieldに文字を書いて、Textに表示させるシンプルなViewです。
下記ののコードを実行するとTextFieldに文字を書いて、Textに表示させるシンプルなViewが表示されます。また書いた文字が永続的に保存されていることを確認できます。

@AppStorage("testKey") var testAppStorage = ""
var body: some View {
    VStack {
        TextField("input text", text: $testAppStorage)
        Text(testAppStorage)
        Button {
            testAppStorage.removeAll()
        } label: {
            Text("削除")
        }
    }
}

削除には.removeAll()を使用しています。

ObservableObjectに準拠させたclassでデータを保持する

View外のclassで@AppStorageを使用してもclassをインスタンス化しても使用することができません。

//class
class TestAppStrageClass {
    @AppStorage("testKey") var testAppStorage = ""
}

//view
struct TestAppStoregeView2: View {
    var testModel = TestAppStrageClass()
    var body: some View {
        VStack {
            TextField("input text", text: $testModel.testAppStorage)
            Text(testModel.testAppStorage)
            Button {
                testModel.testAppStorage.removeAll()
            } label: {
                Text("削除")
            }
        }
    }
}


しかしclassをObservableObjectに準拠させとインスタンス化する際に@ObservedObjectを付けるとclass側でデータの保存を行うことができます。

class TestAppStrageClass:ObservableObject {
    @AppStorage("testKey") var testAppStorage = ""
}

//View
struct TestAppStoregeView2: View {
    @ObservedObject var testClass = TestAppStrageClass()
    var body: some View {
        VStack {
            TextField("input text", text: $testClass.testAppStorage)
            Text(testClass.testAppStorage)
            Button {
                testClass.testAppStorage.removeAll()
            } label: {
                Text("削除")
            }
        }
    }
}

注意点

ここからは注意点になります。
先ほどclassで使用できると書きましたが、実際に使用する場合、一つのclassでデータの管理を行い、複数のViewで使用する状況があると思います。
以下TabViewを使用して試してみました。

//View1
struct TestAppStoregeView: View {
    @AppStorage("testKey") var testAppStorage = ""
    var body: some View {
        VStack {
            TextField("input text", text: $testAppStorage)
            
            Text(testAppStorage)
            
            Button {
                testAppStorage.removeAll()
            } label: {
                Text("削除")
            }
        }
    }
}
//View2
struct TestAppStoregeView2: View {
    @ObservedObject var testClass = TestAppStrageClass()
    var body: some View {
        VStack {
            TextField("input text", text: $testClass.testAppStorage)
            Text(testClass.testAppStorage)
            Button {
                testClass.testAppStorage.removeAll()
            } label: {
                Text("削除")
            }
        }
    }
}
//View3
struct TestAppStoregeView3: View {
    @ObservedObject var testClass = TestAppStrageClass()
    var body: some View {
        VStack {
            TextField("input text", text: $testClass.testAppStorage)
            
            Text(testClass.testAppStorage)
            
            Button {
                testClass.testAppStorage.removeAll()
            } label: {
                Text("削除")
            }
        }
    }
}

//class
class TestAppStrageClass:ObservableObject {
    @AppStorage("testKey") var testAppStorage = ""
}

結果


いかがでしょうか?
上記のようにView1では直接property wrapperを書きましたが、その他はclassからObservableObjectに準拠したプロパティを通してデータを更新しました。その結果データが一度キャッシュされ、リアルタイムで表示することができませんでした。

まとめ

非常に便利に使用できますが、MVVMなどで使用する際は注意が必要かなと思いました。
現状はViewで直接使用することが良いかもしれません。
どうしてもMVVMで保持させたい場合はUserDefaults使用する事が現状無難かと思います。

Discussion