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


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