I am attempting to use @AppStorage
with a data structure that contains a String
and a UUID
by adding RawRepresentable
to the structure. It appears that RawRepresentable
is causing the JSONEncoder.encode()
function to misbehave.
The following Minimal Reproducible Example causes an infinite loop when print(p.rawValue)
is called. In that case, "rawValue 1"
is printed repeatedly until the app eventually crashes.
Printer(name: "HP", id: CBAA56C5-7E14-4351-BFF4-75413AE8B5C5)
rawValue 1
rawValue 1
rawValue 1
...
Removing : RawRepresentable
causes the call to work correctly, but that is needed to use this structure with @AppStorage
.
Printer(name: "HP", id: CBAA56C5-7E14-4351-BFF4-75413AE8B5C5)
rawValue 1
rawValue 2
rawValue 3
{"name":"HP","id":"CBAA56C5-7E14-4351-BFF4-75413AE8B5C5"}
Am I doing something wrong? Or perhaps is this a known bug with a workaround?
Note: this is a simplified example. My Printer
struct is a bit more complex`.
import SwiftUI
struct Printer: Codable {
let name: String
let id: UUID
}
extension Printer: RawRepresentable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Printer.self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
print("rawValue 1")
guard let data = try? JSONEncoder().encode(self) else { return "{}" }
print("rawValue 2")
guard let result = String(data: data, encoding: .utf8) else { return "{}" }
print("rawValue 3")
return result
}
}
struct ContentView: View {
// Ideally I want to use @AppStorage here...
// @AppStorage("printer") var p = Printer(name: "HP", id: UUID())
@State private var p = Printer(name: "HP", id: UUID())
var body: some View {
VStack {
Text("Hello World!")
.onAppear {
print(p)
print(p.rawValue)
}
}
}
}
Your problem is this definition from stdlib:
extension RawRepresentable where Self : Decodable, Self.RawValue == String {
/// Creates a new instance by decoding from the given decoder, when the
/// type's `RawValue` is `String`.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
public init(from decoder: any Decoder) throws
}
RawRepresentable things automatically get an encode(to:)
method that is implemented by encoding their rawValue
, which is recursive.
You would need to write the encode(to:)
and init(from:)
methods by hand to override the default RawRepresentable implementations.
(I expect there's an easier way that doesn't involve RawRepresentable, but it doesn't immediately come to mind.)