views:

1340

answers:

2

I've been experiencing very inconsistent results while developing an iPhone app and trying to save preferences via the standard NSUserDefaults mechanism. I am using code almost straight out of the iPhone Developer's Cookbook by Erica Sadun (fantastic book btw), it looks like this:

(void) updateDefaults
{
    NSMutableArray *spells =  [[NSMutableArray alloc] init];
    NSMutableArray *locs = [[NSMutableArray alloc] init];

    for (DragView *dv in [boardView subviews]) 
    {
     [spells addObject:[dv whichSpell]];
     [locs addObject:NSStringFromCGRect([dv frame])]; 
    }

    [[NSUserDefaults standardUserDefaults] setObject:spells forKey:@"spells"];
    [[NSUserDefaults standardUserDefaults] setObject:locs forKey:@"locs"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    [spells release];
    [locs release];
}

The values are saved, sometimes...and restored, sometimes. I can't get an exact bead on what does or does not make it work.

Does anyone else have any similar experiences? Any suggestions on what might make it work? Is the synchronize method the best way to force a disk write and make the values save, or is there something better (both for production, as well as simulator).

Thanks Ryan

+1  A: 

You should be using an NSKeyedArchiver for saving your arrays, such as:

[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:spells] forKey:@"spells"];
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:locs] forKey:@"locs"];
[[NSUserDefaults standardUserDefaults] synchronize];

You should also make sure your spells class implements the NSCoding protocol (encodeWithCoder: and initWithCoder:), if it's a custom class. It looks like your locs are NSStrings, which will archive just fine.

You'll also need to do something like

NSData *dataRepresentingSavedSpells = [currentDefaults objectForKey:@"spells"];
if (dataRepresentingSavedSpells != nil)
{
 NSArray *oldSpells = [NSKeyedUnarchiver unarchiveObjectWithData:dataRepresentingSavedSpells];
}

To load the old values from the defaults.

I use synchronize to write to disk on exit, and it's been very reliable in my experience.

Brad Larson
Ok, thank you. I'm mildy confused as to why I need to use a NSKeyedArchiver for storing an array...will the standard setObject not work accurately or sufficiently for some reason? I had not run across NSKeyedArchiver before, so I am not familiar with more than the intro paragraph from the docs.
ryan.scott
I believe that you need to use it to make sure that your custom NSCoding-compliant classes are archived properly. I think setObject only works with arrays of supported objects. I could be wrong, though.
Brad Larson
+2  A: 

On Mac OS X, and probably also on iPhone OS, the defaults database can only contain property list objects: NSString, NSNumber, NSArray, NSDictionary, and NSData.

It's not clear from your code snippet what type a spell is, but if it isn't one of the above, it won't get stored in the defaults database. Your options are:

  1. Write a method that will convert your object to and from a plist representation. In other words, a method to create an NSDictionary from your object and another method to initialize the object using an NSDictionary. You could also choose to store it as an NSArray or NSString.
  2. Make your object implement the NSCoding protocol and then used NSKeyedArchiver to convert the object to an NSData object, and NSKeyedUnarchiver to convert it back.
benzado
yea, at some point I realized my custom class was not supported, and have gone with a version of your option #1. I have a canonical string representation of the Spell object, which I use to dynamically recreate the object on load. later on, I will need to go down the option #2 route, but not yet.
ryan.scott