In my previous post I’ve explained that the new Codable protocol of Swift 4 has some problems with references to objects (i.e. class instances). Or, better said, the two implementations of coders available
- ProperListEncoder/PropertyListDecoder
- JSONEncoder/JSONDecoder
have this problem: they expand all references to new objects and create doublets if references in the object tree occur more than once for a single object. In the case of loops in the object tree (references of objects to each other) the encoding leads to an endless loop.
The old NSKeyedArchiver/NSKeyedUnarchiver of the Objective C-world do not have this problem: they respect references and restore them correctly at unarchiving. So, I want to implement this old way, i.e. the NSCoding protocol in my LinkedList as well. Remark: LinkedList uses references to its payload as well even in loops but that is taking care of even in the use of the Coding protocol.
Bad new first: To use the NSCoding protocol our LinkedList class has to be derived from NSObject. That makes some changes necessary:
- Our nice debug printout „CustomStringConvertible“ can’t be declared in an extension. Again, the compiler gives a not very helpful error message:
Members of extensions of generic classes cannot be declared @objc
We have to put it in the main definition of our class:
1 2 3 4 5 6 7 8 9 10 |
override public var description: String { var text = "[" var node = head while node != nil { text += "\(node!.value)" node = node!.next if node != nil { text += ", " } } return text + "]" } |
NSObject is obviously already „CustomStringConvertible“.
- For the same reason we cannot implement the NSCoding protocol in an extension; we have to put it in the main definition of the class.
- The default init() must call super.init():
1 2 3 |
override init() { // just needed to add further inits super.init() // let super do its job } |
The NSCoding protocol is for classes only but we want to carry structs or enums as well as payload in our LinkedList (i.e. as the generic type T).
In this post I’ve shown how to encode them as well with the NSKeyedArchiver. So that we can use here as well.
1 2 3 4 5 6 |
typealias PropertyList = [String : Any] protocol PropertyListReadable { func propertyListRepresentation() -> PropertyList init?(propertyListRepresentation:PropertyList?) } |
Now let’s implement the NSCoding protocol for our LinkedList. First let’s look at the encoding part:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
final public class LinkedList<T>: NSObject, NSCoding, Sequence { // the linked list, finally, conforming to the sequence and the codable protocol … public func encode(with aCoder: NSCoder) { // archive our list var next = head // start at the top if T.self is PropertyListReadable.Type { // our objects are PropertyListReadable (i.e. structs or enums) var list = [PropertyList]() // create an array of PropertyLists while next != nil { // and iterate over the whole list if let item = next!.value as? PropertyListReadable { // each item is PropertyListReadable list.append(item.propertyListRepresentation()) // append the propertyListRepresentation } next = next!.next // and get the next item } aCoder.encode(list) // finally let the Encoder encode the resulting array } } |
We loop through our LinkedList and put the propertyListRepresentation of each node in an array. Finally we encode this array. Very straightforward. Now the other way round:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
required public init?(coder aDecoder: NSCoder) { // create the list from an archive (NSCoding) super.init() // let super do its job if T.self is PropertyListReadable.Type { // the objects are PropertyListReadable (i.e. structs or enums) if let list = aDecoder.decodeObject() as? [PropertyList] { // lets decode the list as an array of ProperList for object in list { // and iterate over all elements if let item = (T.self as? PropertyListReadable.Type)?.init(propertyListRepresentation: object) { self.append(value: item as! T) // initialize the struct or enum and append it to the list } } } } } |
If our generic type T is PropertyListReadable we decode the array of PropertyList. And from this array we create our LinkedList. Couldn’t be simpler. Now let’s test it.
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 |
struct PersonPLR: PropertyListReadable { var name: String var age: Int init(name: String, age: Int) { self.name = name self.age = age } init?(propertyListRepresentation: PropertyList?) { guard let values = propertyListRepresentation else {return nil} if let name = values["name"] as? String, let age = values["age"] as? Int { self.name = name self.age = age } else { return nil } } func propertyListRepresentation() -> PropertyList { let representation: PropertyList = ["name" : self.name as Any, "age" : self.age as Any] return representation } } |
This is a simple struct and I made it PropertyListReadable as described in this post.
1 2 3 4 5 6 7 8 |
let mePLR = PersonPLR(name: "Frank-Peter", age: 62) let personArrayPLR = [mePLR, mePLR] let personListPLR = LinkedList<PersonPLR>(personArrayPLR) print(personListPLR) let dataPLR = try NSKeyedArchiver.archivedData(withRootObject: personListPLR, requiringSecureCoding: false) let personListPLR2 = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(dataPLR) as! LinkedList<PersonPLR> print(personListPLR2) |
Here I’ve created a person (me) and put it twice in a LinkedList. That’s encoded with NSKeyedArchiver and finally decoded as well. The two LinkedList look similar:
[PersonPLR(name: "Frank-Peter", age: 62), PersonPLR(name: "Frank-Peter", age: 62)]
[PersonPLR(name: "Frank-Peter", age: 62), PersonPLR(name: "Frank-Peter", age: 62)]
Now to the next challenge. We want to encode classes using the Codable protocol of Swift 4. First let’s change the Person struct to a class:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class PersonCodable: Codable, CustomStringConvertible { var name: String var age: Int init(name: String, age: Int) { self.name = name self.age = age } public var description: String { let text = "PersonCodable(name: \"\(name)\", age: \(age))" return text } } |
The serialisation is done in an extension as shown in the previous post:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
extension LinkedList: Codable where T: Codable { public convenience init(from decoder: Decoder) throws { var values = try decoder.unkeyedContainer() // get the container from the decoder self.init() if let list = try? values.decode([T].self) { // and decode the array for object in list { // iterate over all elements self.append(value: object) // and append it to our list } } else { throw CodingError.decoding("Decoding Error: \(dump(values))") // had no success } } public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() // get an empty container first var next = head // start at the top var list = [T]() // create an array of the objects while next != nil { // iterate over the whole list list.append(next!.value) // and append our objects next = next!.next // get the next item } try container.encode(list) // and encode the list } } |
Let’s test that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let meCodable = PersonCodable(name: "Frank-Peter", age: 62) let personArrayCodable = [meCodable, meCodable] let personListCodable = LinkedList<PersonCodable>(personArrayCodable) print(personListCodable) var identical = personListCodable[0] === personListCodable[1] ? "===" : "!===" print("[0] \(identical) [1]") let dataCodable = try PropertyListEncoder().encode(personListCodable) let personListCodable2 = try PropertyListDecoder().decode(LinkedList<PersonCodable>.self, from: dataCodable) print(personListCodable2) identical = personListCodable2[0] === personListCodable2[1] ? "===" : "!===" print("[0] \(identical) [1]") |
Again, I put myself twice in a LinkedList. Since the payload of the LinkedList is a class type the objects are put in by reference; the two objects in the LinkedList are identical.
Since I do not have loops in my object graph encoding does not lead to a disaster. But as you can see in the last test on decoding I get two separate objects of myself in the LinkedList:
[PersonCodable(name: "Frank-Peter", age: 62), PersonCodable(name: "Frank-Peter", age: 62)]
[0] === [1]
[PersonCodable(name: "Frank-Peter", age: 62), PersonCodable(name: "Frank-Peter", age: 62)]
[0] !=== [1]
Members 0 and 1 are not identical any more! If you do not care you still may use the Codable protocol as long as you do not have loops in the object graph, i.e. as long as not two objects point to each other directly or indirectly.
Well, and now let’s add support for class types in the NSCoding methods of our LinkedList. As usual first look at the encoding part:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public func encode(with aCoder: NSCoder) { // archive our list var next = head // start at the top if T.self is PropertyListReadable.Type { // our objects are PropertyListReadable (i.e. structs or enums) var list = [PropertyList]() // create an array of PropertyLists while next != nil { // and iterate over the whole list if let item = next!.value as? PropertyListReadable { // each item is PropertyListReadable list.append(item.propertyListRepresentation()) // append the propertyListRepresentation } next = next!.next // and get the next item } aCoder.encode(list) // finally let the Encoder encode the resulting array } else if T.self is NSCoding.Type { // instead its a NSCoding compliant class var list = [T]() // create an array of the objects while next != nil { // iterate over the whole list list.append(next!.value) // and append our objects next = next!.next // get the next item } aCoder.encode(list) // finally let the Encoder encode the resulting array } } |
First we test against the PropertyListReadable type to encode structs and enums. If that’s not the case we look if the generic type T is NSCoding compatible. If that’s the case we transform the LinkedList to an array and encode that array. If neither is true, we can’t proceed in encoding.
The encoding part is very much the other way round:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
required public init?(coder aDecoder: NSCoder) { // create the list from an archive (NSCoding) super.init() // let super do its job if T.self is PropertyListReadable.Type { // the objects are PropertyListReadable (i.e. structs or enums) if let list = aDecoder.decodeObject() as? [PropertyList] { // lets decode the list as an array of ProperList for object in list { // and iterate over all elements if let item = (T.self as? PropertyListReadable.Type)?.init(propertyListRepresentation: object) { self.append(value: item as! T) // initialize the struct or enum and append it to the list } } } } else if T.self is NSCoding.Type { // instead its a NSCoding compliant class if let list = aDecoder.decodeObject() as? [T] { // let it decode directly to an array of objects for object in list { // iterate over all elements self.append(value: object) // and append it to our list } } } } |
If the generic type T is NSCoding compatible we decode to an array of T and form the LinkedList from that array. Very straightforward. Now let’s test it.
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 |
class PersonNSCoding: NSObject, NSCoding { var name: String = "" var age: Int = 0 init(name: String, age: Int) { super.init() self.name = name self.age = age } required init?(coder aDecoder: NSCoder) { super.init() self.name = aDecoder.decodeObject(forKey: "name") as! String self.age = aDecoder.decodeInteger(forKey: "age") } func encode(with aCoder: NSCoder) { aCoder.encode(self.name, forKey: "name") aCoder.encode(self.age, forKey: "age") } override public var description: String { let text = "PersonNSCoding(name: \"\(name)\", age: \(age))" return text } } let meNSCoding = PersonNSCoding(name: "Frank-Peter", age: 62) let personArrayNSCoding = [meNSCoding, meNSCoding] let personListNSCoding = LinkedList<PersonNSCoding>(personArrayNSCoding) print(personListNSCoding) let dataNSCoding = try NSKeyedArchiver.archivedData(withRootObject: personListNSCoding, requiringSecureCoding: false) let personListNSCoding2 = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(dataNSCoding) as! LinkedList<PersonNSCoding> print(personListNSCoding2) identical = personListNSCoding2[0] === personListNSCoding2[1] ? "===" : "!===" print("[0] \(identical) [1]") |
We create a new Person class being NSCoding compatible. Again, we put myself twice in a LinkedList of that type and let that encode by the old NSKeyedArchiver. After decoding of the resulting data object we test against identity of the two members of the LinkedList. And as you can see they are identical. The NSKeyedArchiver/NSKeyedUnarchiver duo respect the correct object relationships:
[PersonNSCoding(name: "Frank-Peter", age: 62), PersonNSCoding(name: "Frank-Peter", age: 62)]
[PersonNSCoding(name: "Frank-Peter", age: 62), PersonNSCoding(name: "Frank-Peter", age: 62)]
[0] === [1]
Here is the complete playground of this post.