Saving custom objects to a plist with NSCoder - objective-c

So, I've realised that I need to use NSCoding to save my custom objects to a plist.
The part I'm struggling with is exactly how I do that after I've implemented the initWithCoder and encodeWithCoder methods. They return id and void respectively, so if they've converted my objects into NSData or whatever they do to them, where's the data, so I can save it?
I have the methods set up thusly:
- (id)initWithCoder:(NSCoder *)decoder { // Decode
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:#"gameName"];
self.genre = [decoder decodeObjectForKey:#"gameGenre"];
self.rating = [decoder decodeObjectForKey:#"gameRating"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder { // Encode
[encoder encodeObject:self.name forKey:#"gameName"];
[encoder encodeObject:self.genre forKey:#"gameGenre"];
[encoder encodeObject:self.rating forKey:#"gameRating"];
}
Trying to figure out what the next steps are to get these objects (which are already in an NSMutableArray) saved to a .plist, and recalled when re-opening the app.
Any help appreciated!

Well, that depends on what you want to do. It's possible that NSKeyedArchiver will be sufficient. Consider:
+ (NSData *)archivedDataWithRootObject:(id)rootObject
+ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path
which takes a root object of an object graph and either creates a NSData object with the serialized data, or serializes the data to a file.
Then look at NSKeyedUnarchiver which has these:
+ (id)unarchiveObjectWithData:(NSData *)data
+ (id)unarchiveObjectWithFile:(NSString *)path
I'm sure that will get you on your way toward your goal.
EDIT
I'm hitting an error when trying to (I think) tell it which array I
want it to write. Getting "Expected identifier" on this line:
[rootObject setValue:[self.dataController.masterGameList]
forKey:#"games"]; – lukech
That's a KVC API. The archive/unarchive methods are class methods, so you just save your entire object graph with:
if (![NSKeyedArchiver archiveRootObject:self.dataController.masterGameList
toFile:myPlistFile]) {
// Handle error
}
and then you load it with:
self.dataController.masterGameList =
[NSKeyedUnarchiver unarchiveObjectWithFile:myPlistFile];

You will need to implement NSKeyedArchiver on the root object of your object graph to save and then NSKeyedUnarchiver to reconstitute the object graph.
See this tutorial

Related

NSCoder not working with NSArray

I am trying to implement the NSCoder methods encodeWithCoder and initWithCoder for a custom object i have created which has a child array of custom objects. Both custom objects employment the above mentioned methods but after the top level object has been decoded the value for the array is always nil.
Both objects implement the methods below, The dictionary and the arrays or popular from a library i have for getting field names and turning objects into dictionaries. I have checked that encodeObject is being called on the Array and at this time the array is not nil. I have equally checked that the decode method is receiving nil on the other side..
I can't work out where i am going wrong? Am i correct is assuming that so long as child array objects implement the protocol i should be fine to do it this way?
- (void)encodeWithCoder:(NSCoder *)aCoder{
NSDictionary* dictionary = [jrModelBinder unBind:self];
for(NSString* field in dictionary)
{
id val = [self valueForKey:field];
[aCoder encodeObject:val forKey:field];
}
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if(self = [super init]){
NSArray* fields = [jrModelBinder propertyNames:self];
for(NSString* field in fields)
{
id val = [aDecoder decodeObjectForKey:field];
[self setValue:val forKey:field];
}
}
return self;
}
I misunderstood your question before:
NSKeyedArchiver/Unarchiver should encode and decode NSArrays and NSDictionaries with no problem.
But I think since your array itself contains custom Objects which implements below 2 functions:
- (id)initWithCoder:(NSCoder *)aDecoder
- (void)encodeWithCoder:(NSCoder *)enCoder
Try using below for array
NSData * encodedData = [NSKeyedArchiver archivedDataWithRootObject:someArray];
Also use NSKeyedUnarchiver to decode.

NSKeyedArchiver/Unarchiver and a custom NSArray subclass unarchives to a different subclass

I have a subclass of NSMutableArray (necessary to enforce certain restrictions on the contained objects). I encode the array as usual, and then decode it. The problem is that while the encoded class (given to NSKeyedArchiver) is a JOTypedMutableArray, the output class is another subclass of NSMutableArray (one of the private ones).
I looked into the archived data with LLDB: po [NSPropertyListSerialization propertyListFromData:archivedData mutabilityOption:0 format:NULL errorDescription:NULL]. The output of that command contained this:
"$classes" = (
NSMutableArray,
NSArray,
NSObject
);
"$classname" = NSMutableArray;
It appears to me that while the archiver is given a subclass of the class cluster, it is set to ignore the concrete subclasses and encode as the abstract superclass.
The question here is: how could I force the archiver to encode the object correctly? (Or am I doing something else wrong here?)
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
NSString *stringType = [aDecoder decodeObjectForKey:#"JOTypedMutableArrayType"];
Class classType = NSClassFromString(stringType);
_type = classType;
NSMutableArray *backingArray = [aDecoder decodeObjectForKey:#"JOTypedMutableArrayContents"];
_jo_backingArray = backingArray;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:NSStringFromClass(self.type) forKey:#"JOTypedMutableArrayType"];
[aCoder encodeObject:self.jo_backingArray forKey:#"JOTypedMutableArrayContents"];
}
When you encode using the property-list serialization you're restricting yourself to the basic property-list types: array, string, dictionary, data, some others I forget right now...docs are here.
This is a "feature" of NSPropertyListSerialization , because property-lists are textual and don't contain explicit type information. If you want to include all the type information you'll need to use a different class of archiver, check NSArchiver.
From what little I know of your case, though, I might recommend just changing the array back to the type you want when you read it in, so you can continue to use the nice text format.
Like, you could do:
NSMutableArray *backingArray = [JOTypedMutableArray arrayWithArray:[aDecoder decodeObjectForKey:#"JOTypedMutableArrayContents"]];

Write complex object to file objective-c

I find it hard to write/read array of custom objects. In my my app, Contact class has a NSDictionary as property and this dictionary has array as objects for keys.
I serialize/deserialize my objects with NSCoder and NSKeyedArchiever and even tried NSPropertyList serialization. I always get errors when serializing as soon as it starts to serialize NSDictionary. Here is my code and I didn't really find a general answer regarding how to serialize custom objects with complex structure?
//Contact.m
//phoneNumbers is a NSDictionary
#pragma mark Encoding/Decoding
-(void)encodeWithCoder:(NSCoder *)aCoder
{
NSLog(#"Encoding");
[aCoder encodeObject:self.firstName forKey:#"firstName"];
NSLog(#"First name encoded");
[aCoder encodeObject:self.lastName forKey:#"lastName"];
NSLog(#"Last name encoded");
[aCoder encodeInt:self.age forKey:#"age"];
NSLog(#"Age encoded");
NSString *errorStr;
NSData *dataRep = [NSPropertyListSerialization dataFromPropertyList:self.phoneNumbers
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorStr];
NSLog(#"Data class %#", [dataRep class]);
if(!dataRep)
{
NSLog(#"Error encoding %#", errorStr);
}
[aCoder encodeObject:dataRep forKey:#"phones"];
NSLog(#"Encoding finished");
}
- (id) initWithCoder: (NSCoder *)coder
{
if (self = [super init])
{
[self setFirstName:[coder decodeObjectForKey:#"firstName"]];
[self setLastName:[coder decodeObjectForKey:#"lastName"]];
[self setAge:[coder decodeIntForKey:#"age"]];
NSString *errorStr;
NSData *data=[coder decodeObjectForKey:#"phones"];
NSDictionary *propertyList = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:&errorStr];
if(!propertyList)
{
NSLog(#"Error %#", errorStr);
}
[self setPhoneNumbers:propertyList];
}
return self;
}
//Serializing/Deserializing an array of Contact objects:
#pragma mark Import/Export
//Export Contacts to file
-(void)exportContactsToFile
{
BOOL done=[NSKeyedArchiver archiveRootObject:self.contacts toFile:[PathUtility getFilePath:#"phonebook"]];
NSLog(#"Export done: %i", done);
}
//Import Contacts from file
-(void)importContactsFromFile
{
self.contacts = [NSKeyedUnarchiver unarchiveObjectWithFile:[PathUtility getFilePath:#"phonebook"]];
}
Is there a generic good way to serialize/deserialize objects in objective-c? thanks
The error I get is:
0objc_msgSend
1 CF_Retain
...
that's stack trace, but I get no other errors(
You shouldn't need to use NSPropertyListSerialization for self.phoneNumbers. NSDictionary adheres to the NSCoding protocol.
So, [aCoder encodeObject:self.phoneNumbers forKey:#"phones"]; should be sufficient.
As long as a class adheres to NSCoding (which nearly all Apple-provided class do), you can just use -encodeObject:forKey:, since that method will call that object's implementation of -encodeWithCoder:
I have a special class in my proprietary library that automatically reads the list of its properties and use the getter and setter to encode and decode the object. Sorry I can't share the code here but I can at least give you steps by steps how my class works:
First, the class must be implement NSCoding and NSCopying protocols.
Inside + (void)initialize, iterate thru the definitions of the properties of the class using class_copyPropertyList(), property_getName() and property_copyAttributeList(). Refer Objective-C Runtime Programming Guide for details on these functions.
For each property, run thru its attribute list and get the attribute with strncmp(attribute.name, "T", 1) == 0 (yup, it's a c-string in there). Use that attribute value to determine the type of the property. For example, "i" means int, "I" means unsigned int, if it starts with a "{" then it's a struct etc. Refer this page on the Type Encoding.
Store the property name-type pairs inside a NSDictionary. At the end of properties iteration, store this dictionary inside a static and global NSMutableDictionary using the class name as the key.
To support auto-encoding, implement - (void)encodeWithCoder:(NSCoder *)aCoder to iterate thru the property name-type pair, calling the property getter method (usually - (returnType)propertyName) and encode it inside the coder using appropriate encodeType: method (e.g. encodeInt:, encodeFloat:, encodeObject:, encodeCGPoint: etc).
To support auto-decoding, implement - (id)initWithCoder:(NSCoder *)aDecoder to iterate thru the property name-type pair, decode it from the decoder using appropriate decodeTypeForKey: method (e.g. decodeIntForKey:, decodeFloatForKey:, decodeObjectForKey:, decodeCGPointForKey: etc). and call the property setter method (usually - (void)setPropertyName:).
Implement an instance method that trigger the encoding (luckily I can share this method here ^__^):
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *arc = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[arc encodeRootObject:self];
[arc finishEncoding];
[arc release];
return data;
Once you have the NSData you can anything with it such as calling writeToFile:atomically: or even [[NSUserDefaults standardUserDefaults] setObject:[self wrapInNSData] forKey:key].
Also, implement a class method that returns a new instance of the object loaded from the file:
NSKeyedUnarchiver *unarc = [[NSKeyedUnarchiver alloc] initForReadingWithData:[NSData dataWithContentsOfFile:dataFilePath]];
MyCoolFileDataStore *data = [unarc decodeObject];
[unarc release];
return data;
Finally, to make another object class supports this auto-encoding-decoding, the class needs to extend the special class.
Sorry, it's a bit long winded, but for my case, the extra trouble that I took to create this class really save a lot of time along the road. Struggle today, breeze through tomorrow ;)

Saving an NSArray of custom objects

I've created a subclass of UIImage (UIImageExtra) as I want to include extra properties and methods.
I have an array that contains instances of this custom class.However when I save the array, it appears the extra data in the UIImageExtra class is not saved.
UIImageExtra conforms to NSCoding, but neither initWithCoder or encodeWithCoder are called, as NSLog statements I've added aren't printed.
My method to save the array looks like this:
- (void)saveIllustrations {
if (_illustrations == nil) {
NSLog(#"Nil array");
return;
}
[self createDataPath];
//Serialize the data and write to disk
NSString *illustrationsArrayPath = [_docPath stringByAppendingPathComponent:kIllustrationsFile];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:_illustrations forKey:kIllustrationDataKey];
[archiver finishEncoding];
[data writeToFile:illustrationsArrayPath atomically: YES];
}
And the UIImageExtra has the following delegate methods for saving:
#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
NSLog(#"Encoding origin data!");
[super encodeWithCoder:aCoder];
[aCoder encodeObject:originData forKey:kOriginData];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:(NSCoder *) aDecoder]) {
NSLog(#"Decoding origin data");
self.originData = [aDecoder decodeObjectForKey:kOriginData];
}
return self;
}
My code to create the array in the first place looks like this (in case that offers any clues)
for (NSDictionary *illustrationDict in illustrationDicts) {
NSString *illustrationString = [illustrationDict objectForKey:#"Filename"];
NSNumber *xCoord = [illustrationDict objectForKey:#"xCoord"];
NSNumber *yCoord = [illustrationDict objectForKey:#"yCoord"];
UIImageExtra *illustration = (UIImageExtra *)[UIImage imageNamed:illustrationString];
//Scale the illustration to size it for different devices
UIImageExtra *scaledIllustration = [illustration adjustForResolution];
NSValue *originData = [NSValue valueWithCGPoint:CGPointMake([xCoord intValue], [yCoord intValue])];
[scaledIllustration setOriginData:originData];
[self.illustrations addObject:scaledIllustration];
}
Or am I just going about saving this data the wrong way? Many thanks.
Your code to initialize the array is not actually creating instances of your UIImageExtra subclass.
UIImageExtra *illustration = (UIImageExtra *)[UIImage imageNamed:illustrationString];
returns a UIImage. Casting it doesn't do what you were intending.
UIImageExtra *scaledIllustration = [illustration adjustForResolution];
is still just a UIImage.
One straightforward-but-verbose way to approach this would be to make UIImageExtra a wrapper around UIImage. The wrapper would have a class method for initializing from a UIImage:
+ (UIImageExtra)imageExtraWithUIImage:(UIImage *)image;
And then every UIImage method you want to call would have to forward to the wrapped UIImage instance-- also being careful to re-wrap the result of e.g. -adjustForResolution lest you again end up with an unwrapped UIImage instance.
A more Objective-C sophisticated approach would be to add the functionality you want in a Category on UIImage, and then use method swizzling to replace the NSCoding methods with your category implementations. The tricky part of this (apart from the required Objective-C runtime gymnastics) is where to store your "extra" data, since you can't add instance variables in a category. [The standard answer is to have a look-aside dictionary keyed by some suitable representation of the UIImage instance (like an NSValue containing its pointer value), but as you can imagine the bookkeeping can get complicated fast.]
Stepping back for a moment, my advice to a new Cocoa programmer would be: "Think of a simpler way. If what you are trying to do is this complicated, try something else." For example, write a simple ImageValue class that has an -image method and an -extraInfo method (and implements NSCoding, etc.), and store instances of that in your array.
You can't add objects to an NSArray after init. Use NSMutableArray, that might be the issue.

NSKeyedArchiver and NSKeyedUnarchiver with NSMutableArray

I'm hoping this isn't something to do with the fact that I'm using a Mutable array here, but this one is baffling me so it wouldn't surprise me if that were the case.
BACKGROUND:
I have made a small database which is essentially an NSMutableArray containing custom objects, which we can call recordObjects. I set up the array:
database = [[NSMutableArray alloc] init];
and my custom object, called "recordObject" contains the following variables and inits:
NSString *name;
int anInt;
Bool aBool;
I also synthesized methods so I can make calls like:
aString = [[database objectAtIndex:someIndex] name];
And added methods to my controller class to add, remove, and select the individual records for display. So far everything works correctly and exactly as expected.
Next, I've set up my recordObject class (subclass of NSObject) to use the NSCoder (by including in the #interface directive, and have added the following custom encoder and decoder methods in the implementation file:
-(void) encodeWithCoder: (NSCoder *) encoder {
[encoder encodeObject: name forKey: #"recordName"];
[encoder encodeInt: anInt forKey: #"recordInteger"];
[encoder encodeBool: aBool forKey: #"recordBool"];
}
-(id) initWithCoder: (NSCoder *) decoder {
name = [decoder decodeObjectForKey: #"recordName"];
anInt = [decoder decodeIntForKey: #"recordInteger"];
aBool = [decoder decodeBoolForKey: #"recordBool"];
}
In order to write the file, I have used the following:
[NSKeyedArchiver archiveRootObject:database toFile:myPath];
When I run the program, everything APPEARS to work correctly. The encoder method is called for each of the records in the array and the file is written to disk. Opening the file with TextEdit shows that the data is there (though mostly unintelligible to me.)
THE PROBLEM:
Here's where I run into a snag.
I added the following code to LOAD the file into my database array:
database = [NSKeyedUnarchiver unarchiveObjectWithFile:myPath];
When I run the program again, this time Loading the database, it APPEARS to work correctly. My first test was to use the NSMutableArray count method:
x = [database count];
The result was that X is filled with the correct number of records in the file. If there were 5 records when I saved the database, X is set to 5 after loading the database on the next execution of the program.
Now, here's the big problem:
The program crashes if I try to use ANY of my accessor methods. For example, if I try to use the following after loading the database:
aString = [[database objectAtIndex:someIndex] name];
the program crashes and returns the following error in the console:
Program received signal: “EXC_BAD_ACCESS”.
sharedlibrary apply-load-rules all
My interpretation is that the data is not being loaded and initialized into the database array correctly for some reason, but for the life of me I can't figure out where I've gone wrong here.
As a side note, everything I've implemented came from Stephen G. Kochan's book "Programming in Objective-C"
Any ideas would be greatly appreciated!
There are a few problems with your code.
Your initWithCoder: method is not fully implemented. You must call [super init] and return self. You must also copy or retain the string object, otherwise it will be autoreleased:
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if(self)
{
name = [[decoder decodeObjectForKey: #"recordName"] copy];
anInt = [decoder decodeIntForKey: #"recordInteger"];
aBool = [decoder decodeBoolForKey: #"recordBool"];
}
return self;
}
The other problem is with this line:
database = [NSKeyedUnarchiver unarchiveObjectWithFile:myPath];
That is fine, except for two things:
You're not holding a reference to the object, so it will be
autoreleased.
NSKeyedArchiver returns an immutable object, in this case an NSArray and not an NSMutableArray.
You need to do this:
database = [[NSKeyedUnarchiver unarchiveObjectWithFile:myPath] mutableCopy];
That will both retain the object (because it's a copy) and make the object an NSMutableArray.
It doesn't appear that you're initializing the recordObjects in -initWithCoder:
Try something like this:
-(id) initWithCoder: (NSCoder *) decoder {
self = [super init];
if (self){
name = [decoder decodeObjectForKey: #"recordName"] copy];
anInt = [decoder decodeIntForKey: #"recordInteger"];
aBool = [decoder decodeBoolForKey: #"recordBool"];
}
return self;
}
The data is there when you archive it but you're not properly unarchiving it.
Sounds like a memory management issue to me. EXC_BAD_ACCESS usually means that you're trying to access an object that has been deallocated. unarchiveObjectWithFile: returns an autoreleased object, so you have to retain it if you want to keep it around, either with an explicit retain or by assigning it to a retained property.
I was having the same issue when unarchiving a custom object
self.calTable = [[NSKeyedUnarchiver unarchiveObjectWithFile:calibrationFile] objectForKey:#"calTable"];
Based on Rob's answer I changed to
self.calTable = [[[NSKeyedUnarchiver unarchiveObjectWithFile:calibrationFile] mutableCopy] objectForKey:#"calTable"];
and it fixed all errors.