Chris Frederick
Chris Frederick

Reputation: 5584

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?

Upvotes: 2

Views: 1637

Answers (1)

Daniel
Daniel

Reputation: 461

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.

Upvotes: 3

Related Questions