Welcome to this new posts in my series about enums with associated values. In the previous posts we looked at the equatability of enums with associated values. Another problem arises when we want to store enums (maybe as part of the model) on a file or want to send it over a network, let’s say: want to serialize the enum. Although the primitive types as Int and String conform to the NSCoding protocol as well as collections of those types (e.g. Arrays and Dictionaries) enums and structs don’t. And since they are not classes we can also not implement the NSCoding protocol for them. For simple enums we can use its raw values for coding, but that’s not possible for enums with associated values.
Let’s look at the following example:
1 2 3 4 5 6 7 |
enum Happiness1 { // our enum with associated values case Sad case Happy(ofGrade: Int) } let mood1 = Happiness1.Happy(ofGrade: 3) // lets create an instance let data1 = NSKeyedArchiver.archivedData(withRootObject: mood1) // that doesn't work: runtime error |
Again we have our well-known enum “Happiness” and an instance of it. With a primitive type or a collection of primitives we could use the NSKeyedArchiver to create a serialized Data object of the instance, but that doesn’t work for the enum. Funny enough we do not get a compiler error but it fails at runtime:
Error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
There are a lot of proposals in the community on how to serialize structs (e.g. The Red Queen Coder) and this mechanism also works for enums. The idea is to transform the struct (or the enum in our case) to a dictionary of NSCoding compliant types. This dictionary can be given to an NSCoder (i.e. NSKeyedArchiver). It’s convenient to define a protocol for the necessary methods.
1 2 3 4 5 6 |
typealias PropertyList = [String : Any] protocol PropertyListReadable { // this protocol can be applied to any struct or enum func propertyListRepresentation() -> PropertyList init?(propertyListRepresentation:PropertyList?) } |
The first method transforms the enum (or struct) to a dictionary and the new init creates it from the supplied one. The implementation of this protocol is not too complicated:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
enum Happiness2: PropertyListReadable { // lets make our enum conform to "PropertyListReadable" case Sad case Happy(ofGrade: Int) init?(propertyListRepresentation: PropertyList?) { // a new init with an optional dictionary as input guard let values = propertyListRepresentation else {return nil} // did we really get a dictionary? if let happiness = values["happiness"] as? Int { // thats the main value of the enum switch happiness { case 0: // the first case mapped to 0 self = .Sad case 1: // the second case mapped to 1 if let grade = values["grade"] as? Int { // and thats the associated value self = .Happy(ofGrade: grade) } else { return nil } default: // everything else is wrong return nil } } else { return nil } } func propertyListRepresentation() -> PropertyList { // and the encoding part var happiness: Int // lets map the cases of the enum to an Int var representation: PropertyList = [:] // this will be the resulting dictionary switch self { case .Sad: // map first case to 0 happiness = 0 case .Happy(let grade): // map second case to 1 happiness = 1 representation["grade"] = grade // and add the assciated value } representation["happiness"] = happiness // add the main value to the dictionary return representation } } |
Let’s look at the encoding part first. The method propertyListRepresentation() should transform the enum to a dictionary „PropertyList“ ([String : Any]). This dictionary has at least one entry for the main value. I call it here “happiness” which is an Int and maps to the case in the enum. The possibly present associated values in the enum form additional entries in the dictionary. This dictionary can be supplied to an NSCoder since it consists only of primitive types.
The other part is the decoding of an optionally supplied dictionary and creating the desired enum. Of course, this may fail either because there is no dictionary supplied or the dictionary has not the necessary entries. First we look for the main entry in the dictionary “happiness”. It maps to the desired case of the enum. Depending on the case we have to add the possibly present associated values. That’s it. Simple, isn’t it?
Now we can use these methods to supply the enum to an NSCoder:
1 2 3 4 5 6 7 8 |
let mood2 = Happiness2.Happy(ofGrade: 3) // another instance of the enum // and the Data object of the enum let data2 = NSKeyedArchiver.archivedData(withRootObject: mood2.propertyListRepresentation()) if let mood22 = Happiness2.init(propertyListRepresentation: NSKeyedUnarchiver.unarchiveObject(with: data2) as? PropertyList) { Swift.print("\(mood22)") // the decoded enum } |
Everything’s fine now. We do not get a compiler nor does it fail at runtime. The creation of a new enum from the archive gives the same enum as the originally supplied one.
OK, now we can archive and unarchive enums with associated values quite simply but our enums are still not NSCoding compliant since they are not classes. This is more a matter of taste but in this post from Ryuichi Saito there is a proposal to create a nested NSObject/NSCoding class within the struct (or enum in our case). This class takes care of the coding and decoding of the enum (or struct). Let’s take a look for this in our example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
extension Happiness2 { class Coding: NSObject, NSCoding { // add a nested class to the enum that conforms to NSCoding let happiness: Happiness2? // remember the parent enum init(happiness: Happiness2) { self.happiness = happiness // set the parent enum super.init() } required init?(coder aDecoder: NSCoder) { // initialize from a Coder (e.g. NSKeyedUnarchiver) guard let propertyListRepresentation = aDecoder.decodeObject() as? PropertyList else {return nil} // decode the dictionary happiness = Happiness2(propertyListRepresentation: propertyListRepresentation) super.init() // and create the enum from it } public func encode(with aCoder: NSCoder) { // encode the enum guard let happiness = happiness else { return } // just encode the dictionary aCoder.encode(happiness.propertyListRepresentation()) } } } |
Here we use our mechanism with the propertyListRepresantation since we do not want to do the job of coding and decoding twice. It’s convenient to put the coded and decoded vars in protocols and extensions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
protocol Encodable { var encoded: Decodable? { get } } protocol Decodable { var decoded: Encodable? { get } } extension Happiness2: Encodable { var encoded: Decodable? { // the NSCoding compliant object return Happiness2.Coding(happiness: self) } } extension Happiness2.Coding: Decodable { var decoded: Encodable? { // the original enum after decoding the dirctionary return self.happiness } } |
Now we can test the coding and decoding using the NSCoding compliant part of the enum:
1 2 3 4 |
let data3 = NSKeyedArchiver.archivedData(withRootObject: mood2.encoded!) if let mood23 = (NSKeyedUnarchiver.unarchiveObject(with: data3) as? Happiness2.Coding)?.decoded { Swift.print("\(mood23)") } |
This works as well but its not really shorter than the original approach. For me it’s not worthwhile to make this effort and I’ll stick to the propertyListRepresentation.
Here is the playground of this exercise.