The other day I was trying to make due with an existing enum that used it’s raw values and add associated values to it. I eventually found out the hard way that in Swift you can’t extract a raw value if the enum has associated values, but I wasn’t going to let that stop me from achieving my goal.

# The problem

I wanted to be able to have an enumerator that had raw values

enum ColorInfo: String {
    case color
    case gradient
}

print( ColorInfo.color.rawValue ) // "color"
print( ColorInfo(rawValue: "color")!.rawValue ) // "color"
print( ColorInfo.gradient.rawValue ) // "gradient"
print( ColorInfo(rawValue: "gradient")!.rawValue ) // "gradient"

as well as associated values,

enum ColorInfo {
  case color(UIColor)
  case gradient([UIColor])
}

// e.g.
let redColor = ColorInfo.color(.red)
let redBlueGradient = ColorInfo.gradient([.red, .blue])

Seems simple enough, right? In my naivety this is what I expected to be possible.

enum ColorInfo: String {
  case color(UIColor)
  case gradient([UIColor])
}

// e.g.
let redColor = ColorInfo.color(.red)
let redBlueGradient = ColorInfo.gradient([.red, .blue])
print( redColor.rawValue ) // "color.red"
print( ColorInfo(rawValue: "color.red").rawValue ) // "color.red"
print( redBlueGradient.rawValue ) // "gradient[.red,.blue]"
print( ColorInfo(rawValue: "gradient[.red,.blue]").rawValue ) // "gradient[.red,.blue]"

But unfortunately, it wasn’t as easy as that 😢. The compiler immediately screamed at me in bright red colors:

'ColorInfo' declares raw type 'String', but does not conform to RawRepresentable and conformance could not be synthesized
Enum with raw type cannot have cases with arguments
Cannot infer contextual base in reference to member 'red'
Reference to member 'red' cannot be resolved without a contextual type
Reference to member 'blue' cannot be resolved without a contextual type

Most of that is as informative as you’d expect so I went to the source looking for some answers, and after complementing it with some quick googling, I managed to find similar problems on stackoverflow as well as a couple of informative articles about enums and what they can and can’t do.

Among them was this medium article that thoroughly explains the problem. It turns out that it makes sense that the compiler can’t create an “Implicitly Assigned Raw Value” (or unique identifier if you will) when it only has access to a data type, especially if the values associated with it are unlimited™ or plentiful, as is the case with UIColor 🌈🎨.

I wasn’t satisfied with “No.” for an answer, and seeing as there’s a RawRepresentable protocol I figured I’d just make my own Enum with associated values and raw values!

# A solution

First things first, let’s start with a regular enum with associated values.

enum ColorInfo {
    case color(UIColor)
    case gradient([UIColor])
}

Secondly, conforming to the RawRepresentable protocol in order to define how the raw values should be computed. We’ll be using strings so that our raw values are easier to debug, but this should work with any other data type that conforms to RawRepresentable OOTB (e.g. Int).

extension ColorInfo: RawRepresentable {
    typealias RawValue = String

    init?(rawValue: String) { return nil }

    var rawValue: RawValue { return "" }
}

Thirdly, we need to emulate the way that implicitly assigned raw values are computed, and for that, we’ll add a nested enum to our extension:

extension ColorInfo: RawRepresentable {
    typealias RawValue = String

    enum Identifier: String {
        case color
        case gradient
    }
// ...

Now, to put it all together, let’s start with computing the raw value, and leave the more complicated reverse process (initializing from a raw value) for last. As defined previously, we want our enum cases to output their raw values, something along these lines:

print( ColorInfo.color(.red).rawValue ) // "color.red"
print( ColorInfo.gradient([.red, .blue]).rawValue ) // "gradient[.red,.blue]"

So we need to iterate through them (using a switch statement) and handle each case accordingly. I’ve opted to convert UIColors to their hexadecimal representation (and in the case of multiple associated values, encode it all as a JSON string) and append that to the end of the corresponding identifier, e.g. "color#ffff0000" and "gradient[#ffff0000,#ff0000ff]", which translates into:

// ...
    var rawValue: RawValue {
        switch self {
        case .color(let color):
            return ColorInfo.Identifier.color.rawValue
                + color.hexStringWithAlpha

        case .gradient(let colors):
            return ColorInfo.Identifier.gradient.rawValue
                + (arrayToJsonString(array: colors.map(\.hexStringWithAlpha)) ?? "[]")
        }
    }
// ...

Lastly, let’s tackle that pesky initializer. We want to determine which identifier (if any!) matches the provided rawValue, and to facilitate our lives a little bit, we can adopt the CaseIterable protocol for out Identifier enum and filter out our candidates. Hopefully, there will be only one, else it just means we might need more distinct identifiers 🙈. All that’s left is to recreate the UIColor from their hexadecimal representation (and in the case of multiple values, decode the JSON string).

// ...
    init?(rawValue: String) { // e.g. "color#ffff0000" or "gradient[#ffff0000,#ff0000ff]"

        let identifierMatches = Identifier.allCases
            .map(\.rawValue)
            .filter { rawValue.contains($0) }

        guard
            identifierMatches.count == 1,
            let match = identifierMatches.first // e.g. "color" or "gradient"
        else {
            print("🎨🙈 Maybe the identifiers aren't that distinct?")
            return nil
        }

        let rawColorInfo = rawValue.replacingOccurrences(of: match, with: "") // e.g. "#ffff0000" or "[#ffff0000,#ff0000ff]"

        switch match {
        case Identifier.color.rawValue:
            self = ColorInfo.color(UIColor(hex: rawColorInfo))

        case Identifier.gradient.rawValue:
            guard let jsonColors: [String] = jsonStringToArray(json: rawColorInfo)
            else { return nil }
            self = ColorInfo.gradient(jsonColors.compactMap{ UIColor(hex: $0) })

        default:
            return nil
        }
    }
// ...

# Nitty gritty

I used this extension to get the Hex code from an UIColor, as well as the following snippets to Encode/Decode the array of Hex colors:

func arrayToJsonString<T: Encodable>(array: [T]) -> String? {
    guard let data = try? JSONEncoder().encode(array)
    else { return nil }

    return String(data: data, encoding: .utf8)
}

func jsonStringToArray<T: Decodable>(json: String) -> [T]? {
    guard
        let data = json.data(using: .utf8),
        let val = try? JSONDecoder().decode([T].self, from: data)
    else { return nil }

    return val
}

# Finished product

As promised, here is the finished product: an enumeration with associated values & raw values.

enum ColorInfo {
    case color(UIColor)
    case gradient([UIColor])
}

extension ColorInfo: RawRepresentable {
    typealias RawValue = String

    enum Identifier: String, CaseIterable {
        case color
        case gradient
    }

    init?(rawValue: String) {

        let identifierMatches = Identifier.allCases
            .map(\.rawValue)
            .filter { rawValue.contains($0) }

        guard
            identifierMatches.count == 1,
            let match = identifierMatches.first
        else {
            print("🎨🙈 Maybe the identifiers aren't that distinct?")
            return nil
        }

        let rawColorInfo = rawValue.replacingOccurrences(of: match, with: "")

        switch match {
        case Identifier.color.rawValue:
            self = ColorInfo.color(UIColor(hex: rawColorInfo))

        case Identifier.gradient.rawValue:
            guard let jsonColors: [String] = jsonStringToArray(json: rawColorInfo)
            else { return nil }
            self = ColorInfo.gradient(jsonColors.compactMap{ UIColor(hex: $0) })

        default:
            return nil
        }
    }

    var rawValue: RawValue {
        switch self {
        case .color(let color):
            return ColorInfo.Identifier.color.rawValue
                + color.hexStringWithAlpha

        case .gradient(let colors):
            return ColorInfo.Identifier.gradient.rawValue
                + (arrayToJsonString(array: colors.map(\.hexStringWithAlpha)) ?? "[]")
        }
    }
}

And since the proof is in the pudding, here’s the proof-of-concept example of it being used.

func identify(colorInfo: ColorInfo?) {
    switch colorInfo {
    case .color(let color):
        if color == .red {
            print("Red color")
        }

    case .gradient(let colors):
        if colors.first == .red && colors.last == .blue {
            print("Red and blue gradient")
        }

    default:
        print("No matches!")
    }
}

// using associated values

print( ColorInfo.color(.red).rawValue ) // "color#ffff0000"
print( ColorInfo.gradient([.red, .blue]).rawValue ) // "gradient[#ffff0000,#ff0000ff]"

// using raw values

let redColor = ColorInfo(rawValue: "color#ffff0000")
let redBlueGradient = ColorInfo(rawValue: "gradient[\"#ffff0000\",\"#ff0000ff\"]")
let other = ColorInfo(rawValue: "randomGibberish") // nil

identify(colorInfo: redColor) // "Red color"
identify(colorInfo: redBlueGradient) // "Red and blue gradient"
identify(colorInfo: other) // "No matches!"

It’s not as pretty as we initially intended, but it was the best way I could find of having the init?(rawValue: String) working without having to deal with delimiters, more hardcoded values, or even natural language 😱.

print( redColor.rawValue ) // expected: "color.red", got: "color#ffff0000"
print( redBlueGradient.rawValue ) // expected: "gradient[.red,.blue]", got: "gradient[#ffff0000,#ff0000ff]"

# Afterthoughts

If you’ve gotten this far, thank you for indulging my weird ramblings about possible solutions for non-existent problems.

In fact, you’re probably wondering why I went to such lengths for something so objectively ugly and hard to maintain. In hindsight, the true solution to this problem using “vanilla” swift would be to re-think the initial problem and existing code’s utility, and re-write it as struct like so

struct ColorInfo {
    let type: ColorType
    let colors: [UIColor]

    enum ColorType: String {
        case color
        case gradient
    }
}

let redColor: ColorInfo4 = .init(type: .color, colors: [.red])
let redBlueGradient: ColorInfo = .init(type: .gradient, colors: [.red, .blue])

But where’s the fun in that? 😜

As to why you might need Enums with Associated values & Raw values, it’s anyone’s guess 😅 but now you know how to achieve it! 💪 Be sure to let me know if you find a better way of achieving the same result using enums 🙏




# Sources