Der Property-Wrapper @AppStorage speichert Value-Types in den UserDefaults. Will man aber ein Objekt dort speichern, muss man einige Klimmzüge machen. Um anderen das Suchen nach einer Lösung zu erleichtern ist hier meine Lösung:
Ausgangslage:
Ich erstelle eine einfache Klasse wie diese:
class Person: ObservableObject {
@Published var name: String
@Published var age: Int
init() {
self.name = ""
self.age = 0
}
}
Will ich diese nun als @AppStorage in den UserDefaults speichern bzw. von diesen laden:
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@AppStorage("person") var person = Person()
var body: some View {
VStack {
TextField("Name: ", text: $person.name).padding()
TextField("Age: ", value: $person.age, formatter: NumberFormatter()).padding()
Spacer()
}
}
}
Leider lässt Swift (derzeitige Version: 5.3) das so nicht zu. Klare Ansage: die Variable kann ein Value-Type sein oder muss einen rawValue haben. Als rawValue zulässig sind jedoch nur Double, Int, Bool oder String. Data ist nicht möglich.
Lösung: Die Klasse muss die Protokolle RawRepresentable und Codable verwenden.
class Person: ObservableObject, RawRepresentable, Codable {
@Published var name: String
@Published var age: Int
init() {
self.name = ""
self.age = 0
}
// CodingKeys: erforderlich, damit rawValue nicht kodiert wird
enum CodingKeys: String, CodingKey {
case name = "name"
case age = "age"
}
// init from decoder
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
}
// init with rawValue
required init?(rawValue: String) {
if rawValue.isEmpty {
print("Initialised with empty rawValue, setting defaults")
self.name = ""
self.age = 0
return
}
print("Initialising with rawValue: \(rawValue)")
guard let data = try? JSONDecoder().decode(Person.self, from: rawValue.data(using: .utf8)!) else {
self.name = ""
self.age = 0
return
}
self.name = data.name
self.age = data.age
}
// encode to encoder
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
}
// raw representable
var rawValue: String {
get {
guard let data = try? JSONEncoder().encode(self) else {
print("Unable to encode data")
return ""
}
print("Returning raw value: \(data)")
return String(data: data, encoding: .utf8)!
}
}
}
Die Funktion encode(…) ist zwingend erforderlich, da der Wert rawValue (computed property) nicht serialisiert werden darf (erzeugt anderenfalls eine Schleife).