RawRepresentable Conformance Leads to Crash

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?

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
    }
}


4 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

2 Likes

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

3 Likes

These are the best practices I've found to accomplish this goal, better than 90% of the similar responses out there, and avoiding custom code per Codable. Thanks to all who responded for providing these insights.

I've summed it all up into a gist, for others who struggled to understand how to put the parts together.

That said, I encountered a weird problem with this method. My Codable model has several members affected by bindings within a single UI. When applying the techniques from this thread, one of those bindings always fetches a stale value of the model. The problem disappears by simply switching the model back to a @State var!

For now, I'm left with this hideous workaround:

.onChange(of: config) { updated in
    // Kludge for stale binding values when using @AppStorage directly.
    persistedConfig = updated
}