Write complex object to file objective-c - 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 ;)

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"]];

Saving custom objects to a plist with NSCoder

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

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.

Replace array display method?

I am curious how I might override the description method that is used when you do the following (see below) for an object. I basically want to better format the output, but am unsure about how I might go about setting this up.
NSLog(#"ARRAY: %#", myArray);
many thanks
EDIT_001
Although subclassing NSArray would have worked I instead decided that I would add a category to NSArray (having not used one before) Here is what I added ...
// ------------------------------------------------------------------- **
// CATAGORY: NSArray
// ------------------------------------------------------------------- **
#interface NSArray (displayNSArray)
-(NSString*)display;
#end
#implementation NSArray (displayNSArray)
-(NSString*)display {
id eachIndex;
NSMutableString *outString = [[[NSMutableString alloc] init] autorelease];
[outString appendString:#"("];
for(eachIndex in self) {
[outString appendString:[eachIndex description]];
[outString appendString:#" "];
}
[outString insertString:#")" atIndex:[outString length]-1];
return(outString);
}
#end
gary
If you're doing this a lot, the easiest way to reformat the display of your array would be to add a new prettyPrint category to the NSArray class.
#interface NSArray ( PrettyPrintNSArray )
- (NSSTring *)prettyPrint;
#end
#implementation NSArray ( PrettyPrintNSArray )
- (NSString *)prettyPrint {
NSMutableString *outputString = [[NSMutableString alloc] init];
for( id item in self ) {
[outputString appendString:[item description]];
}
return outputString;
}
#end
Obviously you'd need to alter the for loop to get the formatting the way you want it.
I'm assuming that you myArray variable is an instance of the NSArray/NSMutableArray class.
When NSLog() encounters the # character in its format string, it calls the -description: method on the object. This is a method on the root class, NSObject from which all other Cocoa classes inherit. -description: returns an NSString allowing any object that implements this method to be passed into NSLog(#"#",anyObject) and have a nicely formatted output. The string returned can be anything you care to construct.
For your specific problem, you could subclass NSMutableArray and override the -description: method with your own implementation. Then utilise your subclass instead of NSMutableArray.
For more information on NSObject and -description: see Apple's docs.
From Formatting string objects:
NSString supports the format characters defined for the ANSI C functionprintf(), plus ‘#’ for any object. If the object responds to the descriptionWithLocale: message, NSString sends that message to retrieve the text representation, otherwise, it sends a description message.
So to customize array conversion to string you should change NSArray descriptionWithLocale: implementation. Here's an example of how you can replace object method in run-time.