8

I am using NSUserDefaults to store whether the app EULA and PP have been accepted (among other things) This works fine in general. I can start, exit then return to the app and it reads the value fine. I can kill the app and restart - reads the defaults fine. I can restart the phone, then restart the app and it reads the defaults fine.

But when the phone restarts from a flat battery, I open the app and am prompted to accept my EULA an PP again. This only happens on my iPhone5 on IOS7. I have a 3GS on IOS6 which does not exhibit the same behaviour.

I suspect it may be a similar issue to the one solved here, but this refers to permission issues in the keychain. Would the same permissions issues apply to NSUserDefaults?

Has anyone experienced similar issues on IOS7 with NSUserDefaults?

muldercnc
  • 421
  • 2
  • 10
  • There are other similar unanswered questions on SO. Looks like maybe an unresolved bug. You could work around it using NSCoder or Core Data. – Aaron Brager Dec 09 '13 at 07:14

1 Answers1

22

So after experimentation and googling the conclusions I came to were as follows:

The Significant update change and in fact any process that launches the app behind the lock screen is the problem here. The file .plist the [NSUserDefaults defaultUser] loads is protected (NSFileProtectionCompleteUntilFirstUserAuthentication) so that it is not accessible until after the first unlock after the app is launched. So if a process launches your app in the background and your app tries to access the defaultUser user defaults it can't load the file and so gives you a new blank set of user defaults.

What happened here in my case is that the app then went into a state of waiting for the EULA and PP to be accepted since it read from the defaults (that couldn't be read) that they hadn't been accepted yet. After unlocking the phone and re-opening the app - which please note is already 'launched' - there are some processes that write to NSUserDefaults, some in my app and some in libraries my app uses. In most of those cases I was calling synchronise on the defaults therefore blasting away the old defaults that couldn't be read. I imagine this could be the case for a lot of people.

There are a few different solutions.

First I wrote a class equivalent to NSUserDefaults wrapping an NSMutableDictionary and saving the dictionary to a .plist in Library/Application Support. I changed the protection on the file to NSFileProtectionNone. Note this is not advisable if you store sensitive information in this file. Also note that you have to set the permissions on the file every time you write it. Something like:

NSError *error;   
BOOL saved = [defaultsDic writeToURL:defaultsFileUrl atomically:YES];
[[NSFileManager defaultManager] setAttributes:[NSDictionary dictionaryWithObject:NSFileProtectionNone forKey:NSFileProtectionKey]ofItemAtPath:[defaultsFileUrl path] error:&error];

This method works fine but as it turns out I had another issue with data I was reading and writing from the keychain. See the link in my question above, its the same problem. The keychain values have the same protection up until the first unlock after the app is launched. I didn't want to remove the protection from the keychain and I actually wasn't super comfortable with removing the protection from my Custom user defaults either.

So the next solution is to actually solve the problem. Don't try to access protected data if the app is launched behind the lock screen! This means I have to detect that the app is launched behind the lock screen and then wait until the app is unlocked before i proceed to read my user defaults and keychain values.

The first requirement is to check on application launch if protected data is available so perhaps in applicationDidLaunch or somewhere else suitable.

[[UIApplication sharedApplication]isProtectedDataAvailable]

If this is not true when the app is launched then you are behind the lock screen. You should pause at this point and refrain from any operations that access NSUserDefaults or Keychain (or any protected file for that matter!). You then need to wait for a signal that the protected data has become available. The appDelegate receives the following when the user unlocks the lock screen:

-(void)applicationProtectedDataDidBecomeAvailable:(UIApplication *)application

Once you receive that you can carry on execution of your application.

In my case I control everything in a singleton class. When that class is created (which happens only at app launch) I check to see if protected data is available or not and subscribe to NSNotificationCenter for the same notification:

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(applicationProtectedDataDidBecomeAvailable) name:UIApplicationProtectedDataDidBecomeAvailable object:nil];

So with this second method, the problem is solved and the data remains protected and everyone is happy.

muldercnc
  • 421
  • 2
  • 10
  • Thanks for your answer, do you know if it's normal that files in NSDocumentDirectory behave like NSUserDefaults when the screen is locked? – franck Mar 31 '14 at 15:46
  • @muldercnc thanks a lot for sharing this. If the user unlocks his screen say 30 minutes after the background app refresh was initiated, can we still fetch data in the background then? (as we have less than a minute before OS cuts us off) Also, what to do with the completion handler of UIBackgroundFetchResult? – Serluca Aug 21 '15 at 09:39
  • @Serluca I believe so. Since it generates an event your app will respond to the event and should happily do that in the background even after 30 minutes. You'll have to try it of course to confirm. I use the Device console in Xcode to monitor the output XCODE=>Window=>Devices, use the console option for the device you are testing with (since you can restart your device while debugging). – muldercnc Aug 24 '15 at 05:04
  • How app be launched when in lock state? How app be launched by another app without user's interaction? – user392412 Oct 14 '16 at 10:59
  • @user392412 Location services launches your app as a result of a Significant update change, if you have subscribed to those. In fact there are a few different cases where the OS will start your app. Eg. Region Monitoring. These are useful to keep your app alive after the user reboots their phone or kills the app. – muldercnc Nov 09 '17 at 00:27