The following code snippet works exactly as expected:
import Foundation
struct S: Codable {
let i: Int
let j: String
init(i: Int, j: String) {
self.i = i
self.j = j
}
init?(rawValue: String) {
guard
let data = rawValue.data(using: .utf8),
let s = try? JSONDecoder().decode(S.self, from: data)
else { return nil }
self = s
}
var rawValue: String {
guard
let data = try? JSONEncoder().encode(self),
let string = String(data: data, encoding: .utf8)
else { return "" }
return string
}
}
let s = S(i: 1, j: "String")
let t = S(rawValue: s.rawValue)!
print(t == s) // true
If I conform S to RawRepresentable though I get:
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=2, address=0x16a92fff0).
I've changed nothing except adding : RawRepresentable in the declaration of S.
What is causing this crash?
xAlien95
(Stefano De Carolis)
2
If I'm not mistaken, when you have a Codable and RawRepresentable type, the rawValue is used to encode/decode the value. If that's the case, you're incurring in an endless recursion by encoding/decoding in the rawValue definition.
Edit: I'm not mistaken.
2 Likes
Thanks! That seems to be the problem.
Adding manual en-/decoding fixed it:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(i, forKey: .i)
try container.encode(j, forKey: .j)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
i = try container.decode(Int.self, forKey: .i)
j = try container.decode(String.self, forKey: .j)
}
1 Like
I recently came across this problem when trying to store a codable into UserDefaults through the AppStorage property wrapper. I thought I'd be clever and use RawRepresentable with a String rawValue type but then this recursion came up.
I didn't like the idea of error-prone manual encoding/decoding and so came up with the following solution:
struct CodableWrapper<Value: Codable> {
var value: Value
}
extension CodableWrapper: RawRepresentable {
typealias RawValue = String
var rawValue: RawValue {
guard
let data = try? JSONEncoder().encode(value),
let string = String(data: data, encoding: .utf8)
else {
// TODO: Track programmer error
return ""
}
return string
}
init?(rawValue: RawValue) {
guard
let data = rawValue.data(using: .utf8),
let decoded = try? JSONDecoder().decode(Value.self, from: data)
else {
// TODO: Track programmer error
return nil
}
value = decoded
}
}
Usage
struct PrintSettings: Codable {
var fontSize: Double = 12.0
var alignment: PrintAlignment = .left
}
struct MyView: View {
@AppStorage("printSettings")
var printSettings: CodableWrapper<PrintSettings> = .init(value: .init())
func foo() {
printSettings.value.fontSize = 30
}
}
2 Likes
This is great! The only thing I would add is:
extension CodableWrapper: Equatable where Value : Equatable {}
This allows you to use SwiftUI's onChange(of: printSettings) if PrintSettings: Equatable
1 Like
To add to your implementation for better legibility, I would recommend making CodableWrapper a Property Wrapper with Dynamic Member Lookup as follows:
@propertyWrapper
@dynamicMemberLookup
struct AppStorageValueWrapper<Value: Codable> {
var wrappedValue: Value
subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
get { wrappedValue[keyPath: keyPath] }
set { wrappedValue[keyPath: keyPath] = newValue }
}
}
This will add the syntactic sugar at call site as under:
@AppStorage("printSettings")
@CodableWrapper
var printSettings: PrintSettings = .init()
This will additionally help with referring to the core AppStorage value and it's binding
2 Likes