Search code examples
nskeyedarchivernscoder

How to replace NSKeyedArchiver's initializer init(forWritingWith:) in iOS 12 to encode the metadata of a CKRecord


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.


Solution

  • 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