NSCoder - Encoding an Array, with multiple levels of nested arrays - objective-c

I have a mainObjectArray (NSMutableArray) which is populated with instances of a custom class. Each instance is itself an array, and objects in each array are NSDates, NSStrings, BOOL, and more arrays containing similar objects.
What I haven't been able to establish is whether it's possible to, inside the
- (void)encodeWithCoder:(NSCoder *)encoder
method, to just say something like that:
[encoder encodeWithObject:mainObjectArray];
Or do have to encode every object in every instance separately? This would be a bit of a pain...
Your help would be very much appreciated.

Just implement the encoding and decoding methods in your custom class. That will do. Some sample,
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:[NSNumber numberWithInt:pageNumber] forKey:#"pageNumber"];
[encoder encodeObject:path forKey:#"path"];
[encoder encodeObject:array forKey:#"array"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if(self = [super init])
{
self.pageNumber = [[aDecoder decodeObjectForKey:#"pageNumber"] intValue];
self.path = [aDecoder decodeObjectForKey:#"path"];
self.array = [aDecoder decodeObjectForKey:#"array"];
}
}
You can see totally three data types being encoded and decoded - int, string, array.
Hope this helps.

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.

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

How to restore an array with NSCoder

So far I have the following:
- (id)initWithCoder:(NSCoder*) coder
{
self = [super initWithCoder: coder];
if (self) {
// Call a setup method
}
return self;
}
Am I supposed to put the code to load the array in here? What could should I put and where should I put it?
You put myArray=[coder decodeObjectForKey:#"myArray"]; inside the if block.
If you haven't set up the encoding part of the code yet, to do that you just add a method:
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:myArray forKey:#"myArray"];
}

How to store a NSUInteger using NSCoding?

How do I store a NSUInteger using the NSCoding protocol, given that there is no method on NSCoder like -encodeUnsignedInteger:(NSUInteger)anInt forKey:(NSString *)aKey like there is for NSInteger?
The following works, but is this the best way to do this? This does needlessly create objects.
#interface MYObject : NSObject <NSCoding> {
NSUInteger count;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:[NSNumber numberWithUnsignedInteger:count] forKey:#"count"];
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (self != nil) {
count = [[decoder decodeObjectForKey:#"count"] unsignedIntegerValue];
}
return self;
}
NSNumber has a lot of methods to store/retrieve different sized and signed types. It is the easiest solution to use and doesn't require any byte management like other answers suggest.
Here is the types according to Apple documentation on NSNumber:
+ (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value
- (NSUInteger)unsignedIntegerValue
Yes your code example is the best way to encode/decode the NSUInteger. I would recommend using constants for the key values, so you don't mistype them and introduce archiving bugs.
static NSString * const kCountKey = #"CountKey";
#interface MyObject : NSObject <NSCoding> {
NSUInteger count;
}
#end
#implementation MyObject
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:[NSNumber numberWithUnsignedInteger:count] forKey:kCountKey];
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (self != nil) {
count = [[decoder decodeObjectForKey:kCountKey] unsignedIntegerValue];
}
return self;
}
#end
The problem is that NSUInteger may not be the same size as an unsigned int. On many (if not most by now) Mac OS X machines, NSUInteger will be an unsigned long, which will be twice as big. On the other hand, a keyed archiver should handle that without a problem, since it builds a dictionary.
Further complicating matters is the fact that NSCoder doesn't have any methods to deal with unsigned types. I can't think of how this would cause any data loss, but it would require some ugly casts, plus it just feels dirty.
If you want to insist upon an unsigned type, the simplest way would be to encode the raw bytes (preferably in network byte order using htonl and ntohl) in the largest type available (unsigned long long) using encodeBytes:length:forKey: and decodeBytesForKey:returnedLength:. For maximum safety, you should check the length of what you decoded and cast the pointer (or use a union) to extract the correct-sized type.
The drawback of this is that the value will be represented in the output as data, not an integer. This mainly matters only if somebody decides to read in the raw plist data for your archive instead of using the keyed unarchiver like you do, and even then only to them. The other cases where it might matter is if Apple (or you) should ever switch to an architecture that has even larger integer types, types whose size in bits is not a power of two (there's at least one old platform where a word was 24 bits), or types with an unusual layout (not big- or little-endian).
As for your NSNumber solution: You might want to crack open your archive in Property List Editor and see what it emitted. If the output contains an integer element, then it's the same as using encodeInteger:forKey:. If the output contains a data element, then it's the same as the solution I mentioned above. To be thorough, you should check the output from every architecture you support.
That is the best way to do it. It does seem like needless work, but there's no other type available that can portably represent the entire range of values NSUInteger can hold.
You're going to want to use NSCoder's
encodeInt:ForKey:
and
decodeIntForKey:
methods. So, in your case you would need :
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeInt:count forKey:#"count"];
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (self != nil) {
count = [decoder decodeIntForKey:#"count"];
}
return self;
}
Hope that helps, also, there's plenty more encode "encode" methods, take a look at the documentation for NSCoder :D

initWithCoder works, but init seems to be overwriting my objects properties?

I've been trying to teach myself how to use the Archiving/Unarchiving methods of NSCoder, but I'm stumped.
I have a Singleton class that I have defined with 8 NSInteger properties. I am trying to save this object to disk and then load from disk as needed.
I've got the save part down and I have the load part down as well (according to NSLogs), but after my "initWithCoder:" method loads the object's properties appropriately, the "init" method runs and resets my object's properties back to zero.
I'm probably missing something basic here, but would appreciate any help!
My class methods for the Singleton class:
+ (Actor *)shareActorState
{
static Actor *actorState;
#synchronized(self) {
if (!actorState) {
actorState = [[Actor alloc] init];
}
}
return actorState;
}
-(id)init
{
if (self = [super init]) {
NSLog(#"New Init for Actor started...\nStrength: %d", self.strength);
}
return self;
}
-(id)initWithCoder:(NSCoder *)coder
{
if (self = [super init]) {
strength = [coder decodeIntegerForKey:#"strength"];
dexterity = [coder decodeIntegerForKey:#"dexterity"];
stamina = [coder decodeIntegerForKey:#"stamina"];
will = [coder decodeIntegerForKey:#"will"];
intelligence = [coder decodeIntegerForKey:#"intelligence"];
agility = [coder decodeIntegerForKey:#"agility"];
aura = [coder decodeIntegerForKey:#"aura"];
eyesight = [coder decodeIntegerForKey:#"eyesight"];
NSLog(#"InitWithCoder executed....\nStrength: %d\nDexterity: %d", self.strength, self.dexterity);
[self retain];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeInteger:strength forKey:#"strength"];
[encoder encodeInteger:dexterity forKey:#"dexterity"];
[encoder encodeInteger:stamina forKey:#"stamina"];
[encoder encodeInteger:will forKey:#"will"];
[encoder encodeInteger:intelligence forKey:#"intelligence"];
[encoder encodeInteger:agility forKey:#"agility"];
[encoder encodeInteger:aura forKey:#"aura"];
[encoder encodeInteger:eyesight forKey:#"eyesight"];
NSLog(#"encodeWithCoder executed....");
}
-(void)dealloc
{
//My dealloc stuff goes here
[super dealloc];
}
I'm a noob when it comes to this stuff and have been trying to teach myself for the last month, so forgive anything obvious.
Thanks for the help!
I use the following template all the time and find it very useful. The loading and saving of the singleton's state is encapsulated and all you have to do is to ask for the shared instance. You might want to make persistToStorage public and call it from the app delegate's applicationWillTerminate: method.
(You might want to make this more thread safe.)
static Actor* SharedActor;
+(Actor*)sharedActor
{
if (SharedActor)
return SharedActor;
SharedActor = [[NSKeyedUnarchiver unarchiveObjectWithFile:[self actorDataFileName]]retain];
if (!SharedActor)
SharedActor = [[Actor alloc]init];
return SharedActor;
}
+(NSString*)actorDataFileName
{
NSString* path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
return [path stringByAppendingPathComponent:#"actor.dat"];
}
-(BOOL)persistToStorage
{
return [NSKeyedArchiver archiveRootObject:self toFile:[Actor userDataFileName]];
}
-(void) encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeInteger:strength forKey:#"strength"];
. . .
}
-(id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init])
{
strength = [decoder decodeIntegerForKey:#"strength"];
. . .
}
}
A couple of things:
Don't do this: [self retain];
There is nothing strange about archiving a singleton...
I'm not sure that it makes sense to have a singleton class that is archived/unarchived. When you do Actor *actor = [Actor shareActorState]; you're accessing the shared instance. If you then archive that NSData *data = [NSKeyedArchiver archivedDataWithRootObject:actor], you'll be saving the correct thing.
What are you intending when you unarchive, though? Actor *newactor = [NSKeyedUnarchiver unarchiveObjectWithData:data]; creates a new actor. It's in a new area of memory from your shared actor. This newactor is different from the singleton, [Actor shareActorState], and to violates the design pattern since you now have more than one object.
More likely, you'd want to implement two instance methods on your Actor class. They would be:
- (NSData *)archiveStateToData;
- (void)unarchiveStateFromData:(NSData *)data;
Which would actually update the values in the instance (using archiving). If you're trying to learn the NSCoding protocol and how to archive/unarchive data, I would recommend not doing it with a singleton.
While I agree with #wbyoung that you probably shouldn't be archiving/unarchiving a singleton (unless you're going through NSUserDefaults), if you were to do this I believe it would work by just changing
-(id)initWithCoder:(NSCoder *)coder
{
if (self = [super init]) {
to
-(id)initWithCoder:(NSCoder *)coder
{
if (self = [self shareActorState]) {
(oh and I would rename shareActorState to sharedActorState, or better yet sharedActor)