5

I'm writing some code for an iPhone app, and I'm having issues getting default data to load in correctly. I am basing my code off some example from the "Learning Cocos2d" book by Ray Wenderlich.

It seems that even when I delete the app outright and try to start from fresh data that the app inconsistently either doesn't try to load the data, or incorrectly thinks that there is data, and loads null.

I'm using containsValueForKey to check if a value exists and then load it or load some default value, but even on a fresh installation the containsValueForKey finds data and doesn't load the defaults. In xcode's organizer I checked my device's file structure and the Documents folder, where I specified to save, doesn't look like it contains any files, so I'm not sure what it's grabbing.

My guess is that the problem is something to do with the initWithCoder function. It seems to mysteriously go through the function sometimes, but not all the time. Another weird thing is that I call [[GameManager sharedGameManager] save] when the player gets a highscore (not shown here, but the code is the exact same as this objectiveList, only an int) and it appears to save it correctly.

And now the code:

GCDatabase.h

#import <Foundation/Foundation.h>
id loadData(NSString * filename);
void saveData(id theData, NSString *filename);

GCDatabase.m

#import "GCDatabase.h"
NSString * pathForFile(NSString *filename) {
// 1
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
// 2
NSString *documentsDirectory = [paths objectAtIndex:0];
// 3
return [documentsDirectory stringByAppendingPathComponent:filename];
}

id loadData(NSString * filename) {

NSString *filePath = pathForFile(filename);
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {

    NSData *data = [[[NSData alloc] initWithContentsOfFile:filePath] autorelease];
    NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease];
    id retval = [unarchiver decodeObjectForKey:@"Data"];
    [unarchiver finishDecoding];
    return retval;
}
return nil;
}

void saveData(id theData, NSString *filename) {

NSMutableData *data = [[[NSMutableData alloc] init] autorelease];
NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease];
[archiver encodeObject:theData forKey:@"Data"];
[archiver finishEncoding];
[data writeToFile:pathForFile(filename) atomically:YES];
}

GameManager.h

@interface GameManager : NSObject <NSCoding>{
NSMutableArray *objectiveDescriptions;
}
@property (nonatomic, retain) NSMutableArray * objectiveDescriptions;
+(GameManager*)sharedGameManager;
-(void)save;
-(void)load;
-(void)encodeWithCoder:(NSCoder *)encoder;
-(id)initWithCoder:(NSCoder *)decoder;

@end

GameManager.m (I added the load function, in an attempt to force it to load, but it doesn't seem to work)

+(GameManager*)sharedGameManager {
@synchronized([GameManager class])
{
    if(!sharedGameManager) {
        sharedGameManager = [loadData(@"GameManager") retain];
        if (!sharedGameManager) {
            [[self alloc] init];
        }
    }
    return sharedGameManager;
}
return nil; 
}

+(id)alloc {
@synchronized([GameManager class]){
    NSAssert(sharedGameManager == nil, @"Attempted to allocate a second instance of the Game Manager singleton");
    sharedGameManager = [super alloc];
    return sharedGameManager;
}
return nil;
}

- (void)dealloc {
[objectiveList release];
[super dealloc];
}

- (void)save {
saveData(self, @"GameManager");
}

-(void)load {
loadData(@"GameManager");
}

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

- (id)initWithCoder:(NSCoder *)decoder  {
self = [super init];
if (self != nil) {
if ([decoder containsValueForKey:@"objectiveList"]) {
        objectiveList = [decoder decodeObjectForKey:@"objectiveList"];
    } else {
        [objectiveList addObjectsFromArray:[NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5", nil]];
    }
}

return self;
}
@end
Mark Tuttle
  • 185
  • 1
  • 1
  • 9
  • I even added a check `if([objectivelist count] == 0) [objectiveList addObjectsFromArray:[NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5", nil]];` and then saved it, and it still shows up as _Objectives Array: (null)_ when I print the object out to the log. – Mark Tuttle May 07 '12 at 02:12
  • Per some advice from @Nikhil I changed the code to remove the containsValueForKey, and instead check if it's null after trying to decode it. This did not fix it though, and I added a retain to the end of the calls, like `objectiveList = [[decoder decodeObjectForKey:@"objectiveList"] retain];` and it appears to work ...kind of. The issue now is that on a fresh installation the app doesn't seem to assign anything, but if I close the app and restart it everything works as expected. – Mark Tuttle May 07 '12 at 20:13

4 Answers4

1

I have not read your full code.. But I found a problem in code....

You have not allocated memory to objectiveList array.. Unless and until you allocate memory to array, objects will not be added...

I think go for

 objectiveList = [[NSMutableArray alloc] initWithArray:[NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5", nil]];

instead of

    [objectiveList addObjectsFromArray:[NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5", nil]];

Check for the syntax.. Hope this may help as it troubled me also in the past where I forgot to allocate memory to the array.. And kept on adding objects resulting in null... :) In case it doesn't solve your problem, I'll look for code later completely.. :)

Nikhil Aneja
  • 1,259
  • 8
  • 13
  • Thank you for the reply. What about for the decoding section of the code? `objectiveList = [decoder decodeObjectForKey:@"objectiveList"];` Should this also be allocated first? – Mark Tuttle May 07 '12 at 16:35
  • I've made the changes, but it still does not seem to populate the array. – Mark Tuttle May 07 '12 at 16:39
  • it would be better to allocate it upwards where you think object is allocated.. like objectiveList = [[NSMutableArray alloc] init]; – Nikhil Aneja May 07 '12 at 16:44
  • This did not seem to make any difference. The app runs through the initWithCoder function on starting up, but it still tries to pull in data, even after erasing the app and doing a fresh install. Ultimately it seems that it is pulling in data that doesn't exist, and then won't let me change it (see my comment on my question). – Mark Tuttle May 07 '12 at 17:05
  • objectiveList is of NSMutableArray type or NSArray? – Nikhil Aneja May 07 '12 at 17:07
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/10981/discussion-between-nikhil-aneja-and-mark-tuttle) – Nikhil Aneja May 07 '12 at 17:09
1

I seem to see the problem. When the constructor is called the first time, the objectiveList is not even created as the "initWithCoder" is never called. You have to override the init method as well in order for the objectiveList array to be constructed. Basically, the code that is calling the init method is in here:

+(GameManager*)sharedGameManager {
@synchronized([GameManager class])
{
    if(!sharedGameManager) {
        sharedGameManager = [loadData(@"GameManager") retain];
        if (!sharedGameManager) {
            [[self alloc] init]; // GOES INTO INIT METHOD, NOT INITWITHCODER!
        }
    }
    return sharedGameManager;
}
return nil; 
}

On a side note, that singleton implementation gave me a headache. Just saying. :)

Andres C
  • 919
  • 7
  • 14
0

There is (as far as I can see from the code you have provided) a logic flaw in your code. Consider what would happen if decoder did not contain an objectiveList key; the else clause would execute, but you never allocated objectiveList so the addObjectsFromArray: call will silently fail.

To test this theory, alter your code as show below, and rerun. If the assertion fires then the above theory is correct, if not you need to hunt a bit more!


- (id)initWithCoder:(NSCoder *)decoder
{ self = [super init]; if (self != nil) { if ([decoder containsValueForKey:@"objectiveList"]) { objectiveList = [decoder decodeObjectForKey:@"objectiveList"]; } else { NSAssert(objectiveList, @"objectiveList must be non-nil to add objects."); [objectiveList addObjectsFromArray[NSArrayarrayWithObjects:@"1",@"2",@"3",@"4",@"5", nil]]; } } return self; }

By the way, objectiveList is never declared as an ivar... I am sort of assuming that objectiveList and objectiveDescriptions are meant to be the same.

idz
  • 12,825
  • 1
  • 29
  • 40
0

The method in GameManager.m should look like this:

- (id)initWithCoder:(NSCoder *)decoder  {
self = [super init];
if (self != nil) {
if ([decoder containsValueForKey:@"objectiveList"]) {
        objectiveList = [[decoder decodeObjectForKey:@"objectiveList"] retain];
    } else {
        objectiveList = [[NSMutableArray alloc] initWithObjects:@"1",@"2",@"3",@"4",@"5", nil];
    }
}

You have two cases: either objectiveList is present, in which case you have previously saved some data, or it is not present and you need to create the default data (1, 2, 3, 4, 5). In the code above, I have changed the first case to retain the array returned by decodeObjectForKey, since Apple's docs state that this method returns an autorelease object. You need to retain it here to prevent the memory from being reused for some other objects that are created later in your app. By not retaining objectiveList, when accessing it later you were probably accessing garbage results (i.e. random memory) rather than what you had just decoded.

On a similar note, in the second case where objectiveList was not already present - i.e. for a new install of the app where there is no saved data present - you are not allocating objectiveList before trying to add objects to it. I have changed this line to actually alloc the object (and therefore the memory required), and then init with the default values you want. Since you were previously trying to add items to an array that had not been created, you would again get garbage data when trying to access the values from it. Note that I assume you are using an NSMutableArray here, but you might also be using an NSMutableSet.

Rob B
  • 1,485
  • 12
  • 16