RawRepresentable conformance for generic types such as Set, ClosedRange

I need to have Set and ClosedRange conform to RawRepresentable protocol. I use the following code for Set and identical code for ClosedRange apart from replacing "Element" by
"Bound" (the parameter type for ClosedRange). The code works ok, but I do not like the code repetition. Can I combine the code for Set and ClosedRange?

extension Set: RawRepresentable where Element: Codable {
    public typealias RawValue = String
    public init?(rawValue: RawValue) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode(Self.self, from: data) else {
            return nil
        }
        self = result
    }

    public var rawValue: RawValue {
        guard let data = try? JSONEncoder().encode(self) else {
            return ""
        }
        return String(decoding: data, as: UTF8.self)
    }
}

You can. But why would you want to do this?

Let's start with the "can" part:

Actually you don't care if Element is Codable, because you are not coding individual elements, only the entire set. So actually you need where Self: Codable constraint. And for ClosuredRange that's the same constraint. So now you can move this code as an extension to a dummy protocol:

protocol RawRepresentableThroughCodable: RawRepresentable, Codable {}
extension Set: RawRepresentableThroughCodable {}
extension ClosedRange: RawRepresentableThroughCodable {}
extension RawRepresentableThroughCodable {
    public typealias RawValue = String
    public init?(rawValue: RawValue) {
        ...
    }
    public var rawValue: RawValue {
        ...
    }
}

Now the why part.

First of all, you adding so-called "retroactive conformance" - you are conforming type that you don't own to the protocol that you don't own, so probably you are doing someone else's job, which can lead to a mess. You can read more here - swift-evolution/proposals/0364-retroactive-conformance-warning.md at main · swiftlang/swift-evolution · GitHub.

This is a red flag, but probably more a symptom than a root cause. I think the root cause is that you are trying to use RawRepresentable for this.

Technically it works - it allows to always convert something to a string and sometimes convert it from a string. But still RawRepresentable was designed for a different purpose.

With a RawRepresentable type, you can switch back and forth between a custom type and an associated RawValue type without losing the value of the original RawRepresentable type. Using the raw value of a conforming type streamlines interoperation with Objective-C and legacy APIs and simplifies conformance to other protocols, such as Equatable, Comparable, and Hashable.

The RawRepresentable protocol is seen mainly in two categories of types: enumerations with raw value types and option sets.

Typically rawValue is a cheap O(1) operation to get the value which is literally stored inside.

I'm not sure how this conformance would affect Objective-C interoperation, but know that "simplifies conformance to other protocols" can cause you some troubles.

There the following extensions in the standard library:

extension RawRepresentable where Self : Encodable, Self.RawValue == String {
    public func encode(to encoder: Encoder) throws
}

extension RawRepresentable where Self : Decodable, Self.RawValue == String {
    public init(from decoder: Decoder) throws
}

With your conformance you make this functions eligible for Set and ClosedRange. If they end up being used as an implementation for Codable, they will call rawValue/init?(rawValue:) inside them, leading to infinite recursion.

I would suggest to use a custom protocol instead of RawRepresentable. Would something like this work for your use case?

protocol StringRepresentable {
    init?(stringRepresentation: String)
    var stringRepresentation: String { get }
}
6 Likes

Thanks for your prompt reply.
I tried a protocol like the RawRepresentableThroughCodable you suggested. This works for
structs that are already Codable; I used it in my code for CGPoint and
other simple structs I defined myself that are Codable.

Indeed, for Set and ClosedRange, using this protocol extension leads to infinite recursion.

The extension that I described for Set and ClosedRange in my question does not lead to infinite recursion but leads to ugly code repetition.
I thought I needed the RawRepresentable conformance so as to be able to persist values in AppStorage. I cannot find the explicit requirement in the official apple docs though, although

The "retroactive conformance" argument, I get, but I somehow would like to be able to work with sets and closedRanges that can be persisted in swift.

I would've turned this around: instead of adding a property to different types (which can be arguable whether they should or shouldn't posses them), create a wrapper type specifically for storing, given that example relies on RawRepresentable, it can look something like this:

import Foundation

struct RawCodable<T>: RawRepresentable where T: Codable {
    typealias RawValue = String

    let wrappedValue: T

    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }

    public init?(rawValue: RawValue) {
        guard
            let data = rawValue.data(using: .utf8),
            let result = try? JSONDecoder().decode(T.self, from: data)
        else {
            return nil
        }
        wrappedValue = result
    }

    public var rawValue: RawValue {
        guard let data = try? JSONEncoder().encode(wrappedValue) else {
            return ""
        }
        return String(decoding: data, as: UTF8.self)
    }
}

But in general I see a lot of issues with the approach: unclear string representation, heavy rawValue, missing errors propagation (you just ignore them and return nil or empty string), which usually makes life harder somewhere down the road. So I'd advised agains this if possible.

EDIT. I've just realised that all of this is for AppStorage. You might as well have complications if you're trying to use it for something more complex then trivial UI state storage: this property wrapper isn't designed to store apps data, not in UI.

1 Like

I see.

Solution #1: The simplest

Don't conform Set and ClosedRange to RawRepresentable, but use a wrapper type. Do not conform wrapper type to Codable, Encodable or Decodable!

struct AppStorageBox<T: Codable>: RawRepresentable {
    var value: T
    init?(rawValue: String) { ... }
    var rawValue: String { ... }
}
...
@AppStorage var storage: AppStorageBox<Set<Int>>

Solution #2: Nicer API, more complex implementation

Make a custom wrapper around AppStorage<String> for values conforming to StringRepresentable. If you are ok with using it only for types which are not directly supported by AppStorage, it is pretty straightforward - you don't need AppStorable and you can hardcode StorableRepresentation to be String (see below). Implementing projectedValue can be a bit tricky.

At the use site you will have a mix of AppStorage and MyAppStorage.

Solution #3: Full encapsulation of AppStorage

If you want to also support types which are already supported by the AppStorage, you will need a few extra steps:

protocol AppStorable {
    func makeStorage(key: String, store: UserDefaults?) -> AppStore<Self>
}
extension Bool: AppStorable {
    func makeStorage(key: String, store: UserDefaults?) -> AppStore<Self> {
        AppStorage(wrappedValue: self, key, store: store)
    }
}
...
extension String: AppStorable {
    func makeStorage(key: String, store: UserDefaults?) -> AppStore<Self> {
        AppStorage(wrappedValue: self, key, store: store)
    }
}

protocol AppStorableRepresentable {
    associatedtype StorableRepresentation: AppStorable
    init?(storableRepresentation: StorableRepresentation)
    var storableRepresentation: StorableRepresentation { get }
}

extension AppStorableRepresentable where Self: AppStorable {
    init?(storableRepresentation: Self) { self = storableRepresentation }
    var storableRepresentation: Self { self }
}

extension Bool: AppStorableRepresentable {}
...
extension String: AppStorableRepresentable {}

@propertyWrapper
struct MyAppStorage<T: AppStorableRepresentable>: DynamicProperty {
    private var defaultValue: T
    private var storage: AppStorage<T.StorableRepresentation>

    init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) {
        self.defaultValue = wrappedValue
        self.storage = wrappedValue.storableRepresentation.makeStorage(key: key, store: store)
    }
    var wrappedValue: Int {
        get { T(storableRepresentation: storage.wrappedValue) ?? defaultValue }
        nonmutating set { storage.wrappedValue = newValue.storableRepresentation }
    }

    var projectedValue: Binding<Int> {
        return storage.projectedValue[dynamicMember: \.[defaultValue: defaultValue]]
    }
}

private extension AppStorable {
    subscript<T: AppStorableRepresentable>(defaultValue defaultValue: T) -> T where T. StorableRepresentation == Self {
        get { T(storableRepresentation: storage.wrappedValue) ?? defaultValue }
        set { self = newValue.storableRepresentation }
    }
}

extension AppStorableRepresentable where Self: Codable {
    ...
}
extension Set: AppStorableRepresentable {}
extension ClosedRange: AppStorableRepresentable {}
1 Like

Thanks again. I tested your AppStorageBox solution; it is ok for me.
A really minor disadvantage is that I access my content through an extra "value" property.
Solution 3 seems to have a disadvantage of extending builtin types. But I should probably explore it more. Thanks.

Thanks for the answers again. I now realise that I should have started my question with the real reason why I needed the RawRepresentable conformance: I needed it for persistently storing Sets and ClosedRanges in a swiftui View. I expected to use that in a property wrapped @AppStorage property.

I looked back on the internet where I now rejected all the solutions involving extension of builtin swift types as your answers here suggestednot to use.

I found an elegant solution, but I suppose it is not ok to post other peoples' code here.
The solution I found from Using Codable with AppStorage, the easy way! · GitHub
(not the code in CodeableAppStorage.swift there , but the comment below that) is a propertyWrapper:

@propertyWrapper
public struct CodableAppStorage<Value: Codable>: DynamicProperty {
    @AppStorage
    private var value: Data
...

where the private var value: Data is set in the init(..) and the wrappedValue is a computed variable decoding/encoding the value.
This simply allows me to specify

struct TestRaw: View {
    @CodableAppStorage("intsetbox2") var set2 = Set([1, 2])