Search code examples
iosswiftios-app-extensionnskeyedarchivernskeyedunarchiver

NSKeyedArchiver and sharing a custom class between targets


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.


Solution

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