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)
September 9, 2021, 12:38pm
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.
extension RawRepresentable where RawValue == String, Self: Encodable {
/// Encodes this value into the given encoder, when the type's `RawValue`
/// is `String`.
///
/// This function throws an error if any values are invalid for the given
/// encoder's format.
///
/// - Parameter encoder: The encoder to write data to.
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.rawValue)
}
}
extension RawRepresentable where RawValue == String, Self: Decodable {
/// 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.
This file has been truncated. show original
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
1 Like