💾
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