Search code examples
swiftnskeyedarchivercodable

Use Codable to serialize custom class containing reference loop


I am trying to serialize a custom class containing a reference loop and got it working using NSCoding:

import Foundation

class Person: NSObject, NSCoding {
    let name: String
    weak var parent: Person?
    var child: Person?
    init(name: String) {
        self.name = name
    }
    required init(coder aDecoder: NSCoder) {
        self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
        self.parent = aDecoder.decodeObject(forKey: "parent") as? Person
        self.child =  aDecoder.decodeObject(forKey: "child") as? Person
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(parent, forKey: "parent")
        aCoder.encode(child, forKey: "child")
    }
}

let per = Person(name: "Per")
let linda = Person(name: "Linda")

linda.child = per
per.parent = linda

var people = [Person]()
people.append(linda)
people.append(per)
let encodedData = NSKeyedArchiver.archivedData(withRootObject: people)

let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: encodedData) as! [Person]
myPeopleList.forEach({
    print("\($0.name)\n\t Child: \($0.child?.name ?? "nil")\n\t Parent: \($0.parent?.name ?? "nil")"

    )}) 
// Linda
//     Child: Per
//     Parent: nil
// Per
//     Child: nil
//     Parent: Linda

No I want to do the same using Codable:

import Foundation

class Person: Codable {
    let name: String
    weak var parent: Person?
    var child: Person?
    init(name: String) {
        self.name = name
    }
}

let per = Person(name: "Per")
let linda = Person(name: "Linda")
linda.child = per
per.parent = linda

var people = [Person]()
people.append(linda)
people.append(per)

let archiver = NSKeyedArchiver()
try archiver.encodeEncodable(people, forKey: NSKeyedArchiveRootObjectKey)

But I get the error:

error: Execution was interrupted, reason: EXC_BAD_ACCESS

During the last line. I assume it has to do with the reference loop, because it works if I comment out the line:

per.parent = linda

So can we use Codable to serialize reference loops? If so, how?


Solution

  • You can choose which properties are serialized by overriding Coding Keys (from here)

    e.g. in your case within the class:

    enum CodingKeys: String, CodingKey
    {
        case name
        case child
    }
    

    Only the keys included here should be saved, so no child->parent loop. This does however mean the connection will only exist in one direction when loading, so you will have to re-connect them when loaded.

    FWIW, if you're dealing with 2-way relationships you will be much better off using a database rather than using NSKeyedArchiver for persistence.