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.
Related
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.
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
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 ;)
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.
I've been reading about NSArrays and NSDictionaires and I think I need the later. I'm trying to populate an object from a small database table. So I can access the string values via a record id. I have to do this several times so putting it into an object makes sense.
I have the basics...
- (void)viewDidLoad {
// WORKING START
NSMutableDictionary *dictCategories = [[NSMutableDictionary alloc] init];
[dictCategories setValue:#"Utility" forKey:#"3"];
[dictCategories setValue:#"Cash" forKey:#"5"];
NSString *result;
result = [dictCategories objectForKey:#"3"];
NSLog(#"Result=%#", result);
// WORKING END
// Can't get this bit right, current error Request for member
// 'getCategories' in something not a structure or union
NSMutableDictionary *dictCategories2 = self.getCategories;
NSLog(#"Result2=%#", [dictCategories2 objectForKey:#"5"]);
[super viewDidLoad];
}
-(NSMutableDictionary*)getCategories {
NSMutableDictionary *dictCategories = [[NSMutableDictionary alloc] init];
[dictCategories setValue:#"Utility" forKey:#"3"];
[dictCategories setValue:#"Cash" forKey:#"5"];
return dictCategories;
}
you are calling the method wrong,try [self getCategories]
You're not being clear on what isn't working, but a few things that are obviously wrong (JonLOo might be spot on though) ...
Firstly. You're using the wrong methods, or at least there's a better one -- setValue:forKey: should/could be setObject:forKey: instead. This might be one of the reasons for your issue.
Secondly. You're over-allocating and not releasing properly. dictCategories2 in your viewDidLoad will vanish into the void and bring with it the allocated memory for dictCategories defined in the getCategories method. An easy standard fix for this is to change
NSMutableDictionary *dictCategories = [[NSMutableDictionary alloc] init];
in getCategories into
NSMutableDictionary *dictCategories = [NSMutableDictionary dictionary];
It will be autoreleased using the latter method by the system.
Thirdly. You want to read up on #property. Instead of getFoo, setBar, the Ob-C standard is to use #properties to (pre)define setters and getter methods. You can then override these to populate default data into your methods when appropriate. You also (probably) want to store the dictionary in your interface as an instance variable, rather than letting it be deallocated all the time. Example of a #property implementation that does this:
#interface foo {
NSMutableDictionary *ingredients;
}
#property (nonatomic, retain) NSMutableDictionary *ingredients;
#end
// ....
#implementation foo
#synthesize ingredients;
// ...
// the #synthesize command above will create getter and setter methods for us but
// we can override them, which we need to do here
- (NSMutableDictionary *)ingredients
{
if (ingredients != nil) {
// we've already got an ingredients variable so we just return it
return ingredients;
}
// we need to create ingredients
ingredients = [[NSMutableDictionary alloc] init];
[ingredients setObject:#"foo" forKey:#"bar"]
return ingredients;
}
In the viewDidLoad method (or anywhere else where you think ingredients might not have been initialized yet), you would do e.g.
NSMutableDictionary *dict = self.ingredients;
Anywhere else you can opt to use just ingredients without self, but if it's nil, your method will never be called, and you will get nil thrown at you.
This is useful in many cases, and is necessary if we want to ever read or write the ingredients variable from outside of our class. It's outside of what you're asking about, but I brought it up because you're trying to do something similar with self.getCategories.
Hope that helps.