3

I am writing a basic game, I am using a GameStateManager which is a singleton and handles all state management, such as saves, loads, delete methods.

I have a separate singelton which handles all the Game stuff. The game object is inside the game state manager so I can save it out using NSCoding and Archiving.

When I save the state there appears to be no issues and the Game object (singleton) is saved properly.

However, when I try to load the state (by relaunching the app), the game object is always null.

Strangley, if I remove the singleton properties and make it a standard class, this issue goes away and I can load the class and all its properties without any major issues.

In summary, I do this:

  1. GameStateManager = Singleton, handles all game state management (load, save) and has a game object (game)

  2. Game = Singleton which handles things within the game and has NSCoding protocol employed.

  3. Saving the game state with the game object is fine, the object is clearly there.

  4. Loading the game state seems to make the game object null. It should be there, but for some reason it never loads it.

  5. If I remove all the properties that make the game class a singelton and make it a normal class, the issue seems to go away.

I think it has something to do with the fact that Game is never init'd, but this does not make sense because I can get Game to load when it has no singleton properties.

Code now follows.

// GameStateManager.m

-(void)saveGameState
{
    CCLOG(@"METHOD: saveGameState()");

    self.lastSaveDate   = [NSDate date];

    NSMutableData *data;
    NSString *archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent:kGameSaveFile];
    NSKeyedArchiver *archiver;
    BOOL result;

    data = [NSMutableData data];
    archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

    [archiver encodeObject:self.lastSaveDate forKey:@"lastSaveDate"];
    [archiver encodeObject:self.game forKey:@"game"];
    [archiver finishEncoding];
    result = [data writeToFile:archivePath atomically:YES];
    [archiver release];

}

-(void)loadGameState
{
    CCLOG(@"METHOD: loadGameState()");


    NSData *data;
    NSKeyedUnarchiver *unarchiver;
    NSString *archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent:kGameSaveFile];

    data = [NSData dataWithContentsOfFile:archivePath];
    unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

    // Customize unarchiver here
    self.game = [unarchiver decodeObjectForKey:@"game"];
    self.lastSaveDate = [unarchiver decodeObjectForKey:@"lastSaveDate"];

    [unarchiver finishDecoding];
    [unarchiver release];


    CCLOG(@"Last Save Date = %@", self.lastSaveDate);
    NSLog(@"game = %@", self.game);
}

// END OF GAMESTATEMANAGER

// -------------------------

 // Game.h
    @interface Game : NSObject
<NSCoding>
{
    NSMutableArray *listOfCities;
    NSMutableArray *listOfColors;
    NSMutableArray *listOfPlayers;
}

@property (nonatomic, retain) NSMutableArray *listOfCities;
@property (nonatomic, retain) NSMutableArray *listOfColors;
@property (nonatomic, retain) NSMutableArray *listOfPlayers;

+(Game *) sharedGame;


 //
// Game.m
// This is a cut-down version of the game object
// Game is a singelton
// The listOfCities, etc are arrays
//
@implementation Game
SYNTHESIZE_SINGLETON_FOR_CLASS(Game)
@synthesize listOfCities, listOfPlayers;

#pragma mark - NSCoding


-(id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];

    if (self) 
    {
        self.listOfCities = [aDecoder decodeObjectForKey:@"listOfCities"];
        self.listOfPlayers = [aDecoder decodeObjectForKey:@"listOfPlayers"];

        NSLog(@"Cities = %d", [self.listOfCities count]);
        NSLog(@"Players = %d", [self.listOfPlayers count]);
    }

    return self;
}

-(void)encodeWithCoder:(NSCoder *)aCoder
{
    // Archive objects
    NSLog(@"Cities = %d", [self.listOfCities count]);
    NSLog(@"Players = %d", [self.listOfPlayers count]);

    [aCoder encodeObject:self.listOfCities forKey:@"listOfCities"];
    [aCoder encodeObject:self.listOfPlayers forKey:@"listOfPlayers"];
}

@end

My question is, how do I successfully save and load an Objective C singelton using NSCoding, and the Archiver?

Edit;

I have tried:

// In the GameStateManager
#pragma mark - NSCoding

-(id)initWithCoder:(NSCoder *)decoder {
    if (self = [super init]) {
        self.lastSaveDate = [[decoder decodeObjectForKey:@"lastSaveDate"] retain];
        self.game = [[decoder decodeObjectForKey:@"game"] retain];
    }
    return self;
}
-(void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.lastSaveDate forKey:@"lastSaveDate"];
    [encoder encodeObject:self.game forKey:@"game"];
}

and

// In Game.m
    self.listOfCities = [[aDecoder decodeObjectForKey:@"listOfCities"] retain];
        self.listOfPlayers = [[aDecoder decodeObjectForKey:@"listOfPlayers"] retain];

        NSLog(@"Cities = %d", [self.listOfCities count]);
        NSLog(@"Players = %d", [self.listOfPlayers count]);
zardon
  • 2,910
  • 6
  • 37
  • 58

4 Answers4

6

Since you're dealing with a singleton, you only ever want a single instance of the class to exist at any time. So you will want to archive and unarchive that single instance only.

Consider this code (assumes ARC is enabled):

@interface Singleton : NSObject <NSCoding>

+ (id)sharedInstance;
@property (strong, nonatomic) NSString *someString;

@end

@implementation Singleton

+ (id)sharedInstance {
    static Singleton instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[Singleton alloc] init];
    });
    return instance;
}

#pragma mark - NSCoding implementation for singleton

- (id)initWithCoder:(NSCoder *)aDecoder {
    // Unarchive the singleton instance.
    Singleton *instance = [Singleton sharedInstance];

    [instance setSomeString:[aDecoder decodeObjectForKey:@"someStringKey"]];

    return instance;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    // Archive the singleton instance.
    Singleton *instance = [Singleton sharedInstance];

    [aCoder encodeObject:[instance someString] forKey:@"someStringKey"]];
}

@end

Using this template, you can archive/unarchive the singleton, like this:

// Archiving calls encodeWithCoder: on the singleton instance.
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:[Singleton sharedInstance]] forKey:@"key"];

// Unarchiving calls initWithCoder: on the singleton instance.
[[NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:@"key"]];
Eric Baker
  • 357
  • 4
  • 14
  • this seems like it might work, but a little hackish, as the init method is not creating a new object like it should. I wonder if ARC would act weird to this. – Matthew Knippen Oct 19 '13 at 17:47
1

try this in your initWithCoder method :

self.listOfCities = [[aDecoder decodeObjectForKey:@"listOfCities"] retain];
self.listOfPlayers = [[aDecoder decodeObjectForKey:@"listOfPlayers"] retain];
moxy
  • 1,634
  • 1
  • 11
  • 19
  • Okay, will do, and I'll report back in a moment – zardon Jul 15 '12 at 12:36
  • It didn't seem to fix it. I think I'll just keep it an object within the GameStateManager for the moment, maybe I'm just doing it wrong. – zardon Jul 15 '12 at 14:38
  • I think this was causing the problem `self.game = [[[Game alloc] init] autorelease];` but I am experimenting with some ideas – zardon Jul 15 '12 at 16:48
  • I'm not sure what was causing the problem, but I've decided to stick everything inside the GameStateManager just to get the game finished. Additionally I found out that from my testing that NSCoder doesn't appear to work with Objects that rely on inheritance and ruined 4 months of work – zardon Jul 16 '12 at 20:52
  • @zardon : inheritance works only if for each class called you have the correct protocol (NSCoding) and methods (encodeWithCoder and initWithCoder) implemented – moxy Jul 16 '12 at 21:40
1

Easiest way:

1. Load singleton:

+ (AppState *)sharedInstance
{
    static AppState *state = nil;
    if ( !state )
    {
       // load NSData representation of your singleton here.
        NSData *data =[[NSUserDefaults standardUserDefaults] objectForKey:@"appStateData"];

        if ( data )
        {
             state = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        }
        else
        {
            state = [[AppState alloc] init];
        }
    }
    return state;
}

2. Save singleton on a disk

- (BOOL)save
{
    NSData *appStateData = [NSKeyedArchiver archivedDataWithRootObject:self];
    // now save NSData to disc or NSUserDefaults.
    [[NSUserDefaults standardUserDefaults] setObject:appStateData forKey:@"appStateData"];
}

3. Implement NSCoding methonds:

- (id)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;

Done!

skywinder
  • 21,291
  • 15
  • 93
  • 123
0

I'm using a hybrid approach encompassing answers by Eric Baker and skywinder.

+ (GameStateManager *)sharedInstance {
    static GameStateManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"key"];
        if (data) {
            instance = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        } else {
            instance = [[GameStateManager alloc] init];
        }
    });
    return instance;
}
raidfive
  • 6,603
  • 1
  • 35
  • 32