Joseph Duffy
Joseph Duffy

Reputation: 4836

Prevent NSKeyedArchiver throwing exception without Objective-C

On iOS version lower than 11 the throwing archivedData(withRootObject:requiringSecureCoding:) is unavailable, so I have tried to do the equivalent on versions less than iOS 11:

let archiveData: NSData
if #available(iOS 11.0, *) {
    archiveData = try NSKeyedArchiver.archivedData(
        withRootObject: rootObject,
        requiringSecureCoding: true
    ) as NSData
} else {
    NSKeyedArchiver.archivedData(withRootObject: userActivity)
    let mutableData = NSMutableData()
    let archiver = NSKeyedArchiver(forWritingWith: mutableData)
    archiver.requiresSecureCoding = true
    archiver.encode(rootObject, forKey: NSKeyedArchiveRootObjectKey)
    if let error = archiver.error {
        throw error
    }
    archiver.finishEncoding()
    archiveData = mutableData
}

However, when the rootObject calls NSCoder.failWithError(_:) in the encode(with:) function an NSInvalidUnarchiveOperationException exception is raised.

If I subclass NSKeyedArchiver as such:

final class KeyedArchiver: NSKeyedArchiver {

    override var decodingFailurePolicy: NSCoder.DecodingFailurePolicy {
        return .setErrorAndReturn
    }

}

It instead raises an NSInternalInconsistencyException exception with the message Attempting to set decode error on throwing NSCoder.

Is there a way to do this kind of archiving without throwing an exception, short of writing an Objective-C function to catch the exception and throwing it as an error?

Upvotes: 0

Views: 591

Answers (1)

Itai Ferber
Itai Ferber

Reputation: 29938

The reason you're still getting the exception at encode time is that the .decodingFailurePolicy is effective only when decoding (i.e., unarchiving via NSKeyedUnarchiver), not encoding. Any object that calls .failWithError(_:) on encode will still produce the exception.

Calling .failWithError(_:) at encode-time is relatively rare: usually, once you have a fully constructed object at runtime, it's not terribly likely that it should be in a state that's not encodable. There are of course cases where this is possible, so you really have two options:

  1. If you're working with objects you know and can check ahead of time whether they're in a valid state to encode, you should do that and avoid encoding invalid objects altogether
  2. If you're working with arbitrary objects which you can't validate up-front, you're going to have to wrap your callout to NSKeyedArchiver via an Objective-C function which can catch the exception (and ideally throw an Error containing that exception, like the newer NSKeyedArchiver API does on your behalf)

Based on your comment above, option 2 is your best bet.


As an aside, you can shorten up your fallback code to avoid having to construct an intermediate NSMutableData instance:

let archiver = NSKeyedArchiver()
archiver.encode(rootObject, forKey: NSKeyedArchiveRootObjectKey)
archiveData = archiver.encodedData

The default initializer on NSKeyedArchiver constructs a new archiver with an internal mutable data instance to use, and NSKeyedArchiver.encodedData property automatically calls -finishEncoding on your behalf.

Upvotes: 1

Related Questions