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:
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 UIColor
s 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: ColorInfo = .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
- https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html
- https://developer.apple.com/documentation/swift/RawRepresentable
- https://medium.com/@PhiJay/why-swift-enums-with-associated-values-cannot-have-a-raw-value-21e41d5ec11
- https://developer.apple.com/documentation/foundation/jsondecoder
- https://github.com/Mindera/Alicerce/blob/408a3015dc578f2598c14645b942f04c9042d7ce/Sources/Extensions/UIKit/UIColor.swift