0

I am having a problem with our approach to data persistence in our app. It was decided to use NSUserDefaults with NSCoding compliant data model, which I disagree with due to the scale of our app.

The problem I'm seeing is when the data model changes, any attempt to deserialized results in a crash. The app must be uninstalled and reinstalled in order to re-serialize.

Scenario:

  1. User installs app
  2. User does stuff.
  3. Developer decides that a property should be added to one of the serialized objects and pushes an update.
  4. User installs update.
  5. App goes 'kaboom'.

This is happening because the data had been serialized with a different model than it is now attempting to be deserialized as.

Example:

class Contact: NSCoding {
    var name
    var address
    var userId
}
... // NSCoding compliance happens next. This object gets serialized.

Someone decides that Contact needs more stuff:

class Contact: NSCoding {
    var name
    var address
    var userId
    var phoneNumber
    var emailAddress
}

Attempting to deserialize the Contact object, even though the NSCoding compliance for encoding and decoding has been updated to load and deserialize, causes

fatal error: unexpectedly found nil while unwrapping an Optional value

Stack Trace

CoreDataManager.unarchiveUser

unarchiveObject

Worker.init

NSCoding

So, my question is, how could we possibly avoid this crash from occurring when running an updated version of the app that has a different schema??

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
Brandon M
  • 349
  • 2
  • 20
  • Are you using Core Data, or not? You said you weren't, but you're using a class called `CoreDataManager`. – Tom Harrington Dec 15 '16 at 20:59
  • You need to post the NSCoding encoding part – Leo Dabus Dec 15 '16 at 21:29
  • http://stackoverflow.com/a/39218020/2303865 – Leo Dabus Dec 15 '16 at 21:31
  • I don't think he is using core data, looks like he is saving it to userdefaults – Leo Dabus Dec 15 '16 at 21:32
  • BTW UserDefaults has a method called `data(forKey:)` – Leo Dabus Dec 15 '16 at 21:34
  • You need to show also your Swift 2.x encoding code – Leo Dabus Dec 15 '16 at 21:35
  • I thought it was odd that this class was named that too. I noticed that CoreData is being imported, but exactly how it's being used, I do not know. What I do know is, the data objects that are being serialized and saved using NSUserDefaults are not CoreData model objects. – Brandon M Dec 15 '16 at 21:42
  • @LeoDabus - The problem is not with encoding, it's with decoding. If I uninstall the app and reinstall it, it works just fine. It encodes like it should. It's when the model changes, and an attempt to deserialize that model is made that a problem occurs. I can go ahead and post the serialize stuff though to pacify. I looked at that link you posted and I'm seeing this: if(aDecoder.containsValue(forKey: "age")){...}, which is something like what I probably should be able to do BUT, since it's a serialized object, I cannot. There's data missing in the serialization. – Brandon M Dec 15 '16 at 21:47
  • @LeoDabus It's encoded with Swift 3 compiled code and attempted to be decoded with Swift 3 compiled code. I'm not sure what purpose showing Swift 2.x encoding would serve. – Brandon M Dec 15 '16 at 21:49
  • The problem is that the old records don't have all properties so all you need is to do as suggested by Tom to use nil coalescing operator to provide a default value to those – Leo Dabus Dec 15 '16 at 21:56
  • try `self.mobilePhoneNumber = decoder.decodeString(forKey: "mobilePhoneNumber") ?? "missing mobilePhoneNumber" self.officePhoneNumber = decoder.decodeObject(forKey: "officePhoneNumber") as? String ?? "missing officePhoneNumber"` – Leo Dabus Dec 15 '16 at 22:09

1 Answers1

4

You're crashing because,

  1. You are attempting to decodeObject(forKey:) on a key that doesn't exist (because it didn't exist on the class when the object was encoded). This method returns nil.
  2. You are using ! to force-unwrap the result of #1.

As a rule of thumb, if your Swift code crashes on a line that contains a !, there's about a 95% chance that the ! is the direct cause of the crash. If the error message mentions unwrapping an optional, it's a 100% chance.

The documentation for the decodeObject(forKey:) method explains that it may return nil. In your case this is guaranteed to happen if you're upgrading from a previous version of the class and you're decoding a key that you just added.

Your code needs to recognize that there might not be a value for the new properties. The simplest fix is replacing as! with as?. Then you'll get a nil value for the new property. For properties that are not optional, you can add something like ?? "default value" to the end.

You could also use the containsValue(forKey:) method to check if a value exists before trying to decode the key.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • if decoder.containsValue(forKey: "mobilePhoneNumber") helped, but let officePhoneNumber: String = decoder.decodeObject(forKey: "officePhoneNumber") as? String ?? "" is better – Brandon M Dec 15 '16 at 22:15
  • 1
    Not really, assuming you care about your classes having valid data. The real solution here is to stop abusing `UserDefaults` and use a real storage system. – Jon Shier Dec 16 '16 at 17:15
  • I completely agree with you @JonShier. I'm pushing to have the system use something better. The team is saying CoreData is too much, but NSUserDefaults is the opposite extreme. I was thinking a wrapper around sqlite3 such as FMDB would be a nice happy medium. – Brandon M Dec 17 '16 at 19:09
  • Perhaps they'd like Realm? It depends on what you need to store. I'd only fallback to SQLite if I needed the performance. Something higher level will likely work better. – Jon Shier Dec 17 '16 at 20:26
  • Realm? I haven't heard of it. Thanks for the suggestion. – Brandon M Dec 19 '16 at 15:07