02fentym
02fentym

Reputation: 1772

Saving OS X Game Data Using NSData and NSMutableArrays

I am trying to store the hi-score of several levels in a NSMutableArray, which will then be saved in a file in the Documents folder. I know I can use plists, but I don't want the content to be modified by the user. It appears that NSMutableArray *hiscore is not being initialized and I'm not sure how to fix this. For primitives it seems fine, but for objects it's not working.

GameData.h

@interface GameData : NSObject <NSCoding>

@property (assign, nonatomic) int level;
@property (assign, nonatomic) NSMutableArray *hiscore;

+(instancetype)sharedGameData;
-(void)save;
-(void)reset;

@end

GameData.m

#import "GameData.h"

@implementation GameData

static NSString* const GameDataLevelKey = @"level";
static NSString* const GameDataHiscoreKey = @"hiscore";

- (instancetype)initWithCoder:(NSCoder *)decoder {
    self = [self init];
    if (self) {
        _level = [decoder decodeDoubleForKey: GameDataLevelKey];
        _hiscore = [decoder decodeObjectForKey:GameDataHiscoreKey];
    }
    return self;
}

+ (instancetype)sharedGameData {
    static id sharedInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [self loadInstance];
    });

    return sharedInstance;
}

+(instancetype)loadInstance {
    NSData* decodedData = [NSData dataWithContentsOfFile: [GameData filePath]];
    if (decodedData) {
        GameData* gameData = [NSKeyedUnarchiver unarchiveObjectWithData:decodedData];
        return gameData;
    }

    return [[GameData alloc] init];
}

-(void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeDouble:self.level forKey: GameDataLevelKey];
    [encoder encodeObject:self.hiscore forKey:GameDataHiscoreKey];
}

+(NSString*)filePath {
    static NSString* filePath = nil;
    if (!filePath) {
        filePath =
        [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]
         stringByAppendingPathComponent:@"gamedata"];
    }
    return filePath;
}

-(void)save {
    NSData* encodedData = [NSKeyedArchiver archivedDataWithRootObject: self];
    [encodedData writeToFile:[GameData filePath] atomically:YES];
    /*[_hiscore writeToFile:[GameData filePath] atomically:YES];*/
}

-(void)reset {
    self.level = 0;
}

@end

LevelScene.m

#import "GameData.h"
...
[[[GameData sharedGameData] hiscore] addObject:@1500];
[[GameData sharedGameData] save];

Upvotes: 0

Views: 59

Answers (1)

zpasternack
zpasternack

Reputation: 17898

A couple things:

First, in initWithCoder:, if there's no game file already (as would be the case on first launch, or if it was saved with no scores), _hiscore will end up nil, so you won't be able to add any scores to it. I'd check it for nil after the call to decodeObjectForKey:, and if it's nil, initialize it to an empty array.

- (instancetype)initWithCoder:(NSCoder *)decoder {
    self = [self init];
    if (self) {
        _level = [decoder decodeDoubleForKey: GameDataLevelKey];
        _hiscore = [decoder decodeObjectForKey:GameDataHiscoreKey];
        if( _hiscore == nil ) {
            _hiscore = [NSMutableArray array];
        }
    }
    return self;
}

Second, GameData owns _hiscore, so it's going to need a strong reference to it.

@property (strong, nonatomic) NSMutableArray *hiscore;

Edit: The general rule (see the Memory Management section of Apple's Cocoa Core Competencies Guide) is:

You own any object you create by allocating memory for it or copying it.

That generally means that any object you create with methods containing alloc or copy, but it also pertains to convenience methods like array, etc. Note that [NSMutableArray array] is exactly equivalent to [[NSMutableArray alloc] init]; the only reason to use the former is that it's less typing (which many, including myself, are a fan of).

Then,

If you own an object, either by creating it or expressing an ownership interest, you are responsible for releasing it when you no longer need it.

Pre-ARC, that meant calling release or autorelease on it.

In an ARC world, memory management is mostly taken care of for you, but you still need to kinda understand the concepts behind it for it to make sense. Basically, if you create an object (using methods containing alloc or copy, or convenience methods such as array), then you own that object. You express ownership of that object by creating a strong reference to it. If you fail to do so (say, your reference is weak or assign), then ARC will (rightly) assume you're done with that object, and will release it as soon as it decides you're done with it. In the code above, it's a safe bet ARC chose to release _hiscore as soon as initWithCoder returned. Later, your code tries to access _hiscore, but it's already been released, and so you crash.

Without code to initialize _highscore, it didn't crash. Because _highscore was always nil, and sending a message to nil is totally fine in Objective-C; nothing happens. But if you do initialize it, and only have it set to assign, then it's no longer nil. Once ARC releases it, _highscore is pointing to an object that no longer exists, and that's what causes the crash.

Upvotes: 1

Related Questions