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.
Related
What is the difference between:
guard let json_data = Data(contentsOf: path) else {return nil}
and
let json_data = try? Data(contentsOf: path)
I dont want to use optional while loading the data into the variable. I want other ways to try it.
Thanks in advance.
The options are:
Your first example, unwrapping it with guard, is missing a try?:
func foo() -> Bar? {
guard let jsonData = try? Data(contentsOf: path) else { return nil }
// if you get here, `jsonData` is not `Optional`
…
}
This will safely unwrap your optional and let you do whatever you want if the unwrapping failed. (In your example, you are returning nil.)
Your second example, yields an optional, which you presumably need to unwrap with an if statement.
func foo() -> Bar? {
let jsonData = try? Data(contentsOf: path)
// jsonData is `Optional` in this example
if let jsonData {
…
} else {
return nil
}
}
We would generally favor the first option over this, where the “early exit” of the guard makes it a little easier to read the code, but there are cases where you might use this pattern.
An option that hasn’t been considered is to actually throw the error (using try instead of try?):
func foo() throws -> Bar {
let jsonData = try Data(contentsOf: path)
// `jsonData` is not `Optional`
…
}
Now, this only passes the buck of handling the error to the caller (i.e., a do-catch block). But it does have a few virtues over the prior two examples, namely that (a) the useful information of the error object is not just discarded, thereby making it easier to diagnose problems during the development process; and (b) you don’t have to return an optional.
Yet another option (to be used only if you know that this will always succeed, e.g., you are reading a well-known file from your bundle that you know must always succeed) is try!, a “force-try”:
func foo() -> Bar {
let jsonData = try! Data(contentsOf: path)
// `jsonData` is not `Optional`
…
}
Now, this will crash if the Data(contentsOf:) can ever fail, so only use this in scenarios where you know that this is impossible.
Personally, I would generally favor option 3 (where I capture what went wrong) in cases where the Data(contentsOf:) might ever plausibly fail at runtime, and I might consider option 4 (where it crashes with a meaningful error message) when I know it is impossible for it to ever fail at runtime. That having been said, more than once I found myself using option 4 and I later regretted not using option 3, simply because there was some weird edge-case that I neglected to consider.
In short, nowadays I tend to defensively catch errors, log the full error in the console and show a nice localized message in the UI (i.e., option 3). I almost never use try?, because if something can fail, it’s rarely a good idea to discard the useful diagnostic information.
It will be better to be optional once you are loading the data to avoid application crash in and problems in case there was no data there. This is consider as a safe feature.
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.
Im making a Fitness app to learn Core data, and I have found that I need to let the user store every performed workout as a WorkoutLog item, and in there, there should be a series of ExerciseLogs which represents performances of that exercise (it contains each lift and also a reference to the actual exercise design).
Problem is that after a while i realize that i need to have these ordered, so that the next time i want to show the user their workout, the order that the exercisese were performed should be the same.
So I checked "ordered" in the top right of the image there, and now my code is in dire need of an update. I have tried to read as much as I could about working with NSOrderedSet and how to fetch them from core data and then manipulate them, but I havent really found much of use to me. (I have no experice in objective-c)
For example my code that used to be:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
guard let exerciseLogsToDelete = workoutLogToDelete.loggedExercises as? Set<ExerciseLog> else {
print("error unwrapping logged exercises in deleteWorkoutLog")
return
}
I get the error: .../DatabaseFacade.swift:84:77: Cast from 'NSOrderedSet?' to unrelated type 'Set' always fails
So what ive learned about sets and core data no longer seems applicable.
Im far from an expert in programming, but im very eager to learn how to get access to the loggedExercises instances.
TLDR; Is there a way to cast NSOrderedSet to something I can work with? How do we usually work with NSManagedSets from core data? Do we cast them to Arrays or MutableSets? I would very much appreciate an example or two on how to get started with retrieving and using these ordered sets!
Thanks
For anyone else wondering how to get started with orderedSets in core data:
After setting my the WorkoutLog.loggedExercises "to-many" relationship to be ordered, I managed to access them through the mutableOrderedSetValue function like this:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
let orderedExerciseLogs: NSMutableOrderedSet = workoutLogToDelete.mutableOrderedSetValue(forKey: "loggedExercises")
let exerciseLogsToDelete = orderedExerciseLogs.array
for exerciseLog in exerciseLogsToDelete {
guard let exerciseLog = exerciseLog as? ExerciseLog else {
return
}
Works great so far.
And to rearrange the NSOrderedSet I ended up doing something like this:
// Swap the order of the orderedSet
if let orderedExerciseLogs: NSOrderedSet = dataSourceWorkoutLog.loggedExercises {
var exerciseLogsAsArray = orderedExerciseLogs.array as! [ExerciseLog]
let temp = exerciseLogsAsArray[indexA]
exerciseLogsAsArray[indexA] = exerciseLogsAsArray[indexB]
exerciseLogsAsArray[indexB] = temp
let exerciseLogsAsOrderedeSet = NSOrderedSet(array: exerciseLogsAsArray)
dataSourceWorkoutLog.loggedExercises = exerciseLogsAsOrderedeSet
}
I was previously using RxSwift and I decided I did not want to use it anymore and was able to convert everything over to Bond which I am much more familiar with. Since the new changes though to Bond v5, I cannot seem to figure out how to observe values in UserDefaults. The following code ends up giving me a fatal error.
userDefaults.reactive
.keyPath(LocationManager.HomeLocationKey, ofType: String.self, context: .immediateOnMain)
.map(self.initLocation(from:))
.bind(to: self.homeLocation)
userDefaults is a reference to UserDefaults.standard and LocationManager.HomeLocationKey is a string. I am providing the initLocation function below as I know it will be asked for. Below that function I will post the error that I am receiving after the app starts up.
func initLocation(from string: String?) -> Location?
{
guard let dataString = string
else { log.warning("Location data did not exist, returning nil"); return nil }
let json = JSON.parse(dataString)
return Location(from: json)
}
Error:
fatal error: Could not convert nil to String. Maybe `dynamic(keyPath:ofExpectedType:)` method might be of help?): file /Users/sam/Documents/iOS Apps/Drizzle/Pods/Bond/Sources/Shared/NSObject+KVO.swift, line 58
It might not be obvious, but if the observed value can be nil, the ofType argument must be an Optional type. In your case, that would be:
userDefaults.reactive
.keyPath(LocationManager.HomeLocationKey, ofType: Optional<String>.self, context: .immediateOnMain)
...
Return Value Returns the newly initialized data detector. If an error
was encountered returns nil, and error contains the error.
That is all the apple docs say about NSDataDetector and it's init.
Is it only wrapped in a do try catch because you might feed it a set of NSTextCheckingType that will fail the init or does it have some dependencies that might make it fail?
Or to put it another way:
If an init works once, will it always work and can I then use try!
This works:
guard let detector = try? NSDataDetector(types: NSTextCheckingType.Date.rawValue) else {
return
}
So will this always work:
let detector = try! NSDataDetector(types: NSTextCheckingType.Date.rawValue)
Not having to deal with optionals would be great in some cases.
Of course the class might one day change and then it might fail. So this question is just about NSDataDetector as it exists today.
Update :
NSDataDetector and all NSTextCheckingType's are available from iOS 4. This makes me think that once an init works, it will always work.
next thing I will check:
Are all init's, or all functions, that accept an OptionSetType fallible?