Nsuserdefaults Losing Its Keys & Values When Phone Is Rebooted But Not Unlocked

NSUserDefaults losing its keys & values when phone is rebooted but not unlocked

After a while, Apple recognised this as an official bug. So we are only left with different workarounds until it's solved:

  1. If you need the data while executing BEFORE the phone has been unlocked use one of the following options and set the NSPersistentStoreFileProtectionKey = NSFileProtectionNone option:

    • Save data using Core Data. (If you need to access the DB in background when the phone wasn't unlocked yet AND you don't have sensible information in it, you can add to the options Array the following option: NSPersistentStoreFileProtectionKey = NSFileProtectionNone)
    • Use the Keychain.
    • Use a .plist file.
    • Use custom made files: (e.g.: .txt with a specific format).
    • Any other way you might find comfortable for storing data.

    Choose yours ;)

  2. If you don't need or don't care about the data before the phone
    has been unlocked
    you can use this approach (Thanks @maxf):

    Register to the applicationProtectedDataDidBecomeAvailable: notification and execute the following line of code inside the callback [NSUserDefaults resetStandardUserDefaults]

    This will make you NSUserDefault reload right after your phone has been granted permission to access protected data, helping you to avoid this issue entirely.

Thanks all for the help!

Occasional NSUserDefaults trouble

iOS does some complicated stuff to (almost) seamlessly encrypt data written to the disk, so this kind of bug is definitely possible. Perhaps the file cannot be decrypted for some reason and is deleted instead, reverting NSUserDefaults.

I don't know that is the cause but it seems likely to me.

Also, beware NSUserDefaults saves data to <Application_Home>/Library which is not a safe location. It is intended for "files your application downloads or generates and can recreate as needed".

Perhaps a better location to store your data is <Application_Home>/Documents, which is for data that "cannot be recreated by your app". If your user defaults are important enough for this to be a problem, then it classifies as "user generated content" and should therefore be stored in the Documents folder.

So, I suggest dropping NSUserDefaults since it does not meet your needs, and save the data by writing an NSDictionary to the Documents folder, using either NSCoding or Binary Plist (make sure you set it to NSPropertyListBinaryFormat_v1_0, as that is not the default and should be used on slow flash memory like iOS devices have).

Apple has good documentation and sample code for NSCoding and Plist serialization:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Articles/creating.html#//apple_ref/doc/uid/20000949-BABGBHCA

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/PropertyLists/SerializePlist/SerializePlist.html#//apple_ref/doc/uid/10000048i-CH7-SW1

You could also use core data, which is what I'm using in my app, or SQLite. But if you are only storing "hundreds" of settings I wouldn't go with either of these options. They're generally only a good choice if the data does not fit in RAM. For data that does fit in memory NSCoding and Plist are significantly faster and easier to work with.

And also read up on "Where You Should Put Your App‘s files": https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

iPhone: preserve NSUserDefaults values when application is killed

NSUserDefaults is actually used to store values permanently, in fact if you create any Settings for your program they will be saved as NSUserDefaults.

I think the problem is that you are not saving it with the same key you are retrieving. Try saving like this:

  //To save
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:appDefaults forKey:kFavouriteItemsKey];
[[NSUserDefaults standardUserDefaults] synchronize];

//To retrieve
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *favourites = [[standardUserDefaults objectForKey:kFavouriteItemsKey] mutableCopy];

For the dictionary try:

 NSDictionary *myDictionary= [[NSUserDefaults standardUserDefaults] objectForKey:kFavouriteItemsKey];

// Create a mutable dictionary to replace the old immutable dictionary
NSMutableDictionary *myMutableDictionary= [NSMutableDictionary dictionaryWithCapacity:[myDictionary count]+1];

// transfer the old dictionary into the new dictionary
[myMutableDictionaryaddEntriesFromDictionary:myDictionary];

// Now add the data
[myMutableDictionary setObject:myObject forKey:myKey];

Do NSUserDefaults persist through an Update to an app in the Appstore?

They are usually not reset unless the user deletes the app. For basic data, NSUserDefaults is the best way to save data such as preferences, dates, strings etc. If you are looking to save images and files, the file system is a better bet.

NSHTTPCookieStorage loosing cookies if app starts before phone has been unlocked at least once after a reboot

Seems that iOS is having this same issue but with cookies stored on NSHTTPCookieStorage.

I've already created a radar so feel free to add more so this issue is pushed up a bit in the queue. Radar number is 16237165.

In the meanwhile, you can perform the following workarounds:

  1. Handle cookies manually (aka not rely on iOS automatically
    managing cookies and storing them in the NSHTTPCookieStorage
    class). But DON'T use NSUSerDefaults because its having the
    same issue.

  2. Prevent all network activity before phone has been unlocked at least once. This is the option we took and we did the following:

    • Create a file in disk with the NSFileProtectionCompleteUntilFirstUserAuthentication permission.
    • Before making any network call, check if the app is able to read that file. If it is, it means phone was unlocked at least once, so you are good to do the call without loosing cookies.

Here is a code sample:

static BOOL phoneIsUnlocked = NO;

//If this var is true, then avoid re checking if file has permissions (Cause if it was granted permissions once, it will have access now)
if(phoneIsUnlocked) return phoneIsUnlocked;

//If phone has never been unlocked, prevent all networking stuff just to make sure cookies are not lost due to an ios7 bug.
//Creates a file with NSFileProtectionCompleteUntilFirstUserAuthentication permissions. If the app is able to read it, it means the phone was unlocked at least once after a reboot.

//Get the file path
NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *fileName = [documentsDirectory stringByAppendingPathComponent:@"a.secure"];

//create file if it doesn't exist
if(![[NSFileManager defaultManager] fileExistsAtPath:fileName])
[[NSFileManager defaultManager] createFileAtPath:fileName
contents:[@"secure" dataUsingEncoding:NSUTF8StringEncoding]
attributes:[NSDictionary dictionaryWithObject:NSFileProtectionCompleteUntilFirstUserAuthentication
forKey:NSFileProtectionKey]];

NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:fileName];
phoneIsUnlocked = file != nil; //If file is not nil, the phone has been unlocked
[file closeFile];

NSUserDefaults not saving

I don't notice anything wrong in the NSUserDefaults code, per se, though it's only grabbing the values when the view loads, not when the values are updated. To fix that, you could send a notification to let all other interested view controllers aware of the state change. In -[NewEntry saveButton:], after saving the values in NSUserDefaults, add

[[NSNotificationCenter defaultCenter] postNotificationName:@"ValuesChanged"
object:self];

to send a notification. Somewhere in Projects.m (init is a good place), subscribe to the notification with

[[NSNotificationCenter defaultCenter] addObserver:self 
selector:@selector(valuesChanged:)
name:@"ValuesChanged"
object:nil];

This causes the notification center to call back to your valuesChanged: method when the notification is posted. We'll be updating the UI in pretty much the same way we did in viewDidLoad, so let's factor that code out:

- (void)reloadData
{
categoryselected = [[NSUserDefaults standardUserDefaults] integerForKey:@"category"];
NSLog(@"category selected %i", categoryselected);

// XXX - note the following strings are returned autoreleased. If they're
// stored in ivars, it's a good idea to retain them, even though we know
// they're retained by the arrays below.

titlestring = [[NSUserDefaults standardUserDefaults] objectForKey:@"titletextprojects"];
detailsstring = [[NSUserDefaults standardUserDefaults] objectForKey:@"detailsprojects"];

[tabledata release];
tabledata = [[NSArray alloc] initWithObjects:titlestring, nil];

[tablesubtitles release];
tablesubtitles = [[NSArray alloc] initWithObjects:detailsstring, nil];

[tableView reloadData];
}

- (void)viewDidLoad
{
[super viewDidLoad];
[self reloadData];
}

- (void)valuesChanged:(NSNotification*)notification
{
[self reloadData];
}

One last thing: all those strings, the keys for NSUserDefaults and the notification name @"ValuesChanged"? They really should be defined as constants so that there's only one "true" version of the string. First, this saves you from the case where you accidentally misspell it in one place and wonder why the values aren't syncing up. It can take hours to figure out what's going on when that happens. (Speaking from experience here.) Second, the compiler can then check that you have the right name and the IDE's autocomplete (when it actually works) can suggest key names for you. You can do a #define in a shared header somewhere

#define kDefaultsKeySelectedCategory @"category"

and the linker will probably create a single constant instance of the string shared between every place it's used. Still, if I change the string in that define and Xcode's being cranky and doesn't recompile every source file that uses it, we're back to the case where the key is spelled differently in different places. No good. A fancier way to do this that ensures there's only one copy of the string is to declare

extern NSString* const kDefaultsKeySelectedCategory;

in the header file, then

NSString* const kDefaultsKeySelectedCategory = @"category";

in the .m file. Another thing I like about this way is it hides the implementation details. Nobody needs to know what the specific string is, so it shouldn't be in the header file.



Related Topics



Leave a reply



Submit