Reputation: 115322
I've just got burned because an update to my iOS app went live on the App Store and unfortunately it crashes when using a new feature because of a bug in the way my code uses NSUserDefaults
. My app uses registerDefaults
, but the new feature tries to write to a key that doesn't exist after upgrading. It works fine if installing from scratch because the key gets created by registerDefaults
.
To be specific, my app uses nested dictionaries, so before the update the preferences file has this structure:
<dict>
<key>Alpha</key>
<dict>
<key>Key</key>
<string>Value</string>
</dict>
<key>Beta</key>
<dict>
<key>Key</key>
<string>Value</string>
</dict>
<key>Charlie</key>
<dict>
<key>Key</key>
<string>Value</string>
</dict>
</dict>
—and after the update, key Delta is required:
<dict>
<key>Alpha</key>
<dict>
<key>Key</key>
<string>Value</string>
</dict>
<key>Beta</key>
<dict>
<key>Key</key>
<string>Value</string>
</dict>
<key>Charlie</key>
<dict>
<key>Key</key>
<string>Value</string>
</dict>
<key>Delta</key>
<dict>
<key>Key</key>
<string>Value</string>
</dict>
</dict>
My code is falling over when it tries to retrieve the dictionary for Delta from the user defaults in order to write some keys and values to it. For this scenario the dictionary is nil
because after an update it's not in the preferences file.
What's the best practice way to handle this situation? Should I simply check to see if the dictionary for Delta is nil
before attempting to write to it and create it if required, or is there a less belt and braces approach?
Upvotes: 2
Views: 960
Reputation: 69027
I have done a simple test with your data:
add a defaults.plist file to an empty project;
set defaults.plist to contain Alpha/Beta/Charlie keys (as per your example);
use the following code at appDidFinishLaunching time:
NSString* path = [[NSBundle mainBundle] pathForResource:@"defaults" ofType:@"plist"];
NSDictionary* factoryPrefs = [NSMutableDictionary dictionaryWithContentsOfFile:path];
[[NSUserDefaults standardUserDefaults] registerDefaults:factoryPrefs];
NSLog(@"Delta Key: %@", [[NSUserDefaults standardUserDefaults] objectForKey:@"Delta"]);
//-- store some user custom values
NSLog(@"Charlie Key %@", [[NSUserDefaults standardUserDefaults] objectForKey:@"Charlie"]);
[[NSUserDefaults standardUserDefaults] setObject:@"test" forKey:@"Charlie"];
NSLog(@"Charlie Key %@", [[NSUserDefaults standardUserDefaults] objectForKey:@"Charlie"]);
run the app;
Outcome was:
2013-02-23 17:59:10.795 JigSaw[14747:c07] Delta Key (null)
2013-02-23 17:59:10.797 JigSaw[14747:c07] Charlie Key {
Key = Value;
}
2013-02-23 17:59:10.797 JigSaw[14747:c07] Charlie Key test
Then, I changed the content of defaults.plist by adding the Delta key (again, as per your example), and built/ran the app again (without deleting it from the device, of course).
Outcome was:
2013-02-23 18:00:37.840 JigSaw[15040:c07] Delta Key {
Key = Value;
}
2013-02-23 18:00:37.842 JigSaw[15040:c07] Charlie Key test
2013-02-23 18:00:37.842 JigSaw[15040:c07] Charlie Key test
So, it seems that registerDefaults
will correctly handle any newly added key when used like in the example above.
I tend to think that either you are using registerDefaults
incorrectly, or you are doing something elsewhere that is the cause of the crash.
At a more general level, apart from testing the app update scenario (which is trivial to suggest after the fact, and we all learned it the hard way, one way or another), the good practice for me would me ensuring that your app does not crash even in the presence of wrong or unforeseen input.
Upvotes: 1
Reputation: 9039
The pattern I follow to avoid these situations is as follows:
In my ApplicationDelegate +(void)initialize
method I set up the keys and their defaults like so to ensure nothing attempts to touch the defaults before they are setup:
+(void)initialize
{
NSDictionary *factoryPrefs = @{MyNewPrefKey:@"ANewPrefKeyDefaultValue"};
}
[[NSUserDefaults standardUserDefaults] registerDefaults:factoryPrefs];
userSettings = [NSUserDefaults standardUserDefaults];
Next, and immediately after this I have a block of code that based on current version, starts checking for the existence of previous values that imply a user was upgrading (not new). In that case I'm going to have to migrate/update the prefs.
For example, sometimes keys have to be altered or values inserted like if you stored an NSDictionary object with value a, b, c in it in some previous version, but now in this Ver. your depending on (and assuming) a 'd' value is also in there.
In that case registerDefaults
is NOT going to help because as you noted the value was previously created. So YOU have to manually check if 'd' is in there and if NOT insert it with along with the default value. This is sort of the CoreData migration equivalent for Prefs :)
That is what I do. I do it religiously. I check it EVERY release, because... I was burnt, just like you previously by NOT doing it. And then we all know what happens next, you get a serious 1 star a.. whooping in the App Store.
Upvotes: 1