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_7.MyGraphNode'

Can anyone explain why the test text is ignored during the deserialization?

Or why I have to add the convenience initializer at all?

Accepted Reply

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, and it needs to be declared something like this:


    private(set) var text: String!


I'm not sure exactly why it crashes without an explicit "init()" method, but I guess Swift takes measures to ensure that classes don't silently inherit the default "init()" from NSObject, which would break Swift's own initialization rules.


I don't think there's much you can do about this situation, since it's valid Obj-C that just doesn't translate well into Swift.

Replies

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, and it needs to be declared something like this:


    private(set) var text: String!


I'm not sure exactly why it crashes without an explicit "init()" method, but I guess Swift takes measures to ensure that classes don't silently inherit the default "init()" from NSObject, which would break Swift's own initialization rules.


I don't think there's much you can do about this situation, since it's valid Obj-C that just doesn't translate well into Swift.