Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

User Defaults migration from Obj C to Swift

I am working on an Update for my OS X app which was initially written in Obj-C.
The update has been re written in Swift


I am facing a strange problem of User Defaults handling.(Since User preferences must not be changed in update)

All the native type (like Bool, String) user preferences are working fine, but the the Class which was NSCoding compliant is not able to deserialse / Unarchive. It is giving an error :

Error :[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (KSPerson) for key (NS.objects); the class may be defined in source code or a library that is not linked

I made following try outs, but still not able to figure out the solution

  1. Initially I thought that it was due to different class names(in Swift its Person instead of KSPerson). But changing the class name (back to KSPerson) did not solve the problem
  2. Inorder to make class more Objective C like I also tried prepending the class with @objc

Here is the code

let UD = NSUserDefaults.standardUserDefaults()

func serialize() {
    // Info: personList: [KSPerson]
    let encodedObject: NSData = NSKeyedArchiver.archivedDataWithRootObject(personList)
    UD.setObject(encodedObject, forKey: "key1")
    print("Serialization complete")
}

func deserialise() {
    let encodedObject: NSData = UD.objectForKey("key1") as! NSData
    let personList: [KSUrlObject] = NSKeyedUnarchiver.unarchiveObjectWithData(encodedObject) as! [KSPerson]

    for person in personList {
        print(person)
    }

Note:
I created a duplicate copy of Objective C code, and it deserialised ( what was serialised by original copy) perfectly.
To my surprise. when I rename the class KSPerson to JSPerson in duplicate copy it gave me the exact same error as seen above

So one thing is clear. You need to have same class name to Unarchive NSCoding compliant objects. But this is clearly not sufficient for Swift

like image 941
Kaunteya Avatar asked Dec 22 '15 09:12

Kaunteya


People also ask

How to migrate Objective c project to Swift?

Create a Swift class for your corresponding Objective-C . m and . h files by choosing File > New > File > (iOS, watchOS, tvOS, or macOS) > Source > Swift File. You can use the same or a different name than your Objective-C class.

How does NSUserDefaults work?

NSUserDefaults caches the information to avoid having to open the user's defaults database each time you need a default value. When you set a default value, it's changed synchronously within your process, and asynchronously to persistent storage and other processes.

What is NSUserDefaults in Swift?

A property list, or NSUserDefaults can store any type of object that can be converted to an NSData object. It would require any custom class to implement that capability, but if it does, that can be stored as an NSData. These are the only types that can be stored directly.


1 Answers

Swift classes include their module name. Objective-C classes do not. So when you unarchive the Objective-C class "KSPerson", Swift looks through all its classes and finds "mygreatapp.KSPerson" and concludes they don't match.

You need to tell the unarchiver how to map the archived names to your module's class names. You can do this centrally using setClass(_:forClassName:):

NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "KSPerson")

This is a global registration that applies to all unarchivers that don't override it. You of course can register multiple names for the same class for unarchiving purposes. For example:

NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "KSPerson")
NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "mygreatapp.Person")
NSKeyedUnarchiver.setClass(KSPerson.self, forClassName: "TheOldPersonClassName")

When you write Swift archives, by default it will include the module name. This means that if you ever change module names (or archive in one module and unarchive in another), you won't be able to unarchive the data. There are some benefits to that (it can protect against accidentally unarchiving as the wrong type), but in many cases you may not want this. If not, you can override it by registering the mapping in the other direction:

NSKeyedArchiver.setClassName("KSPerson", forClass:KSPerson.self)

This only impacts classes archived after the registration. It won't modify existing archives.

This still is pretty fragile. unarchiveObjectWithData: raises an ObjC exception when it has a problem, which Swift can't catch. You're going to crash on bad data. You can avoid that using -decodeObjectForKey rather than +unarchiveObjectWithData:

let encodedObject: NSData = UD.objectForKey("key1") as? NSData ?? NSData()
let unarchiver = NSKeyedUnarchiver(forReadingWithData: encodedObject)
let personList : [KSPerson] = unarchiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as? [KSPerson] ?? []

This returns [] if there's a problem rather than crashing. It still respects the global classname registrations.

like image 92
Rob Napier Avatar answered Oct 10 '22 19:10

Rob Napier