@AppStorage mit Swift-Klasse

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).

Dieser Beitrag wurde unter Allgemein veröffentlicht. Setze ein Lesezeichen auf den Permalink.