Reputation: 1772
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
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