Unarchiving encoded data returning nil and incorrect format

Viewed 6876

I have added category methods to NSUserDefaults to store and retrieve encoded objects (in this case, an NSArray). I am having a problem retrieving the encoded data. Here is my code:

- (void)encodeObject:(id<NSCoding>)object forKey:(NSString *)key {
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object requiringSecureCoding:NO error:nil];
    [self setObject:data forKey:key];
    [self synchronize];
}

- (id)decodeObjectForKey:(NSString *)key class:(Class)aClass {
    NSData *data = [self objectForKey:key];
    NSError *error = nil;
    id object = [NSKeyedUnarchiver unarchivedObjectOfClass:aClass fromData:data error:&error];
    NSLog(@"E: %@ %@", error.localizedDescription, object);
    return object;
}

Calling [[NSUserDefaults standardDefaults] encodeObject:object forKey:key] should encode the object passed and store it in defaults, and then calling [[NSUserDefaults standardDefaults] decodeObjectForKey:key class:aClass should return the encoded object.

The problem is that [NSKeyedUnarchiver unarchivedObjectOfClass:fromData:error:] is returning nil, and the error text is logged as The data couldn’t be read because it isn’t in the correct format.. The data retrieved using [self objectForKey:] is of type __NSCFData. I don't know if this is relevant since AFAIK __NSCFData is toll-free bridged to NSData.

Replacing [NSKeyedUnarchiver unarchivedObjectOfClass:fromData:error:] with [NSKeyedUnarchiver unarchiveObjectWithData:] solves the problem. The data are stored and retrieved correctly. But this method is now deprecated so I need to move to the more modern method, but cannot identify why it isn't working.

4 Answers

I had a similar issue. I found that I had to pass in the classes for the objects that were in the NSArray.

NSSet *set = [NSSet setWithArray:@[
                      [NSArray class],
                      [STUFF_IN_ARRAY class]
                      ]];

NSArray *results = [NSKeyedUnarchiver unarchivedObjectOfClasses:set fromData: rawData error: &error];

None of the other answers cover this well enough for custom classes (or make it clear enough) so hopefully I can help some people by adding an answer!

If your data represents an archived array of a custom class that has various class properties then it's not enough to just provide the array class and your custom class, you need to provide all the classes that your custom class uses to, e.g.:

NSData* data;
NSError* error;
[NSKeyedUnarchiver unarchivedObjectOfClasses:
                        [NSSet setWithArray: @[
                             [NSMutableArray class],
                             [CustomClass class],
                             // CustomClass has properties using the following classes:
                             [NSDate class],
                             [NSString class],
                             [NSNumber class],
                             [NSIndexSet class]
                         ]]
                         fromData: data
                         error: &error];

for inspiration I'm coping my method for unarchiving data from file by [NSKeyedUnarchiver unarchivedObjectOfClasses: fromData:]... There is small improvement: you don't need to list all classes in the method call because the most used classes are added automatically in the method body.

// MyFileManager.m
- (id)getDataFromFile:(NSString *)file inSubfolder:(NSString *)subfolder dataClasses:(NSArray<Class> *)classes {
    NSString *filePath = [self pathForFile:file subfolder:subfolder];

    NSData *data = [[NSFileManager defaultManager] contentsAtPath:filePath];
    NSArray *extendedClasses = [classes arrayByAddingObjectsFromArray:@[[NSArray class], [NSMutableArray class], [NSDictionary class], [NSMutableDictionary class], [NSDate class], [NSNumber class]]];

    NSError *unarchivingError;
    id unarchived = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithArray:extendedClasses] fromData:data error:&unarchivingError];

    if (unarchivingError) {
        return nil;
    } else {
        return unarchived;
    }
}

And here is usage of the method // expected unarchived data are in dictionary

NSDictionary *dictionary = [FILE_MANAGER getDataFromFile:@"testfile" inSubfolder:nil dataClasses:@[[MyClass class]]];

And a small advice. Your "MyClass" must conform the "NSSecureCoding" protocol. And also implement 3 method in you "MyClass"...

@interface MyClass : NSObject <NSSecureCoding> // MyClass.h

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic) NSInteger count;

@end

@implementation MyClass // MyClass.m

- (id)initWithCoder:(NSCoder *)decoder {
    if (self = [super init]) {
        self.name = [decoder decodeObjectOfClass:[NSString class] forKey:@"name"];
        self.date = [decoder decodeObjectOfClass:[NSDate class] forKey:@"date"];
        self.count = [decoder decodeIntegerForKey:@"count"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeObject:self.date forKey:@"date"];
    [encoder encodeInteger:self.count forKey:@"count"];
}

+ (BOOL)supportsSecureCoding{
    return YES;
}

For those who want to see the helping method "pathForFile" - I use as the main forlder "Library/Application Support" (because of this article Table 1-3: iOS File System Basics)

// MyFileManager.m

- (NSString *)pathForFile:(NSString *)file subfolder:(NSString *)subfolder {

    NSString *folderPath = [self storageFolderPathWithSubfolder:subfolder];
    NSString *filePath = [folderPath stringByAppendingPathComponent:file];

    return filePath;
}

- (NSString *)storageFolderPathWithSubfolder:(NSString *)subfolder {

    NSString *storageFolderPath = [self storageFolderPath];

    if (!subfolder.length) {
        return storageFolderPath;
    }

    NSString *subfolderPath = [storageFolderPath stringByAppendingPathComponent:subfolder];

    if (![[NSFileManager defaultManager] fileExistsAtPath:subfolderPath]) { //Does directory already exist?
        [[NSFileManager defaultManager] createDirectoryAtPath:subfolderPath withIntermediateDirectories:NO attributes:@{ NSFileProtectionKey: NSFileProtectionNone } error:nil];
    }

    return subfolderPath;
}

- (NSString *)storageFolderPath {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
    NSString *storageFolderPath = [paths firstObject];

    // create folder if it doesn't exist
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL folderExists = [fileManager fileExistsAtPath:storageFolderPath];

    if (!folderExists) {
        [fileManager createDirectoryAtPath:storageFolderPath withIntermediateDirectories:NO attributes:@{ NSFileProtectionKey: NSFileProtectionNone } error:nil];
    }

    return storageFolderPath;
}

this line should work for you:

[NSKeyedUnarchiver unarchivedObjectOfClass:NSArray.class fromData:data error:&err];
Related