Oct 26, 2018

Archiving Swift Codable Objects

Swift has support for serializing to JSON and property lists, but sometimes you need an archive, and these simple serialization formats won't do. Archives are better for complex object graphs. They contain type information. They can contain Data without round-tripping through a Base-64 string. And they can resolve multiple references to an object. NSKeyedArchiver and NSKeyedUnarchiver support Codable types. But how do you use them? If you're used to using the convenience class methods, you might try something like this:

class CodableClass: Codable {
    var answer: Int
    var name: String
    public init(answer: Int, name: String) {
        self.answer = answer
        self.name = name
    }
}

let myClass = CodableClass(answer: 42, name: "Arthur Dent")
let data = NSKeyedArchiver.archivedData(withRootObject: myClass)
// "NSInvalidArgumentException", "-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x103a6b500"

As commented, this produces a runtime exception because the convenience methods are expecting conformance to NSCoding, and CodableClass doesn't implement NSCoding.

When Codable was released, Foundation added an instance method to NSKeyedArchiver: encodeEncodable(_, forKey:) and one to NSKeyedUnarchiver: decodeDecodable(_, forKey:). To use these, you need to create an instance of NSKeyedArchiver and NSKeyedUnarchiver, respectively.

let myClass = CodableClass(answer: 42, name: "Arthur Dent")
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
do {
    try archiver.encodeEncodable(myClass, forKey: NSKeyedArchiveRootObjectKey)
}
catch {
    fatalError(error.localizedDescription)
}
archiver.finishEncoding()

let unarchiver = NSKeyedUnarchiver(forReadingWith: archiver.encodedData)
unarchiver.decodingFailurePolicy = .setErrorAndReturn
let decoded = unarchiver.decodeDecodable(CodableClass.self, forKey:
                                                    NSKeyedArchiveRootObjectKey)!
unarchiver.finishDecoding()
if let error = unarchiver.error {
    fatalError(error.localizedDescription)
}
else {
    assert(decoded!.name == "Arthur Dent")
    assert(decoded!.answer == 42)
}

As a bonus, since these are Swift methods, they are well-typed. So decodeDecodable(_, forKey:) returns an object of the type you are decoding. In the example above, decoded is of type CodableClass.

The example uses a class, but these methods also work for Swift structs that implement Codable.