2

My app uses a custom class as its data model:

class Drug: NSObject, NSCoding {
    // Properties, methods etc...
}

I have just created a Today extension and need to access the user’s data from it, so I use NSCoding to persist my data in both the app container and the shared container. These are the save and load functions in the main app:

func saveDrugs() {
    // Save to app container
    let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
    if isSuccessfulSave {
        print("Drugs successfully saved locally")
    } else {
        print("Error saving drugs locally")
    }

    // Save to shared container for extension
    let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
    if isSuccessfulSaveToSharedContainer {
        print("Drugs successfully saved to shared container")
    } else {
        print("Error saving drugs to shared container")
    }
}

func loadDrugs() -> [Drug]? {
    return NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
}

I encountered the problem of class namespacing where the NSKeyedUnarchiver in my Today extension could not decode the object properly, so I used this answer and added @objc before the class definition:

@objc(Drug)
class Drug: NSObject, NSCoding {
    // Properties, methods etc...
}

This solved the problem perfectly. However, this will be version 1.3 of my app, and it seems this breaks the unarchiving process for pre-existing data (as I thought it might).

What is the best way to handle this scenario, as if I just make this change, the new version of the app will crash for existing users!

I cannot find any other answers about this, and I am not sure that the NSKeyedArchiver.setClass() method is relevant, nor am I sure where to use it.

Any help would be gratefully received. Thanks.

Chris
  • 4,009
  • 3
  • 21
  • 52
  • Why does the app crash? – matt Feb 02 '18 at 13:41
  • Hi @matt It crashes as the old Drug class (myApp.Drug) cannot be decoded by NSKeyedUnarchiver once I have added the @objc(Drug) line - the class now becomes *.Drug, which works when sharing between the app and extension, but does not decode old user data. New users would be fine. – Chris Feb 02 '18 at 13:55

1 Answers1

8

This is exactly the use case for NSKeyedUnarchiver.setClass(_:forClassName:) — you can migrate old classes forward by associating the current classes for their old names:

let unarchiver = NSKeyedUnarchiver(...)
unarchiver.setClass(Drug.self, forClassName: "myApp.Drug")
// unarchive as appropriate

Alternatively, you can provide a delegate conforming to NSKeyedUnarchiverDelegate and providing a unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:), but that's likely overkill for this scenario.


Keep in mind that this works for migrating old data forward. If you have newer versions of the app that need to send data to old versions of the app, what you'll need to do is similar on the archive side — keep encoding the new class with the old name with NSKeyedArchiver.setClassName(_:for:):

let archiver = NSKeyedArchiver(...)
archiver.setClassName("myApp.Drug", for: Drug.self)
// archive as appropriate

If you have this backwards compatibility issue, then unfortunately, it's likely you'll need to keep using the old name as long as there are users of the old version of the app.

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • Thank you for this! I only need to unarchive as my Today extension only needs to read data and not write it. Once a user has upgraded the app and loaded their stored data, it will be re-archived in the new way. Just to clarify, will your implementation unarchive both the new (@objc(Drug)) class and also the old one? Do I still need to use @objc? – Chris Feb 02 '18 at 16:02
  • I’m not at my Mac now but I’ll test this later and mark yours as correct answer. Thanks. – Chris Feb 02 '18 at 16:03
  • @Ampoule You'll still need to give the class an `@objc` name to keep it stable between both targets, so keep `@objc(Drug)`. Setting a class for the existing class name is additive — it adds a mapping from `"myApp.Drug"` to `Drug` and will allow you to unarchive `"Drug"` as itself. – Itai Ferber Feb 02 '18 at 16:04
  • I tried this but it won’t compile as I’m trying to use the unArchiveObject method of the unarchiver instance, but it is a static method so needs to be called on NSKeyedUnarchiver. However, how then do I use setClass? Could I do NSKeyedUnarchiver.setClass(...) then NSKeyedUnarchiver.unarchiveObject(...) ? – Chris Feb 02 '18 at 23:51
  • @Chris That's one way to do it. There's the [equivalently-named class function](https://developer.apple.com/documentation/foundation/nskeyedunarchiver/1409718-setclass) that allows you to set it across _all_ instances of `NSKeyedUnarchiver`. So you'd write `NSKeyedUnarchiver.setClass(Drug.self, forClassName: "myApp.Drug")` and then unarchive with `NSKeyedUnarchiver.unarchiveObject(with: ...)`. – Itai Ferber Feb 03 '18 at 00:05
  • Ah I see now. Just checked the Apple documentation and I see there are both class and instance methods. I was clearly only selecting the instance method in XCode. Thanks again! – Chris Feb 03 '18 at 00:07
  • @Chris Happy to have helped.:) – Itai Ferber Feb 03 '18 at 01:35
  • Excuse me sir, where are you putting the unarchiver method? on the class ? or appdelegate?, im talking about this: let archiver = NSKeyedArchiver(...) archiver.setClassName("myApp.Drug", for: Drug.self) – Alejandro Viquez Jan 08 '19 at 23:17
  • 2
    @AlejandroViquez Wherever you are about to actually go ahead and archive or unarchive the data. So, in whichever routines handle either saving the data to disk, or coordinating that. That will vary based on how you have this set up in your app. Alternatively, `NSKeyedArchiver` and `NSKeyedUnarchiver` have class methods to do the same thing (e.g. [`NSKeyedArchiver.setClass(_:forClassName:)`](https://developer.apple.com/documentation/foundation/nskeyedunarchiver/1409718-setclass?language=objc) which affect all archives in the app, and you can call from the AppDelegate or similar – Itai Ferber Jan 09 '19 at 22:51