Loading a Singleton's state from NSKeyedArchiver
Asked Answered
S

2

7

I have a class that I've made into a singleton and am able to save it's state using an NSKeyedArchiver, but I can't wrap my head around pulling it's state back out.

In the function that does the loading I have

Venue *venue = [Venue sharedVenue];
NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[self dataFilePath]];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
venue = [unarchiver decodeObjectForKey:@"Venue"];
[unarchiver finishDecoding];

With this code, what does decodeObjectForKey return? It can't really be another instance of Venue and it's not loading in any of the saved values. Before I converted it to a singleton saving and loading was working fine.

Sensorium answered 18/7, 2009 at 23:18 Comment(0)
D
8

Here's where I think this is going wrong. You're familiar with NSCoding and typically adopting it by making your object codable via encodeWithCoder: and initWithCoder: overrides. What will make this all simpler is that you can still use NSCoding, and NSCoders without overriding those methods. You can encode the state of the shared object without encoding the shared obejct itself. This prevents the awkward question of decoding the shared object.

Here's an example of what I think you could do:

@implementation MySharedObject
+ (id)sharedInstance {
    static id sharedInstance = nil;
    if (!sharedInstance) {
        sharedInstance = [[MyClass alloc] init];
    }
}

- (id)init {
    if ((self = [super init])) {
        NSData *data = /* probably from user defaults */
        if (data) { // Might not have any initial state.
            NSKeyedUnarchiver *coder = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease];
            myBoolean = [coder decodeBoolForKey:@"MyBoolean"];
            myObject = [[coder decodeObjectForKey:@"MyObject"] retain];
            [coder finishDecoding];
        }
    }
    return self;
}

- (void)saveState {
    NSMutableData *data = [NSMutableData data];
    NSKeyedArchiver *coder = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease];
    [coder encodeBool:myBoolean forKey:@"MyBoolean"];
    [coder encodeObject:myObject forKey:@"MyObject"];
    [coder finishEncoding]
    // Save the data somewhere, probably user defaults...   
}
@end

Here we have a shared object, and it uses keyed archive to persist configuration, but we don't encode the shared object itself. This avoid that awkward question of decoding a second instance of the singleton class.

Dworman answered 20/7, 2009 at 1:35 Comment(2)
This answer makes a bit more sense to me, just because I can wrap my head around it. The other's init seems correct but a bit harder to conceptualize.Sensorium
Great answer...just an FYI in case anyone stumbles upon this question, I believe the init method should use an NSKeyedUnarchiver.Preservative
D
3

You need to do the loading in the singleton itself what is going on here is you create the single, you assign an lval to the singleton, you then create a new object and reassign the lval to that new object WITHOUT modifying the singleton. In other words:

//Set venue to point to singleton
Venue *venue = [Venue sharedVenue];

//Set venue2 to point to singleton
Venue *venue2 = [Venue sharedVenue];

NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[self dataFilePath]];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

//Set venue to unarchived object (does not change the singleton or venue2)
venue = [unarchiver decodeObjectForKey:@"Venue"];
[unarchiver finishDecoding];

What you want to do is deal with this in the sharedVenue. There are a couple of ways people do singletons, so I can't be sure what you are doing, but lets assume sharedVenue currently looks something like this:

static Venue *gSharedVenue = nil;

- (Venue *) sharedVenue {
  if (!gSharedVenue) {
    gSharedVenue = [[Venue alloc] init];
  }

  return gSharedVenue;
}

Assuming that is the case you want to change it to load the object into the global backing the singleton:

static Venue *gSharedVenue = nil;

- (Venue *) sharedVenue {
  if (!gSharedVenue) {
    NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[self dataFilePath]];
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    [data release];

    gSharedVenue = [unarchiver decodeObjectForKey:@"Venue"];
    [unarchiver finishDecoding];
    [unarchiver release];
  }

  if (!gSharedVenue) {
    gSharedVenue = [[Venue alloc] init];
  }

  return gSharedVenue;
}

Obviously you need to somehow convey the actual path to archived object file.

EDIT BASED ON COMMENT:

Okay, if you are using the alloc based singleton you need to deal with this in the classes init method:

- (id) init {
  self = [super init];

  if (self) {
    NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[self dataFilePath]];
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    [data release];

    Venue *storedVenue = [unarchiver decodeObjectForKey:@"Venue"];
    [unarchiver finishDecoding];
    [unarchiver release];

    if (storeVenue) {
       [self release];
       self = [storedVenue retain];
    }

  }

  return self;
}
Desmonddesmoulins answered 18/7, 2009 at 23:30 Comment(2)
Ok, I think I understand that, let me give it a try. As to the way I'm implementing my singleton, I'm using this guy's macro which is pretty awesome: cocoawithlove.com/2008/11/…Sensorium
He uses a different pattern, you need to handle this in your singletons init method. You need to do one tricky thing, which is return a non-super thing, I am appending how you do this to the answerDesmonddesmoulins

© 2022 - 2024 — McMap. All rights reserved.