💾
structをCodableとRawRepresentableに準拠させて@AppStorageで値を保存する
概要
- 下記のstructを
@AppStorageで扱えるようにしたい。
struct FontData {
var name: String
var pointSize: CGFloat
}
- 逆算すると以下の流れである。
-
@AppStorageで扱うには - ->
RawRepresentableに準拠が必要 - ->
Codableに準拠が必要
-
参考
実装
Codableへの準拠
- まず
Codableに準拠させる。 - structのプロパティが全てCodableの場合でも、Codableに必要なメソッドを自前で実装する必要がある
- 具体的には値を更新したときに後述の
var rawValue: String内でencodeが無限ループしてしまいクラッシュする
- 具体的には値を更新したときに後述の
RawRepresentable Conformance Leads to Crash
when you have a Codable and RawRepresentable type, the rawValue is used to encode/decode the value. If that's the case, you're incurring in an endless recursion by encoding/decoding in the rawValue definition.
extension FontData: Codable {
enum CodingKeys: String, CodingKey {
case name
case pointSize
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(pointSize, forKey: .pointSize)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
pointSize = try container.decode(CGFloat.self, forKey: .pointSize)
}
}
RawRepresentableへの準拠
- 追記: AppStorageにCodableを保存する時に気を付けたいパフォーマンスの話の記事も参考に
Equatableを実装するといいかと思います。
- 次に
RawRepresentableに準拠させる - associatedtype RawValueに関しては、内部的に扱う型を指定する
- 今回は
Stringを使っている-
var rawValue: StringではJSON文字列のStringを書き出す -
init?(rawValue: String)では上記の文字列をFontData型に変換して取得する
-
-
rawValueにはIntまたはStringのみと決まっている
https://nilcoalescing.com/blog/SaveCustomCodableTypesInAppStorageOrSceneStorage/
Now we need to add RawRepresentable conformance to our custom Codable type. Remember, that AppStorage only supports RawRepresentable where the RawValue's associatedtype is of type Int or String. So the rawValue property has to return an Int or a String.
extension FontData: RawRepresentable {
init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let decoded = try? JSONDecoder().decode(FontData.self, from: data) else {
return nil
}
self = decoded
}
var rawValue: String {
guard
let data = try? JSONEncoder().encode(self),
let jsonString = String(data: data, encoding: .utf8) else {
return ""
}
return jsonString
}
}
呼び出し例
- 以上で
@AppStorageからFontDataを扱えるようになったので、以下のように呼び出して使用できるようになった。 - 別の言い方をすると
@AppStorageの内部はUserDefautlsであり、RawRepresentableの準拠によりFontData型はStringを経由してUserDefautlsで扱えるようになっている。

import SwiftUI
struct ContentView: View {
@AppStorage("font-data")
var fontData: FontData = .init(name: "Helvetica", pointSize: 14)
var body: some View {
Form {
Section {
HStack {
Text("Value")
Spacer()
Text("\(fontData.name): \(Int(fontData.pointSize))")
.monospacedDigit()
}
.foregroundColor(.secondary)
}
Section {
TextField("Name", text: $fontData.name)
Slider(value: $fontData.pointSize, in: 1...100) {
Text("Size")
}
Button("Reset") {
let appDomain = Bundle.main.bundleIdentifier
UserDefaults.standard.removePersistentDomain(forName: appDomain!)
// fontData = .init(name: "Helvetica", pointSize: 14)
}
.frame(maxWidth: .infinity, alignment: .trailing)
} header: {
Text("Input")
}
}
.frame(width: 400)
.formStyle(.grouped)
}
}
#Preview {
ContentView()
}
Discussion