swiftnskeyedarchivernscodernskeyedunarchiver

Round-trip encoding and decoding with NSKeyedArchiver and NSKeyedUnarchiver


In the process of implementing init(coder:) for a custom NSView subclass, I came across some strange behavior with NSKeyedArchiver and NSKeyedUnarchiver that I still don't entirely understand. Consider this sample code:

let label = NSTextField(labelWithString: "Test")

// Encode
let data = try NSKeyedArchiver.archivedData(withRootObject: label, requiringSecureCoding: false)

// Decode
try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSTextField

This appears to encode and decode an NSTextField as expected. However, if I try to use decodeTopLevelObject() instead of unarchiveTopLevelObjectWithData(_:), the result is nil:

// Encode
let data = try NSKeyedArchiver.archivedData(withRootObject: label, requiringSecureCoding: false)

// Decode
let decoder = try NSKeyedUnarchiver(forReadingFrom: data)
decoder.decodeTopLevelObject() as? NSTextField // nil

Similarly, if I try to use encodedData instead of archivedData(withRootObject:requiringSecureCoding:), the result is nil:

// Encode
let coder = NSKeyedArchiver(requiringSecureCoding: false)
coder.encodeRootObject(label)
let data = coder.encodedData

// Decode
try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSTextField // nil

The result is even nil if I use encode(_:forKey:) and decodeObject(forKey:):

// Encode
let coder = NSKeyedArchiver(requiringSecureCoding: false)
coder.encode(label, forKey: "label")
let data = coder.encodedData

// Decode
let decoder = try NSKeyedUnarchiver(forReadingFrom: data)
decoder.decodeObject(forKey: "label") as? NSTextField // nil

I'm surprised that the first example above appears to work correctly but none of the others do (especially the last one). Could someone help me understand what's going on here?


Solution

  • If you read the documentation for init(forReadingFrom:) it states:

    This initializer enables requiresSecureCoding by default....

    This has probably been the main source of your confusion. Setting requiresSecureCoding back to false, then, will make the following work:

    /* ENCODING */
    let archiver = NSKeyedArchiver(requiringSecureCoding: false)
    archiver.encodeRootObject(label)  // same as .encode(label)
    archiver.encode(label, forKey: "SOME_CUSTOM_KEY")
    archiver.finishEncoding()  // as per documentation
    let data = archiver.encodedData
    
    /* DECODING */
    let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: data)
    
    // DON'T FORGET THIS!!
    unarchiver.requiresSecureCoding = false
    
    let firstResult = unarchiver.decodeTopLevelObject() as! NSTextField . // same as .decodeObject()
    let secondResult = unarchiver.decodeObject(forKey: "SOME_CUSTOM_KEY") as! NSTextField
    unarchiver.finishDecoding()  // as per documentation
    

    When it comes to encoding and decoding correctly, just make sure you have matching keys. encodeRootObject(_:), which is implemented the same as encode(_:), internally uses the key of nil, so then just call decodeTopLevelObject(), or decodeObject().

    On the other hand, NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:) uses the key NSKeyedArchiveRootObjectKey, so you could technically decode by performing:

    let value = unarchiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as! NSTextField
    

    ...but you wouldn't want to do this, since it's an internal implementation that theoretically could change. Instead you'd just use NSKeyedArchiver.unarchiveTopLevelObjectWithData(_:), as you did in your working example.

    Note: if you are using secure coding, there are other considerations to be made, but I think that's beyond the scope of this question.