As mentioned in my previous post, the raw values used with enumerations in Swift are limited to either String
, Character
, Int
, Float
or Double
values. In certain cases though we might want to use our own custom types. In this post I thought I’d investigate whether it was possible to use custom types of our own as raw values.
Let’s start off with an example. Say we had a Vehicle
struct with two properties (a UInt8
to hold the number of wheels the vehicle has and a Bool
to indicate whether the vehicle has a motor) – yeah I know, silly example but go with it for now:
struct Vehicle {
let wheelCount: UInt8
let motorized: Bool
}
Also imagine that we wanted to use that struct as the raw value in an enumeration. The bad news is that if we wrote the following it wouldn’t compile:
enum Transportation : Vehicle {
case bike = Vehicle(wheelCount: 2, motorized: false)
case car = Vehicle(wheelCount: 4, motorized: true)
case truck = Vehicle(wheelCount: 6, motorized: true)
}
Ok, so that doesn’t work, but we do know that enumerations in Swift support raw values that are of type String
so what if we could represent our Vehicle
as a String
instead? One string representation might be JSON:
struct Vehicle {
let wheelCount: UInt8
let motorized: Bool
func JSONString() -> String {
var dict : Dictionary = [:]
dict["wheelCount"] = Int(self.wheelCount)
dict["motorized"] = self.motorized
let jsonData = try! NSJSONSerialization.dataWithJSONObject(dict, options:NSJSONWritingOptions())
return NSString(data: jsonData, encoding: NSUTF8StringEncoding) as! String
}
}
let v = Vehicle(wheelCount: 2, motorized: false)
print(v.JSONString())
// prints - {"wheelCount":2,"motorized":false}
Ok, good start. That gets us a string representation of our struct but we also need to be able to recreate our Vehicle
struct from a given JSON string.
Let’s deal with that next.
Built in to Swift is a protocol called StringLiteralConvertable
. As the documentation for this protocol states, the StringLiteralConvertable
protocol allows any conforming type to be ”initialised from an arbitrary string”. This sound like exactly what we want.
The StringLiteralConvertable
protocol requires any type that conform to the protocol to implement three initialisation methods:
init(stringLiteral value: String.StringLiteralType)
init(extendedGraphemeClusterLiteral value: String.ExtendedGraphemeClusterLiteralType)
init(unicodeScalarLiteral value: String.UnicodeScalarLiteralType)
Although this might look a little daunting, is not as complicated as it seems. In reality, String.StringLiteralType
, String. ExtendedGraphemeClusterLiteralType
and String.UnicodeScalarLiteralType
are all actually type aliases for the good old String
type. This simplifies things somewhat and means that we can implement a single init
function that accepts a String
as a parameter and then call that function from these others initialisers as long as we cast their value
parameters to a String
value e.g.:
init(string value: String) {
// Do initialisation here....
}
init(stringLiteral value: String.StringLiteralType) {
self.init(string: String(value))
}
init(extendedGraphemeClusterLiteral value: String.ExtendedGraphemeClusterLiteralType) {
self.init(string: String(value))
}
init(unicodeScalarLiteral value: String.UnicodeScalarLiteralType) {
self.init(string: String(value))
}
Ok, we now need to make our Vehicle
type actually conform to the protocol:
extension Vehicle : StringLiteralConvertible {
internal init(string value: String) {
guard let data = value.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) else {
self.init(wheelCount: 0, motorized: false)
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject]
guard let wheelCount = json["wheelCount"] as? Int else {
self.init(wheelCount: 0, motorized: false)
return
}
guard let motorized = json["motorized"] as? Bool else {
self.init(wheelCount: 0, motorized: false)
return
}
self.init(wheelCount: UInt8(wheelCount), motorized: motorized)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.init(wheelCount: 0, motorized: false)
}
}
init(stringLiteral value: String.StringLiteralType) {
self.init(string: String(value))
}
init(extendedGraphemeClusterLiteral value: String.ExtendedGraphemeClusterLiteralType) {
self.init(string: String(value))
}
init(unicodeScalarLiteral value: String.UnicodeScalarLiteralType) {
self.init(string: String(value))
}
}
This works, but isn’t quite as tight as I would like. For example, in the case of an invalid JSON string we have to return a default struct (not ideal – it feels like we should really use an optional return value) and even with a valid JSON string, it works for any JSON string as long as it contains a boolean motorized
value and a numeric wheelCount
value. For example, given the implementation above the following would work:
let testVehicle = Vehicle(jsonString: "{\"name\":\"Honda\",\"wheelCount\":2,\"motorized\":false}")
Again, this is not ideal and something I probably need to revisit.
Anyway, putting those issues aside, we can now represent values of type Vehicle
as String
values and also re-create those Vehicle
instances from a corresponding String
. At this point then we could potentially re-write our enumeration as:
enum Transportation : String {
case bike = "{\"wheelCount\":2,\"motorized\":false}"
case car = "{\"wheelCount\":4,\"motorized\":true}"
case truck = "{\"wheelCount\":6,\"motorized\":true}"
}
And we could even create enumeration values from instances of our Vehicle
struct and reconstitute those values from the enumeration cases raw value:
// From Vehicle to JSON to Enum
let v1String = Vehicle(wheelCount: 4, motorized: true).JSONString()
let transport1 = Transportation(rawValue: v1String)
// From Enum to JSON to Vehicle
if let transport1 = transport1 {
let vehicle1 = Vehicle(jsonString: transport1.rawValue)
print(vehicle1)
}
But it’s still a little clunky. Notice that I’ve had to indicate the enumerations raw values are String
values not Vehicles
. I think we can do better.
Let’s see what happens if we change the raw value type to Vehicle
:
enum Transportation : Vehicle {
case bike = "{\"wheelCount\":2,\"motorized\":false}"
case car = "{\"wheelCount\":4,\"motorized\":true}"
case truck = "{\"wheelCount\":6,\"motorized\":true}"
}
// RawRepresentable 'init' cannot be synthesized because the raw type 'Vehicle' is not Equatable.
Ok, so the compilers not happy and it’s giving us a hint about what we need to do to fix it. Essentially we need to make our type conform to the Equatable
protocol.
Making our Vehicle
type conform to the Equatable
protocol is actually pretty simple. The protocol requires two things. Firstly, any conforming type should declare that it is going to conform to the protocol (our Vehicle
type doesn’t currently). Secondly the protocol requires any conforming type to define the equality operator (==
) for that type. These are both things we can cope with:
extension Vehicle : Equatable {}
func ==(lhs : Vehicle, rhs: Vehicle) -> Bool {
return lhs.wheelCount == rhs.wheelCount &&
lhs.motorized == rhs.motorized
}
With that done the compiler is happy!
Let’s test it out. First, let’s define a value of type Vehicle
and create a corresponding associated enumeration case:
let transport : Transportation? = Transportation(rawValue: Vehicle(wheelCount: 2, motorized: false))
// transport is now Optional(Transportation.bike)
Ok, so far so good. Let’s see if we can take an enumeration case and access it’s raw value:
let vehicle: Vehicle = Transportation.bike.rawValue
// vehicle is now equal to Vehicle(wheelCount : 2, motorized: false)
Again, it works! It’s not pretty but it does work. Anyway, I’m going to leave my investigation there for now but if you have any suggestions for improvements, it’d love to hear them.