objective-cnskeyedarchiver

archivedDataWithRootObject: always returns "nil"


I have a class named Person and created a Person instance "person".

Person *person = [Person personWithName:@"Kyle", andAge:15];

Then I tried to encode it using method archivedDataWithRootObject:requiringSecureCoding:error:.

NSData *personData = [NSKeyedArchiver archivedDataWithRootObject:person 
                                      requiringSecureCoding:YES error:nil];

However, the personData always returns nil. Did I miss something?

Person.h

@interface Person : NSObject<NSSecureCoding>
@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;
+ (instancetype)personWithName:(NSString *)name andAge:(NSInteger)age;
@end

Person.m

@implementation Person
+ (instancetype)personWithName:(NSString *)name andAge:(NSInteger)age{
    Person *p = [Person new];
    p.name = name;
    p.age = age;
    return p;
}
+ (BOOL)supportsSecureCoding {
    return YES;
}
- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder]; // error: No visible @interface for 'NSObject' declares the selector 'initWithCoder'
    return self;
}
@end

Update (after implementing +supportsSecureCoding in .m):

Class 'Person' has a superclass that supports secure coding, but 'Person' overrides -initWithCoder: and does not override +supportsSecureCoding. The class must implement +supportsSecureCoding and return YES to verify that its implementation of -initWithCoder: is secure coding compliant.


Solution

  • What's wrong: Person isn't NSSecureCoding compliant. That's the first thing that pops in mind if you ever played with Archiving Custom Class into Data with NSKeyedArchiver (if it's an old developer, he/she would have said NSCoding, but that's "almost the same", same logic).
    What's the deal with that? It's just how to convert Person into NSData and reverse. What's the logic? Do you want to save its properties? How? Etc.

    BUT, your biggest mistake as a developer was to totally ignore the error parameter!

    NSData *personData = [NSKeyedArchiver archivedDataWithRootObject:person 
                                          requiringSecureCoding:YES error:nil];
    

    ==>

    NSError *archiveError
    NSData *personData = [NSKeyedArchiver archivedDataWithRootObject:person 
                                          requiringSecureCoding:YES
                                          error:& archiveError];
    if (archiveError) {
        NSLog(@"Ooops, got error while archiving: %@", archiveError);
    }
    

    Then the error would have stated that it was indeed missing the NSSecureCoding compliancy

    See the documentation of Archives and Serializations Programming Guide: Encoding and Decoding Objects, you'll see how to implement initWithCoder: (from NSData to Person) and encodeWithCoder: (from Person to NSData).

    Applied to your class (and add it to the compliance with: @interface Person : NSObject< NSSecureCoding > for instance):

    - (void) encodeWithCoder:(NSCoder *)encoder { 
        [encoder encodeObject:_name forKey:@"name"]; 
        [encoder encodeInteger:_age forKey:@"age"]; 
    } 
    
    - (id)initWithCoder:(NSCoder *)coder { 
        self = [super init]; 
        if (self) { 
            _name = [coder decodeObjectForKey:@"name"]; 
            _age = [coder decodeIntegerForKey:@"age"]; 
        } 
        return self; 
    }
    
    + (BOOL)supportsSecureCoding {
        return YES;
    }
    

    Note that the strings key ("name" & "age" needs to be the same for encode/decode, you can use const, etc.)