43

I have an iOS 7 application that saves a custom object to app's iCloud Docs folder as a file. For this, I make use of NSCoding protocol.

@interface Person : NSObject <NSCoding>

    @property (copy, nonatomic) NSString *name
    @property (copy, nonatomic) NSString *lastName

@end

Object serialization works perfectly in iOS 7 version of the app:

  1. initWithCoder and encodeWithCoder

  2. [NSKeyedArchiver archivedDataWithRootObject:person]

  3. person = NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)theData]

But I need to move this app to iOS 8, and this class will be coded in swift and 'renamed' for this new iOS 8 version of the app.

class PersonOldVersion: NSObject, NSCoding {
    var name = ""
    var lastName = ""
}

When I try to unarchive the object I got the following error:

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

I already tried renaming swift class 'PersonOldVersion' to original class name ('Person') but still fails.

How can I decode an object which original class isn't available?

JAL
  • 41,701
  • 23
  • 172
  • 300
Aнгел
  • 1,361
  • 3
  • 17
  • 32
  • @Alterecho had same issue [here](http://stackoverflow.com/a/25473229/2785261) but there is no response – Aнгел Aug 25 '14 at 18:38
  • I had a similar situation, check my answer here: https://stackoverflow.com/a/46832840/1433612 – Au Ris Oct 19 '17 at 15:38

5 Answers5

68

This might be another solution, and it's what I did (since I didn't use Swift at that time).

In my case, I archived an object of class "City" but then renamed the class to "CityLegacy" because I created a completely new "City" class.

I had to do this to unarchive the old "City" object as a "CityLegacy" object:

// Tell the NSKeyedUnarchiver that the class has been renamed
[NSKeyedUnarchiver setClass:[CityLegacy class] forClassName:@"City"];

// Unarchive the object as usual
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:@"city"];
CityLegacy *city = [NSKeyedUnarchiver unarchiveObjectWithData:data];
Ferran Maylinch
  • 10,919
  • 16
  • 85
  • 100
  • 1
    This is the solution I needed. However, where do you put the first line? Do you execute it in `encodeWithCoder` and `initWithCoder`? Do you execute them each time? Can I just do a dispatch_once and take care of all my changes somewhere in my root view controller or the app delegate? – Rob Jul 30 '15 at 18:09
  • This is a better answer – the Reverend Mar 11 '16 at 18:04
  • 1
    @Rob the code is executed in that order; configure name-to-class mapping and then (un)archive. By the way, you may need to reconfigure mapping each time if you have want to store both classes with same name. Not recommended, but there may be cases where you are forced to do so. – Ferran Maylinch Mar 11 '16 at 18:34
  • 1
    Great answer. The ``setClass`` method was just what I needed. – Andreas Kraft Mar 21 '16 at 15:33
  • Awesome answer! Didn't know about this method – derpoliuk Mar 28 '16 at 10:44
  • This fixed "cannot decode..." crash after refactoring class name from "fileInfo" to "FileInfo". – Chintan Patel Jun 24 '16 at 06:47
  • Thanks for this answer! Exactly what I needed! I was already afraid that I couldn't rename my legacy class ;) – Buju Apr 16 '19 at 12:53
  • Instead of class got renamed, I just renamed few properties. Do we have anything in unarchive to map property to new name? – Saran Jul 24 '19 at 16:00
27

If the name of the class in Objective-C is important, you need to explicitly specify the name. Otherwise, Swift will provide some mangled name.

@objc(Person)
class PersonOldVersion: NSObject, NSCoding {
    var name = ""
    var lastName = ""
}
newacct
  • 119,665
  • 29
  • 163
  • 224
  • 3
    This also fixed an issue with creating a Today extension and sharing a class between two projects (the app itself and the Today widget). Thanks! – swilliams Oct 15 '14 at 15:59
18

Here's a Swift translation for Ferran Maylinch's answer above.

Had a similar problem in a Swift project after I duplicated the original target in order to have 2 builds of my product. The 2 builds needed to be useable interchangeably.

So, I had something like myapp_light.app and my app_pro.app. Setting the class fixed this issue.

NSKeyedUnarchiver.setClass(MyClass1.classForKeyedUnarchiver(), forClassName: "myapp_light.MyClass1")
NSKeyedUnarchiver.setClass(MyClass1.classForKeyedUnarchiver(), forClassName: "myapp_pro.MyClass1")



if let object:AnyObject = NSKeyedUnarchiver.unarchiveObjectWithFile("\path\...") {
    var myobject = object as! Dictionary<String,MyClass1>
    //-- other stuff here
}
Ferran Maylinch
  • 10,919
  • 16
  • 85
  • 100
nspire
  • 1,567
  • 23
  • 26
  • 1
    This normally happens when you change the class' target name or move it to a framework. Do you know how to reset the class name to the new name after loading it? – João Nunes May 28 '16 at 09:33
5

I think you could resolve it as follow:

@implementation PersonOldVersion

+ (void)load
{
    [NSKeyedUnarchiver setClass:[PersonOldVersion class] forClassName:@"Person"];
}
Shuangquan Wei
  • 121
  • 1
  • 3
1

newacct's answer helped me with the class name, but I also had an issue where I changed a variable name in my Swift class from my Objective-C class. Using @objc() fixed that as well:

Old Class:

@interface JALProgressData : NSObject <NSCoding>

@property (nullable, nonatomic, strong) NSString *platformID;
@property (nonatomic, assign) NSTimeInterval secondsPlayed;
@property (nonatomic, assign) NSTimeInterval totalDuration;
@property (nullable, nonatomic, strong) NSDate *lastUpdated;

@end

New Class:

@objc(JALProgressData)
class VideoProgress: NSObject, NSCoding {
    @objc(platformID) var videoId: String?
    var secondsPlayed: NSTimeInterval?
    var totalDuration: NSTimeInterval?
    var lastUpdated: NSDate?
}

I also realized I was using encodeObject decodeObjectForKey for value types, which was causing issues with my Swift class. Switching to encodeDouble and decodeDoubleForKey for the NSTimeInterval types fixed aDecoder returning nil for those values.

Community
  • 1
  • 1
JAL
  • 41,701
  • 23
  • 172
  • 300