Cannot decode object of class

匿名 (未验证) 提交于 2019-12-03 01:20:02

问题:


I am trying to send a "Class" to my Watchkit extension but I get this error.

* Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '* -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyApp.Person)

Archiving and unarchiving works fine on the iOS App but not while communicating with the watchkit extension. What's wrong?

InterfaceController.swift

    let userInfo = ["method":"getData"]      WKInterfaceController.openParentApplication(userInfo,         reply: { (userInfo:[NSObject : AnyObject]!, error: NSError!) -> Void in              println(userInfo["data"]) // prints 

AppDelegate.swift

func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,     reply: (([NSObject : AnyObject]!) -> Void)!) {          var bob = Person()         bob.name = "Bob"         bob.age = 25          reply(["data" : NSKeyedArchiver.archivedDataWithRootObject(bob)])         return } 

Person.swift

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")     } } 

回答1:

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.



回答2:

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")     } } 


回答3:

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) 


回答4:

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.



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!