So, now Swift 4 is here and things are changing. One of the big improvements of Swift 4 is the incorporation of serialisation for classes, structs and enums. In contrast to the previous versions which rely on the old NSObject/NSCoding mechanisms the new Codable protocol is not restricted to classes only. And even better, if you do not have special needs the compiler will synthesise the needed functions for you as we will see later.
There are some nice articles (e.g. by Todd Olsen or Craig Grummitt) which show how to use the new protocols and how to migrate from the old world. I want to update my old ideas for enums with associated values concerning serialisation. So, let’s start over again.
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 39 40 41 42 43 44 45 |
enum CodingError: Error { // enum for coding errors case decoding(String) } enum Happiness: Codable { // our enum is "Codable" case Sad case Happy(ofGrade: Int) private enum CodingKeys: String, CodingKey { // the keys for the coders are not simple strings anymore case Happiness // but enums of Strings (or Ints) case Grade } init(from decoder: Decoder) throws { // our init throws errors let values = try decoder.container(keyedBy: CodingKeys.self) // get the container from the decoder if let happiness = try? values.decode(Int.self, forKey: .Happiness) { // this is the main value of the enum switch happiness { // and look at it case 0: // its the first member of the enum self = .Sad // with no assciated value return // no need to continue case 1: // the second one if let grade = try? values.decode(Int.self, forKey: .Grade) { // decode the associated value self = .Happy(ofGrade: grade) // and set the enum return } default: // should never happen break } } throw CodingError.decoding("Decoding Error: \(dump(values))") // had no success } func encode(to encoder: Encoder) throws { // the encoder thows errors as well var happiness: Int // lets map the cases of the enum to an Int var container = encoder.container(keyedBy: CodingKeys.self) // get an empty container first switch self { // and look at our enum case .Sad: // map first case to 0 happiness = 0 // no associated value to encode case .Happy(let grade): // map second case to 1 happiness = 1 try container.encode(grade, forKey: .Grade) // encode the associated value } try container.encode(happiness, forKey: .Happiness) // and encode the main value of the enum } } |
Bad news first: the compiler does not synthesise the functions needed to conform to the Codable protocol for you in the case of enums. So, we have to make them by ourself. Let’s start with the encoding part. The function encode works quite similar to that of the the old NSCoding protocol. It’s called with an encoder and you have to supply the encoded properties of your class, struct or enum. But now it may throw exceptions if the encoding fails.
We are free how we want to map our enum to a codable structure. I prefer to map the cases of the enum to Ints. In our example this is the variable „happiness“. In addition I encode each possibly present associated value separately (in this example „grade“ for Happiness.Happy). In the above mentioned article each case is encoded uniquely with all its associated values at once. If you have several associated values of diverting types you have to introduce a Codable struct to encode them, for me another unwanted layer of complexity. So, here the associated values are encoded in each case separately and in the end I do it for the main value of the enum. Quite simple, isn’t it?
In contrast to the old NSCoding protocol we cannot encode our values directly with the encoder. We need to get an (empty) container first. The keys for the coding are not simple strings anymore as they were in the old NSCoding protocol but String (or optionally Int) enums conforming to the CodingKey protocol. That enum I have defined private in lines 9 to 12.
Well, now to the decoding part. That is performed in the exceptions throwing init. Its quite the other way round to the encoding part. First I get the filled container from the decoder and then I decode each value separately. I just have to take care if the decoding fails. For that I have introduced the error CodingError. That’s it. Now we can test our Codable enum:
1 2 3 4 |
let mood = Happiness.Happy(ofGrade: 42) let data = try PropertyListEncoder().encode(mood) let mood2 = try PropertyListDecoder().decode(Happiness.self, from: data) Swift.print("\(mood2)") |
Funny enough we cannot give our enum to an NSKeyedArchiver directly; that crashes. Instead we can use the PropertyListEncoder and PropertyListDecoder classes for encoding and decoding the Data object. That we can give to the old class functions:
1 2 3 4 5 |
let data1 = NSKeyedArchiver.archivedData(withRootObject: data) if let data11 = NSKeyedUnarchiver.unarchiveObject(with: data1) as? Data, let mood1 = try? PropertyListDecoder().decode(Happiness.self, from: data11) { Swift.print("\(mood1)") } |
And now come the good news: To make our struct from this post being Codable we have to do almost nothing:
1 2 3 4 5 6 7 8 9 10 |
struct Person: Codable { // thats enough for structs to be Codable let name: String var age: Int var mood: Happiness // as long as all members are Codable } let me = Person(name: "Frank-Peter", age: 60, mood: Happiness.Happy(ofGrade: 5)) let data2 = try PropertyListEncoder().encode(me) let me2 = try PropertyListDecoder().decode(Person.self, from: data2) Swift.print("\(me2)") |
Here is the complete playground of this exercise: