I want to use AppStorage for a custom struct I am using
struct Activities {
var name: String
var age: Int
}
struct ContentView: View {
@AppStorage("key") private var activities: Activities = .init(name: "Albert", age: 42)
var body: some View {
VStack {
TextField("Activity Name", text: $activities.name)
}
}
}
The above code generates a compiler warning, recommending I add RawRepresentable conformance. So I've added it like this:
extension Activities: RawRepresentable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8) else {
return nil
}
do {
let result = try JSONDecoder().decode(Activities.self, from: data)
self = result
}
catch {
print(error)
return nil
}
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8) else {
return "{}"
}
return result
}
}
This leads to a stack overflow because calling encode from rawValue calls rawValue. :-( I overcame this by declaring Codable conformance and overriding the default Encodable implementation:
extension Activities: Codable {
enum CodingKeys: String, CodingKey {
case name
case age
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
}
}
This solves the stack overflow, but now init?(rawValue: String) is failing and I'm not sure why. When I set a breakpoint in my catch block I see the following:
(lldb) po error
▿ DecodingError
▿ typeMismatch : 2 elements
- .0 : Swift.String
▿ .1 : Context
- codingPath : 0 elements
- debugDescription : "Expected to decode String but found a dictionary instead."
- underlyingError : nil
(lldb) po rawValue
{"name":"Albert2","age":42}
(lldb) po data
▿ 27 bytes
- count : 27
▿ bytes : 27 elements
- 0 : 123
- 1 : 34
- 2 : 110
- 3 : 97
- 4 : 109
- 5 : 101
- 6 : 34
- 7 : 58
- 8 : 34
(truncated to save space for posting :-)
I’m not 100% sure what’s causing all the behaviour you’re seeing [1] but if I were in your shoes I’d get around this by declaring an codable version of your struct solely for the benefit of the coding system:
struct Activities {
var name: String
var age: Int
fileprivate struct Inner: Codable {
var name: String
var age: Int
}
}
extension Activities: RawRepresentable {
init?(rawValue: String) {
guard let i = try? JSONDecoder().decode(Inner.self, from: Data(rawValue.utf8)) else {
return nil
}
self.init(name: i.name, age: i.age)
}
var rawValue: String {
let i = Inner(name: self.name, age: self.age)
let e = try! JSONEncoder().encode(i)
return String(decoding: e, as: UTF8.self)
}
}
The downside is that you need to keep Activities and Activities.Inner in sync, but the benefit is that the extra code is super simple.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] I think it’s because the encoding system has special cases for types with a raw value, but I haven’t actually dug into that properly.