Prevent NSKeyedArchiver throwing exception without Objective-C - swift

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?

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:
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
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.

Related

Catching exceptions when unarchiving using NSKeyedUnarchiver

We've got a Swift class which inherits from NSObject and implements NSCoding. We need to change the name of the class in code and in the archives on disk. Fortunately, we don't need to retain the data. Wiping the cached files and returning default data will be fine. The trouble is detecting when it fails and handling it appropriately.
Ok, to start with the data is written out like this (ignore liberties with force unwrapping etc. it's just to keep the question concise):
let someObject: [Int: MyClass] = ...
let data = NSKeyedArchiver.archivedData(withRootObject: someObject)
try! data.write(to: someUrl, options: .atomic)
This works fine. Our existing method of decoding is this (again, our code is safer in practice):
let someObject = NSKeyedUnarchiver.unarchiveObject(withFile: someUrl.path)! as! [Int: MyClass]
Now, when we rename our class, we are going to have to deal with the cases where it fails to decode. With NSKeyedUnarchiver, you can set a delegate which will have a method called if decoding fails. Before changing it, I wanted to re-write our existing decoder to make sure it can decode as is before I make any changes. Unfortunately, it can't. Here is what we've got:
let fileData = fileManager.contents(atPath: fileUrl.path)!
let unarchiver = NSKeyedUnarchiver(forReadingWith: fileData)
guard let myObject = try! unarchiver.decodeTopLevelObject() as? [Int: MyClass] else {
return [Int: MyClass]()
}
Now, I didn't expect any issues with this. However, the call to decodetopLevelObject() fails every time, simply returning nil (it's definitely not the cast that's the problem). I have no idea why. The documentation for this is effectively non-existent. There is a similar call which is decodeObject(), but that also fails for no obvious reason. I've tried setting the delegate and implementing the method, but it never gets called.
Where are we going wrong?
Try using decodeObject(forKey:) with NSKeyedArchiveRootObjectKey or similar API.

Foundation._GenericObjCError.NilError from core data batch delete

I am trying to use batch delete feature of core data. I have an entity named Car. That entity has a column name modelNumber as Int. I want to delete all cars which has modelNumber older than 2000. Here is my code:
func deleteCarsOlderThan(modelNumber: Int) {
let predicate = NSPredicate(format: "modelNumber <= %#", NSNumber(int: modelNumber))
let fetchRequest = NSFetchRequest(entityName: "Car")
fetchRequest.predicate = predicate
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .ResultTypeCount
do {
let result = try self.fhirManagedObjectContext.executeRequest(deleteRequest)
try self.fhirManagedObjectContext.save()
}
catch {
print(error)
}
}
While executing this code, control goes to catch block and it gives an error says: Foundation._GenericObjCError.NilError. My fetch request is working well as if I use:
let olderCars = self.executeFetchRequest(fetchRequest)
it returns me an array of older cars. I don't know where I am doing wrong.
I am using iOS9 for this purpose.
TL;DR: While self.fhirManagedObjectContext is non-optional, it's probably returning nil from Objective-C.
The error you observed is generated by Swift's Foundation bridging runtime. (See the source code here.) This occurs when an Objective-C method with an error pointer returns a failure value (NO or nil), but no actual error was passed back via the NSError pointer. This could either be the result of a bug in Core Data or, more likely, a nil managed object context that when using Objective-C method dispatch causes the method to appear to return NO.

initWithDictionary: in Objective-C and Swift

In Objective-C we can use object mapping with json response like this way
PolicyData *policyData = [[PolicyData alloc] initWithDictionary:responseObject error:&err];
Here we can map responseObject with PolicyData class properties.
How i can do the same in Swift?
It should be as easy as adding a bridging header (because PolicyData is likely written in Objective-C). Instructions on how to do this can be seen in this Apple documentation.
Then you can create that PolicyData object as easily as doing:
do {
let newPolicyDataObject = try PolicyData(responseObject)
} catch error as NSError {
print("error from PolicyData object - \(error.localizedDescription)")
}
This assumes your responseObject is a NSDictionary. And Swift 2 helpfully (?) turns error parameters into try/catch blocks.
That is, PolicyData's
- (instancetype) initWithDictionary:(NSDictionary *)responseObject error:(NSError *)err;
declaration magically becomes
func initWithDictionary(responseObject : NSDictionary) throws
as described in the "Error Handling" section of this Apple Objective-C/Swift interoperability doc.
You can add a
convenience init?(dictionary: NSDictionary)
to any object you want to initialize from a dictionary and initialize it's properties there.
Yet, as swift does no dynamic dispatching (sooner or later), you may not generalize that to expect the properties' names to be keys in the dictionary for any object.

NSURLSession crashing from Swift

I've got a simple class that uses an NSURLSession.
class test {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration());
func f() {
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0), { () -> Void in
var task = self.session.dataTaskWithRequest(request, completionHandler: { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
if error != nil {
// cry
return;
}
var error: NSError? = nil;
var dict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &error) as! Dictionary<String, String>;
// use dict
});
task.resume();
});
}
When I try to deserialize the data as JSON, the application crashes.
I've determined that the data seems to be of the right length but the content looks like garbage in the debugger, so it seems to me that the data object passed in is broken. Furthermore, I suspect some stack smashing as I can step through this completion handler and see that it's the attempt to deserialize the data that's crashing, but when the actual crash occurs, the stack in the debugger mentions nothing about the completion handler or any of my code.
I've seen several samples of using NSURLSession that look pretty much exactly like mine that just work. I tried using the shared session instead of making a new one, but that did not help either.
What is causing this crash?
Seems that the JSON was not actually all strings- I brainfarted and one of them was actually a number.
The real problem in the question is that Swift is completely worthless when handling the problem of force casts failing. Not only do you not get any kind of useful error at runtime that you could handle or recover from, but the debugging information presented when it occurs is completely misleading and points to totally the wrong place. You simply get a trap in a function without symbols with the wrong callstack.

When decoding an object from NSCoder, what's the best way to abort?

I'm decoding a custom object from a cached serialization. I've versioned++ the object since it was encoded, and if the serialized version is an old version I want to just throw it away. I was under the impression that I could just return nil from my initWithCoder: method and all would be well. That's throwing an error though.
EDIT - Per NSObject's +version: documentation "The version number applies to NSArchiver/NSUnarchiver, but not to NSKeyedArchiver/NSKeyedUnarchiver. A keyed archiver does not encode class version numbers.". I'm using a keyed archiver so that approach won't help.
I am using my own embedded version number, not the NSObject version + NSCoder's versionForClassName:. Is that the way to go about this? Will NSCoder automatically abort attempting to decode if the Class versions don't match, or do I need to do this check manually.
I'm currently doing this:
- (id)initWithCoder:(NSCoder *)coder {
if (![coder containsValueForKey:#"Class.User.version"]) {
// Version information missing
self = nil;
return nil;
}
NSInteger modelVersion = [coder decodeIntForKey:#"Class.User.version"];
if (modelVersion < USER_MODEL_VERSION) {
// Old user model, discard
self = nil;
return nil;
}
...
}
and I'm getting this error when it tries to decode old versions
-[__NSPlaceholderDictionary initWithObjects:forKeys:]: number of objects (0) not equal to number of keys (46)'
This all seems odd because initializers can fail for any number of reasons, and the pattern is to return nil in that case, so should this really crash a decoder?
EDIT - Per Apple's documentation it sounds like I'm approaching this the right way. There does not appear to be, however, a mechanism for completely aborting decoding if an object is old and out of date. I don't have or want an upgrade path from the old object, so what do I do?
instead of setting self to nil, set all of the individual properties that you decode to nil, then return self. So, write this:
if (modelVersion < USER_MODEL_VERSION) {
// Old user model, discard
object1 = nil;
object2 = nil;
...
object46 = nil;
}
return self;
This will essentially return an object of the right type and all, but all properties will be nil.