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
Swift
its Person
instead of KSPerson
). But changing the class name (back to KSPerson
) did not solve the problemObjective 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
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With