I have a custom struct I want to store in AppStorage
:
struct Budget: Codable, RawRepresentable {
enum CodingKeys: String, CodingKey {
case total, spent
}
var total: Double
var spent: Double
init(total: Double = 5000.0, spent: Double = 3000.0) {
self.total = total
self.spent = spent
}
init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Budget.self, from: data)
else { return nil }
self = result
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return ""
}
return result
}
}
I then have the following view:
struct DemoView: View {
@AppStorage(UserDefaults.StorageKeys.budget.rawValue) var budget = Budget()
var body: some View {
Button("Update") {
budget.total = 10
}
}
}
When I tap the button the app crashes with Thread 1: EXC_BAD_ACCESS
on guard let data = try? JSONEncoder().encode(self)
for rawValue
in Budget
. What am I doing wrong here?
CodePudding user response:
You are running into infinite recursion. This is because types that conforms to both Encodable
and RawRepresentable
automatically get this encode(to:)
implementation (source), which encodes the raw value. This means that when you call JSONEncoder().encode
, it would try to call the getter of rawValue
, which calls JSONEncoder().encode
, forming infinite recursion.
To solve this, you can implement encode(to:)
explicitly:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(total, forKey: .total)
try container.encode(spent, forKey: .spent)
}
Note that you should also implement init(from:)
explicitly, because you also get a init(from:)
implementation (source) that tries to decode your JSON as a single JSON string, which you certainly do not want.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
total = try container.decode(Double.self, forKey: .total)
spent = try container.decode(Double.self, forKey: .spent)
}