Search code examples
swiftnscodingnskeyedunarchivergameplay-kit

Deserialize subclass of GKGraphNode using NSKeyedUnarchiver


I want to serialize and deserialize an object of my GKGraphNode subclass using NSKeyedArchiver and NSKeyedUnarchiver. So I try the following:

//: Playground - noun: a place where people can play

import GameplayKit

class MyGraphNode: GKGraphNode {
    static let textCodingKey = "TextCodingKey"

    let text: String

    override convenience init() {
        self.init(text: "Default Text")
    }

    init(text: String) {
        self.text = text

        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        text = aDecoder.decodeObject(forKey: MyGraphNode.textCodingKey) as! String

        super.init(coder: aDecoder)
    }

    override func encode(with aCoder: NSCoder) {
        super.encode(with: aCoder)

        aCoder.encode(text, forKey: MyGraphNode.textCodingKey)
    }
}

let text = "Test Text"

let graphNode = MyGraphNode(text: text)

let data = NSKeyedArchiver.archivedData(withRootObject: graphNode)

if let unarchivedGraphNode = NSKeyedUnarchiver.unarchiveObject(with: data) as? MyGraphNode {
    print("Text: \(unarchivedGraphNode.text)")
}

Unfortunately the example prints only the default text and not the expected test text:

Text: Default Text

First I omitted the convenience initializer. But in this case it crashed with this error:

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0). The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

GKGraphNodeSubclass.playground: 5: 7: Fatal error: Use of unimplemented initializer 'init()' for class '__lldb_expr_58.MyGraphNode'

Can anyone explain why the test text is ignored during the deserialization?
Or why I have to add the convenience initializer at all?


Solution

  • I got help in the Apple Developer Forum:

    The "text" property is ending up being reset by "super.init(coder: aDecoder)", presumably because that calls "init()" internally, and that ends up at your convenience "init ()". This would be illegal in Swift, but it's legal in Obj-C, which doesn't have the same strict initialization rules.

    The solution is to initialize "text" after the super.init(coder:), rather than before. This means you can't use a "let" property

    To fix my example I changed the variable declaration and the NSCoding initializer like this:

    var text: String!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    
        text = aDecoder.decodeObject(forKey: MyGraphNode.textCodingKey) as! String
    }