I am trying to send a \"Class\" to my Watchkit extension but I get this error.
* Terminating app due to uncaught exception \'NSInvalidUna
I had a similar situation where my app used my Core
framework in which I kept all model classes. E.g. I stored and retrieved UserProfile
object using NSKeyedArchiver
and NSKeyedUnarchiver
, when I decided to move all my classes to MyApp
NSKeyedUnarchiver
started throwing errors because the stored objects were like Core.UserProfile
and not MyApp.UserProfile
as expected by the unarchiver. How I solved it was to create a subclass of NSKeyedUnarchiver
and override classforClassName
function:
class SKKeyedUnarchiver: NSKeyedUnarchiver {
override open func `class`(forClassName codedName: String) -> Swift.AnyClass? {
let lagacyModuleString = "Core."
if let range = codedName.range(of: lagacyModuleString), range.lowerBound.encodedOffset == 0 {
return NSClassFromString(codedName.replacingOccurrences(of: lagacyModuleString, with: ""))
}
return NSClassFromString(codedName)
}
}
Then added @objc(name)
to classes which needed to be archived, as suggested in one of the answers here.
And call it like this:
if let unarchivedObject = SKKeyedUnarchiver.unarchiveObject(withFile: UserProfileServiceImplementation.archiveURL.path) as? UserProfile {
currentUserProfile = unarchivedObject
}
It worked very well.
The reason why the solution NSKeyedUnarchiver.setClass(YourClassName.self, forClassName: "YourClassName")
was not for me because it doesn't work for nested objects such as when UserProfile
has a var address: Address
. Unarchiver will succeed with the UserProfile
but will fail when it goes a level deeper to Address
.
And the reason why the @objc(name)
solution alone didn't do it for me was because I didn't move from OBJ-C to Swift, so the issue was not UserProfile
-> MyApp.UserProfile
but instead Core.UserProfile
-> MyApp.UserProfile
.
I had to add the following lines after setting up the framework to make the NSKeyedUnarchiver
work properly.
Before unarchiving:
NSKeyedUnarchiver.setClass(YourClassName.self, forClassName: "YourClassName")
Before archiving:
NSKeyedArchiver.setClassName("YourClassName", forClass: YourClassName.self)
I started facing this after the App Name change,
The error I got was - ".....cannot decode object of class (MyOldModuleName.MyClassWhichISerialized) for key....."
This is because code by default saves Archived object with ModuleName prefix, which will not be locatable after ModuleName changes. You can identify the old Module Name from the error message class prefix, which here is "MyOldModuleName".
I simply used the old names to locate the old Archived objects. So before Unarchieving add line,
NSKeyedUnarchiver.setClass(MyClassWhichISerialized.self, forClassName: "MyOldModuleName.MyClassWhichISerialized")
And before Archieving add line
NSKeyedArchiver.setClassName("MyOldModuleName.MyClassWhichISerialized", for: MyClassWhichISerialized.self)
NOTE: While the information in this answer is correct, the way better answer is the one below by @agy.
This is caused by the compiler creating MyApp.Person
& MyAppWatchKitExtension.Person
from the same class. It's usually caused by sharing the same class across two targets instead of creating a framework to share it.
Two fixes:
The proper fix is to extract Person
into a framework. Both the main app & watchkit extension should use the framework and will be using the same *.Person
class.
The workaround is to serialize your class into a Foundation object (like NSDictionary
) before you save & pass it. The NSDictionary
will be code & decodable across both the app and extension. A good way to do this is to implement the RawRepresentable
protocol on Person
instead.
According to Interacting with Objective-C APIs:
When you use the
@objc(
name)
attribute on a Swift class, the class is made available in Objective-C without any namespacing. As a result, this attribute can also be useful when you migrate an archivable Objective-C class to Swift. Because archived objects store the name of their class in the archive, you should use the@objc(
name)
attribute to specify the same name as your Objective-C class so that older archives can be unarchived by your new Swift class.
By adding the annotation @objc(name)
, namespacing is ignored even if we are just working with Swift. Let's demonstrate. Imagine target A
defines three classes:
@objc(Adam)
class Adam:NSObject {
}
@objc class Bob:NSObject {
}
class Carol:NSObject {
}
If target B calls these classes:
print("\(Adam().classForCoder)")
print("\(Bob().classForCoder)")
print("\(Carol().classForCoder)")
The output will be:
Adam
B.Bob
B.Carol
However if target A calls these classes the result will be:
Adam
A.Bob
A.Carol
To resolve your issue, just add the @objc(name) directive:
@objc(Person)
class Person : NSObject, NSCoding {
var name: String!
var age: Int!
// MARK: NSCoding
required convenience init(coder decoder: NSCoder) {
self.init()
self.name = decoder.decodeObjectForKey("name") as! String?
self.age = decoder.decodeIntegerForKey("age")
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.name, forKey: "name")
coder.encodeInt(Int32(self.age), forKey: "age")
}
}