BadmintonCat
BadmintonCat

Reputation: 9586

Storing custom objects with Core Data

I'm trying to store a NSMutableArray consisting of VOs (NameVO) with Core Data but getting the following exception thrown:

'NSInvalidArgumentException', reason: '-[NameVO encodeWithCoder:]: unrecognized selector sent to instance 0x1096400a0'

My NameVO is just a simple value object, extending NSObject and contains two fields, a string and a NSMutableArray that itself contains strings. It also contains a compare function.

I'm trying to prepare this to be stored as a CD transformable attribute type with ('names' is my NameVO array):

NSData *tempNamesData = [NSKeyedArchiver archivedDataWithRootObject:names];

My question is: what do I need to do to make NameVO be accepted by NSKeyedArchiver to convert it successfully to a NSData?

I don't want NameVO to extend NSManagedObject because then I cannot instantiate and init it directly.

Upvotes: 1

Views: 4822

Answers (3)

vsanthanam510
vsanthanam510

Reputation: 370

Now that we're in 2017, it's best to use the safer NSSecureCoding protocol instead of the older, less safe NSCoding protocol. The implementation changes are minimal:

1) ensure that your class declares its conformation to NSSecureCoding

@interface MyClass : NSObject<NSSecureCoding>

@property (nonatomic, strong) NSNumber *numberProperty;
@property (nonatomic, strong) NSString *stringProperty;

@end

2) NSSecureCoding protocol houses the same two instance methods methods in the NSCoding protocol, plus an additional class method, +supportsSecureCoding. You'll need to add that method, as well as slightly modify your -initWithCoder: method.

@implementation MyClass

// New Method for NSSecureCoding. Return YES.
+ (BOOL)supportsSecureCoding {

    return YES;

}

// Your Encode Method Can Stay The Same, though I would use NSStringFromSelector whenever possible to get the keys to ensure you're always getting what you're looking for.

- (void)encodeWithCoder:(NSCoder *)aCoder {

    [aCoder encodeObject:self.numberProperty forKey:NSStringFromSelector(@selector(numberProperty))];
    [aCoder encodeObject:self.stringProperty forKey:NSStringFromSelector(@selector(stringProperty))];

}

// Slightly updated decode option

- (instancetype)initWithCoder:(NSCoder *)aDecoder {

    self = [super init];

    if (self) {

        self.numberProperty = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(numberProperty))];
        self.stringProperty = [aDecoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(@selector(stringProperty))];


    }

}

@end

Notice that NSCoder's -decodeObjectOfClass:withKey: requires you to specify the class that you're expecting to receive. This is a much safer way to do things.

Then, to store this decodable object in CoreData, simply create a Managed object that contains an NSData attribute and some identifying information (a string, a date, an id, or a number or something)

@interface MyClassMO : NSManagedObject

@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, strong) NSData *data;

@end

@implementation MyClassMO

@dynamic identifier;
@dynamic data;

@end

In practice, it would look something like this:

- (void)storeObject:(MyClass *)object withIdentifier:(NSString *)identifier {

    NSData *objectData = [NSKeyedArchived archivedDataWithRootObject:object];

    NSManagedObjectContext *moc = ... // retrieve your context

    // this implementation relies on new NSManagedObject initializers in the iOS 10 SDK, but you can create it any way you typically create managed objects
    MyClassMO *managedObject = [[MyClassMO alloc] initWithContext:moc];

    managedObject.data = objectData;
    managedObject.identifier = identifier;

    NSError *error;

    [moc save:&error];

}

- (MyClass *)retrieveObjectWithIdentifier(NSString *)identifier {

    NSManagedObject *moc = ... // retrieve your context

    // This also relies on iOS 10 SDK's new factory methods available on NSManagedObject. You can create your request any way you typically do;
    NSFetchRequest *request = [MyClassMO fetchRequest];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identifier = %@", identifier];

    request.predicate = predicate;

    NSError *error;

    NSArray<MyClassMO *> *results = [moc executeFetchRequest:request withError:&error];

    // if you're only storing one object per identifier, this array should only be 1 object long. if not, you'll need to decide which object you're looking for. you also might want to implement an overwrite policy or a delete before store type thing.

    MyClassMO *managedObject = results.firstObject;

    NSData *objectData = managedObject.data;

    MyClass *object = [NSKeyedUnarchiver unarchiveObject:objectData];

    return object;

}

This solution is obviously a bit of an oversimplification, how and when you store stuff in the db is up to your needs, but the main idea is that, you'll need to make sure your custom class conforms to NSSecureCoding, and that you'll need to make a separate Managed Object class to store and retrieve your data.

Upvotes: 4

ChenSmile
ChenSmile

Reputation: 3441

Use this initWithCoder and encodeWithCode method.I hope it will work for you. it works for me in same issue as u have...Use this sample code

-(id)initWithCoder:(NSCoder *)aDecoder
{
if(self = [super init]){
    storePlaylist=[aDecoder decodeObjectForKey:@"storePlaylist"];
    playlistName=[aDecoder decodeObjectForKey:@"playlistName"];
}
return self;
}

-(void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:storePlaylist forKey:@"storePlaylist"];
[encoder encodeObject:playlistName forKey:@"playlistName"];
}

Upvotes: 1

Greg
Greg

Reputation: 25459

As your exception says you need to implement NSCoding protocol to your class and you have to override two methods:

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;

It should sorted your issue.

// EXTENDED

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        _formId = [aDecoder decodeObjectForKey:@"FormID"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.formId forKey:@"FormID"];
}

Upvotes: 3

Related Questions