16

In iOS 12, the NSKeyedArchiver's initializer init(forWritingWith:) was deprecated. Xcode 10 recommends replacing it with the new initializer init(requiringSecureCoding:). The problem is that this initializer only sets the value of the requiresSecureCoding property of the NSCoder object, but it doesn't set the NSMutableData object that will contain the encoded data. The following is the original code propose by Apple to encode the metadata of a CKRecord (CloudKit record):

let data = NSMutableData()
let coder = NSKeyedArchiver.init(forWritingWith: data)
coder.requiresSecureCoding = true
record.encodeSystemFields(with: coder)
coder.finishEncoding()

The encodeSystemFields method of the CKRecord class requires an NSKeyedArchiver object (an NSCoder subclass) and the encoded data is stored in the NSMutableData object associated with this object. If I replace the init(forWritingWith:) initializer by the init(requiringSecureCoding:) initializer, I get an NSKeyedArchiver object, but this object is not associated with any NSMutableData object and therefore I don't get the record's metadata. I'm not sure how to complete the code to get the data produced by the NSKeyedArchiver object into an NSMutableData object.

Mikiko Jane
  • 463
  • 6
  • 13

1 Answers1

26

For a few releases now NSKeyedArchiver has had an encodedData method which calls -finishEncoding on the archiver and returns to you the finalized encoded data that the archiver has created. This was how you would get the finished data when initializing via -[NSKeyedArchiver init]:

let coder = NSKeyedArchiver()
/* encode stuff */
let data = coder.encodedData

This obviated the need for the NSMutableData variant, and with this update, the mutable data variant has been deprecated, favoring the newer paradigm. So rather than writing

let data = NSMutableData()
let coder = NSKeyedArchiver.init(forWritingWith: data)
coder.requiresSecureCoding = true
record.encodeSystemFields(with: coder)
coder.finishEncoding()

you'd write

let coder = NSKeyedArchiver(requiringSecureCoding: true)
record.encodeSystemFields(with: coder)
let data = coder.encodedData

The manual assignment to .requiresSecureCoding and the manual finishEncoding() call are both no longer necessary.


Note this dance is only necessary when calling CKRecord.encodeSystemFields(with:), which explicitly takes an NSCoder in order to encode only a subset of itself. In the general case of encoding an object, you would go with the new NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:) method:

let data = try NSKeyedArchiver.archivedData(withRootObject: /* object */, requiringSecureCoding: true)

which is equivalent to

let coder = NSKeyedArchiver(requiringSecureCoding: true)
coder.encodeObject(/* object */, forKey: NSKeyedArchiveRootObjectKey)
let data = coder.encodedData
Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • Re: your "root object" equivalent, any idea why if I immediately decode that (to create a copy) I would get error `-decodeValueOfObjCType:at:size: only defined for abstract class. Define -[NSKeyedArchiver decodeValueOfObjCType:at:size:]!`? For example: `let copy = coder.decodeObject()` – David James Oct 22 '18 at 07:19
  • EDIT: (trying to copy a subclass of `UIView`) – David James Oct 22 '18 at 07:32
  • @DavidJames Based on the error it looks like you're trying to decode from an `NSKeyedArchiver` (which does only encoding) when you should be using an `NSKeyedUnarchiver`. If you can share more of your code it might be easier to point out. – Itai Ferber Oct 22 '18 at 14:47
  • Yes, that was it! Thank you. New, working code: `let copy = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(NSKeyedArchiver.archivedData(withRootObject:view, requiringSecureCoding:false))` – David James Oct 22 '18 at 15:03