43

We are currently experiencing the following weird issue with our iPhone app. As the title says, NSUserDefaults is losing our custom keys and values when phone is rebooted but not unlocked, and this is happening on a very specific scenario.

Context:

  • We are using the NSUserDefaults in the app to store user data (e.g. username).

  • Our app has Location enabled on Background Mode.

  • We are experiencing this issue only when distributing over the air or by Testflight. If I drag and drop the .ipa (same that was distributed over the air) into my phone using Xcode I don't experience this issue.

Situation: The user installs the app, logs in and the username is stored on the NSUserDefaults successfully. Then, the user switches OFF they device and turns it back ON and lets the phone sit around for some time before unlocking the screen.

Problem: If in that time a significant location change is triggered, the app comes to live on background but the NSUserDefaults is empty (Only has some keys from apple but none of our custom keys). Then, the NSUserDefaults never gets this keys recovered no matter what you do (e.g. if you unlock your phone and open the app you will see the keys are still missing).

Any help or idea will be truly appreciated :)

ToolmakerSteve
  • 5,893
  • 8
  • 67
  • 145
mp3821
  • 751
  • 1
  • 9
  • 18
  • 3
    Have you called `[[NSUserDefaults standardUserDefaults] synchronize];` after setting your user name to make sure the data is flushed? If so, you might have found a bug and need to file a Radar. – DarkDust Nov 28 '13 at 15:00
  • Hi DarkDust, yes i've synchronised both after setting the username and on AppicationDidEnterBackground. I've also explored the NSUserDefault and the keys are there on the ApplicationWillTerminate event. – mp3821 Nov 28 '13 at 15:18
  • Already submitted the bug on Radar, let see what happens. Thanks DarkDust for the help. In the meanwhile im still looking for the error and im open to other suggestions :) – mp3821 Nov 28 '13 at 15:49
  • Hi @TonyMkenu, thanks for the reply. I agree that it is the expected behaviour for the keychain, which is a secure store. But the NSUserDefaults isn't supposed to be a secure place to store data, so i don't think any security permission clearance should be needed in order to access it. If not for the NSUserDefault we don't have any other non-secure place to store data that could be accessed on our situation. Unlocking the keychain unfortunately is not an option for us cause we use it to store other info that must remain protected (e.g. user password). – mp3821 Nov 30 '13 at 15:07
  • 1
    I'm seeing a similar problem @mp3821. There is also a thread on the [Apple Developer forums (login required) with some more reports of this issue too](https://devforums.apple.com/message/923752#923752) – Glen T Dec 02 '13 at 23:31
  • Thanks for the thread @Glen T. Nice place to follow up this issue. Right now its seems that many users are experiencing this, so i guess we will have to wait for apple to respond to the Radar. In the meantime, im migrating all this data to NSCoreData (though its a very painful process). – mp3821 Dec 03 '13 at 13:27
  • What about using another plist file? That would be less painful, wouldn't it? – Victor Engel Dec 30 '13 at 06:03

4 Answers4

32

I was having a very similar issue. Background the application. Use other memory heavy applications till my application gets jettisoned from memory. (You can observe this event if you have your device plugged and xcode running the build. Xcode will tell you "application was terminated due to memory pressure). From here if your application is registered for background fetch events, it will wake up at some point and get relaunched but into the background. At this point if your device is locked, your NSUserDefaults will be null.

After debugging this case for days, I realized it wasn't that NSUserDefaults was being corrupted or nilled out, it was that the application has no access to it due to device lock. You can actually observe this behavior if you manually try to download the application contents via xcode organizer, you'll notice that your plist which stores the NSUserDefaults settings is no present if your device remains locked.

Ok so NSUserDefaults is not accessible if application is launched into the background while device is locked. Not a big deal but the worst part of this is, once the application is launched into the background it stays in memory. At this point IF the user then unlocks the device and launches the application into the foreground, you STILL do not have anything inside NSUserDefaults. This is because once the application has loaded NSUserDefaults into memory (which is null), it doesn't know to reload it once device becomes unlocked. synchronize does nothing in this case. What I found that solved my problem was calling

[NSUserDefaults resetStandardUserDefaults] inside the applicationProtectedDataDidBecomeAvailable method.

Hope this helps someone. This information could have saved me many many hours of grief.

animaonline
  • 4,399
  • 3
  • 25
  • 53
maxf
  • 391
  • 3
  • 5
  • Cool information. This is a great solution for people who is having this issue and doesn't need that info in a pre applicationProtectedDataDidBecomeAvailable environment. (Which regrettably wasn't my situation). Thanks for the feedback anyways! – mp3821 Apr 16 '14 at 17:34
29

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!

Shebuka
  • 2,965
  • 1
  • 23
  • 41
mp3821
  • 751
  • 1
  • 9
  • 18
  • Do you have a radar that can be duped so we can get this issue pushed up the flagpole? – Bill Burgess Jan 18 '14 at 13:20
  • The radar I sent was marked as a duplicate of this one: 10535951. It basically has the same description explained in my initial post, so if you can fill another radar we might get this issue pushed up a bit in the queue. :) – mp3821 Jan 20 '14 at 13:50
  • @mp3821 Did you observe that there was no such bug when using other way of storing the data, a .plist for example? Was your custom .plist indeed accessible during the specific scenario you described in your question? – Aurelien Porte Jul 16 '14 at 23:11
  • 1
    @AurelienPorte the .plist is just like any other file. As long as you create it with NSPersistentStoreFileProtectionKey = NSFileProtectionNone option you won't have this problem (Which is caused due to a lack of permission over most of iOS data before unlocking the screen) before the phone has been unlocked. – mp3821 Jul 28 '14 at 20:27
  • 1
    Just to add, since this helped me out. It always seems a little sketchy to have no protection, so there are some little better options like: NSFileProtectionCompleteUntilFirstUserAuthentication (The file is stored in an encrypted format on disk and cannot be accessed until after the device has booted.). – kgaidis Sep 17 '14 at 23:32
  • 2
    i have the exact same issue, could you please give us the link where Apple recognised this as an official bug, please :'( – Red Mak Jan 13 '16 at 10:30
  • @RedMak could you fix it? – Utku Dalmaz Jul 07 '16 at 12:20
  • For Plist/NsDictionary: NSMutableDictionary *data = [NSMutableDictionary dictionaryWithObject:NSFileProtectionNone forKey:NSFileProtectionKey]; – DaNLtR Jul 26 '17 at 13:01
  • 1
    Ran into what seems to be a similar issue. I'm considering implementing workaround nr 2 but I noticed that the official documentation at https://developer.apple.com/documentation/foundation/nsuserdefaults/1407708-resetstandarduserdefaults claims that the resetStandardUserDefaults doesn't do anything. Is this workaround still working? – Dan Marinescu Mar 20 '18 at 16:36
2

We also experienced this problem when using significant location change, on devices with pass code enabled. The app launches on BG before the user even unlock the pass code, and UserDefaults has nothing.

I think it is better to terminate the app before the synchronize occurs, because the reasons below:

  • UserDefaults' synchronize should not be executed once after UserDefaults cleared by this bug.
  • we can't strictly control the call of synchronize because we use many 3rd party libraries.
  • the app will not do any good if UserDefaults can't be loaded (even before the user passes pass code lock).

So here's our (a bit weird) workaround. The app kills itself immediately when the situation (app state = BG, UserDefaults is cleared, iOS >= 7) detected.

It should not violate UX standard, because terminating app on background will not be even noticed by the user. (And also it occurs before the user even passes the pass code validation)

#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)

+ (void)crashIfUserDefaultsIsInBadState
{
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")
        && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
        if ([[NSUserDefaults standardUserDefaults] objectForKey:@"firstBootDate"]) {
            NSLog(@"------- UserDefaults is healthy now.");
        } else {
            NSLog(@"----< WARNING >--- this app will terminate itself now, because UserDefaults is in bad state and not recoverable.");
            exit(0);
        }
    }
    [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:@"firstBootDate"];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [self.class crashIfUserDefaultsIsInBadState]; // need to put this on the FIRST LINE of didFinishLaunchingWithOptions

    ....
}
makoto
  • 31
  • 2
  • Hi @makoto. The problem is about the exact situation you are describing. In your case, if you can't control the synchronise calls it might be OK to kill the app. But our app heavy relies on SLC, no matter if phone has been unlocked or not, so we can't use your solution. Thanks anyways for the help – mp3821 Aug 13 '14 at 22:21
  • @mp3821 Thank you for the comment. We also rely heavily on significant location change on the background, and this workaround seems to be working. To be precise, this workaround is not applicable if your app really need SLC to work before the first pass code unlock after device reboot. It is the time where the user does nothing on the phone (other than seeing the lock screen) after reboot. But if the device locked next time and SLC occurs, the app should work like before with this workaround. – makoto Aug 14 '14 at 04:38
  • Thanks for your solution. We see no other way as we need to load the data from the start. If the data is not available, it there is no reason to start the app in the background. – Mr. Morris Oct 27 '15 at 10:00
  • 1
    Rather than checking if the application is in the background, it's probably better to just check if protected data is available yet: `UIApplication.shared.isProtectedDataAvailable` – Ryan Pendleton Feb 25 '18 at 10:13
  • @RyanPendleton `isProtectedDataAvailable` doesn't help here. It will return `NO` if the device was unlocked and then locked again because it tells you if files in `NSFileProtectionCompleteUnlessOpen` or `NSFileProtectionComplete` states are available, while `standardUserDefaults` is under `NSFileProtectionCompleteUntilFirstUserAuthentication` state and when the device is locked again it is still available. – Shebuka Feb 04 '20 at 10:48
1

This is still the behavior on IOS 9.0 and has been since IOS 7.0.

I suspect that Apple will not change this, since it's a consequence of the fact that the .plist the [NSUserDefaults standardUserDefaults] loads is protected with NSFileProtectionCompleteUntilFirstUserAuthentication.

See also Why are NSUserDefaults not read after flat battery IOS7

Community
  • 1
  • 1