32

I have an app that uses [NSUserDefaults standardUserDefaults] to store session information. Generally, this information is checked on app launch, and updated on app exit. I have found that it seems to be working unreliably in iOS 8.

I am currently testing on an iPad 2, although I can test on other devices if need be.

Some of the time, data written before exit will not persist on app launch. Equally, keys removed before exit sometimes appear to exist after launch.

I've written the following example, to try and illustrate the issue:

- (void)viewDidLoad 
{
    [super viewDidLoad];

    NSData *_dataArchive = [[NSUserDefaults standardUserDefaults] 
                                            objectForKey:@"Session"];

    NSLog(@"Value at launch - %@", _dataArchive);

    NSString *testString = @"TESTSTRING";
    [[NSUserDefaults standardUserDefaults] setObject:testString 
                                           forKey:@"Session"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    _dataArchive = [[NSUserDefaults standardUserDefaults] 
                     objectForKey:@"Session"];

    NSLog(@"Value after adding data - %@", _dataArchive);

    [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"Session"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    _dataArchive = [[NSUserDefaults standardUserDefaults] 
                     objectForKey:@"Session"];

    NSLog(@"Value before exit - %@", _dataArchive);

    exit(0);
}

Running the above code, I (usually) get the output below (which is what I would expect):

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - (null)

If I then comment out the lines that remove the key:

//[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"Session"];
//[[NSUserDefaults standardUserDefaults] synchronize];

And run the app three times, I would expect to see:

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - TESTSTRING
Value after adding data - TESTSTRING
Value before exit - TESTSTRING

Value at launch - TESTSTRING
Value after adding data - TESTSTRING
Value before exit - TESTSTRING

But the output I actually see is:

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

e.g. It seems to not be updating the value on exiting the app.

EDIT: I have tested the same code on an iPad 2 running iOS 7.1.2; and it appears to work correctly every time.

TLDR - In iOS 8 does [NSUserDefaults standardUserDefaults] work unreliably? And if so is there a workaround/solution?

HaemEternal
  • 2,229
  • 6
  • 31
  • 50
  • 3
    Please state what device you are running on. If it's the simulator - while it *should* work, I'm not particularly surprised that it doesn't. It has plenty of bugs! – Airsource Ltd Sep 18 '14 at 16:12
  • 1
    What if you `synchronize` before reading the data in initially? Not sure if that should be necessary, but the Apple docs about `NSUserDefaults` do say that you can synchronize "if you want to update the user defaults to what is on disk" [NSUserDefaults](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/Reference/Reference.html#//apple_ref/doc/uid/20000318-CIHDDEGI) – Stonz2 Sep 18 '14 at 16:12
  • @AirsourceLtd I've updated above. This was tested on an iPad 2. I have other devices available, so I will try them today. – HaemEternal Sep 19 '14 at 07:33
  • @Stonz2 Thanks for the suggestion. Tried that just now, same outcome unfortunately. – HaemEternal Sep 19 '14 at 07:40
  • It looks like there are two places in your code that modify the `Session` value. At least the log output differs. – Nikolai Ruhe Sep 19 '14 at 08:40
  • @NikolaiRuhe Yes, that is correct. When I check the output while the app is running, everything seems OK. It's only once I exit and re-enter the app, that the value appears to have not been stored correctly. Perhaps there is some caching going on behind the scenes in iOS 8? – HaemEternal Sep 19 '14 at 08:41
  • @HaemEternal I guess I have a similar problem. I set an `NSUserDefaults` key for first launch and deepening on that I do different things when my app starts as follows `// Define FirstLaunch in NSUserDefault [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],@"FirstLaunch", nil]];` But when I check for the value of that first launch key it's 0 or NO which means it is not set or registered properly in the code above. I hope we can find help soon. – Ali Sep 22 '14 at 15:33
  • I am having the same issue, if I use the iPhone 6 simulator with iOS 8 the settings are not being saved at all. But if I use iPhone 5s with iOS < 8 the settings are being saved ok. – iVela Sep 26 '14 at 18:23
  • Have seen the same behaviour in my apps on IPAD Air IOS 8.0.2, although it seems it's just a matter of seconds to wait for the 'synchronize' to happen – Macistador Oct 09 '14 at 15:11
  • @Macistador I tried adding a 5 second sleep after the synchronize to try and rule that kind of thing out; but it still didn't work reliably. – HaemEternal Oct 15 '14 at 07:50
  • I'm experiencing the same thing. On start up one specific key keeps showing null, but during the program and before exit the nsuserdefaults stores that key. When i restart the program, that same key value is null again whereas all my other keyvalues are not null. Its weird. Have you solved your problem? – Pavan Oct 30 '14 at 04:59
  • 1
    @Pavan Until I come up with a better solution, I have written a simple class to save/load data to/from a plist. – HaemEternal Oct 30 '14 at 09:10

12 Answers12

14

iOS 8 introduced a number of behavior changes to NSUserDefaults. While the NSUserDefaults API has changed little, the behavior has changed in ways that may be relevant to your application. For example, using -synchronize is discouraged (and always has been). Addition changes to other parts of Foundation and CoreFoundation such as File Coordination and changes related to shared containers may affect your application and your use of NSUserDefaults.

Writing to NSUserDefaults in particular has changed because of this. Writing takes longer, and there may be other processes competing for access to the application's user defaults storage. If you are attempting to write to NSUserDefaults as your application is exiting, your application may be terminated before the write is committed under some scenarios. Forcefully terminating using exit(0) in your example is very likely to stimulate this behavior. Normally when an application is exited the system can perform cleanup and wait for outstanding file operations to complete - when you are terminating the application using exit() or the debugger this may not happen.

In general NSUserDefaults is reliable when used correctly on iOS 8.

These changes are described in the Foundation release notes for OS X 10.10 (currently there is not a separate Foundation release note for iOS 8).

quellish
  • 21,123
  • 4
  • 76
  • 83
  • Calling -synchronize is often required, such as when sharing preferences between an iOS 8 application and extension. – EricS Nov 13 '14 at 21:47
  • Please review the release notes I linked to. The scenario you describe is one that is specifically mentioned as one where `-synchronize` should *not* be used. – quellish Nov 13 '14 at 21:49
  • Re-read them. It says "If you synchronized because you immediately send out a notification to re-read preferences in another program after setting, and you wanted to be sure it will pick it up, then it may still be worth calling synchronize." In iOS, extensions run as separate processes. – EricS Nov 13 '14 at 22:40
  • 1
    On iOS and MacOS, preferences are not modified by the application. They are *always* accessed by a separate process. Read the release notes again: "If you: Synchronized to read changed values from outside your process. Then please don’t do so now". Additionally, when using shared containers with extensions a suite must be used. Using `+standardUserDefaults` to share information between an app and an extension will not behave correctly. This is covered in the extension programming guide, as well as at least one technical Q&A. – quellish Nov 13 '14 at 22:48
9

It looks like iOS 8 does not like setting strings in NSUserDefaults. Try encoding the string into NSData before saving.

When saving:

[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:testString] forKey:@"Session"];

When reading:

NSData *_data = [[NSUserDefaults standardUserDefaults] objectForKey:@"Session"];
NSString *_dataArchive = [NSKeyedUnarchiver unarchiveObjectWithData:_data];

Hope this helps.

Igor
  • 99
  • 3
  • Any evidence for this? – gnasher729 Sep 23 '14 at 13:05
  • 3
    My 8 apps which are heavily using NSUserDefaults have stopped saving data for quite a lot of users. All that because one of the variables saved was a string. After replacing with the above method saves are working back fine. – Igor Sep 23 '14 at 16:57
  • Thanks for the suggestion @Igor, but it doesn't seem to make a difference for me. – HaemEternal Oct 14 '14 at 13:42
2

As gnasher729 said, don’t call exit(). There may be an issue with NSUserDefaults in iOS8, but calling exit() simply won’t work.

You should see David Smith’s comments on NSUserDefaults (https://gist.github.com/anonymous/8950927):

Terminating an app abnormally (memory pressure kill, crash, stop in Xcode) is like git reset --hard HEAD, and leaving

n-b
  • 929
  • 4
  • 6
  • So what should I do instead in your opinion? This is not an app store app. It is a custom app, and it has a requirement to close the application, and exit to the homescreen, without using the home button. My original question is "Why is this different in iOS 8" and "how can I achieve this". I don't think "don't call exit" helps me get to a solution – HaemEternal Nov 13 '14 at 16:24
  • 1
    Not using `exit()` in an iOS app isn't simply a matter of Apple policy, it's a matter of making your app work correctly. The fact that this isn't going to the app store is not relevant. – Tom Harrington Nov 13 '14 at 21:09
  • @HaemEternal AFAIK, there’s no correct documented way to exit an iOS app. You could try a private API, as this is a in-house app, but I encourage you to discuss the “requirement to close the application”. As you can see, this in unsupported, but this is also “not the iOS way”. Everyone will be happier if you can get your client to reconsider this requirement. – n-b Nov 15 '14 at 21:23
2

I found NSUserDefaults to behave nicely on iOS 8.4 when using a suite name to create an instance instead of relying on standardUserDefaults.

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"MySuiteName"];

Thomas Verbeek
  • 2,361
  • 28
  • 30
1

I have the same problem with iOS 8 and the only solution worked for me is to delay perform of exit() function some duration (Ex.: 0.1 seconds) using:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 10), dispatch_get_main_queue(), ^{ exit(0); });

or create a method and then call it using performSelector:withObject:afterDelay:

- (void)exitApp {
   exit(0);
}

[self performSelector:@selector(exitApp) withObject:nil afterDelay:0.1];
Basem Saadawy
  • 1,808
  • 2
  • 20
  • 30
1

I faced the same issue. I solved it by calling

[[NSUserDefaults standardUserDefaults] synchronize];

before calling

[[NSUserDefaults standardUserDefaults] stringForKey:@"my_key"].

It turns out one has to call synchronize not only after setting but before getting too.

fnc12
  • 2,241
  • 1
  • 21
  • 27
0

Since this is an Enterprise App and not an App Store app, you can try:

@interface UIApplication()
-(void) _terminateWithStatus:(int)status;
@end

and then call:

[UIApplication.sharedApplication _terminateWithStatus:0];

It's using an undocumented API so may not work in previous or future versions of iOS.

EricS
  • 9,650
  • 2
  • 38
  • 34
0

It is bug in simulators.This bug also exist in prior to iOS8 beta4 on devices.But on devices this bug is resolved but it currently exist on simulators.They have also changed the simulator directories structure.If you reset your simulator it will work fine.On iOS8 devices it will also work fine.

Waseem Shah
  • 2,219
  • 23
  • 23
0

I found it in Foundation Framework Reference, think it will be useful:

The NSUserDefaults class provides convenience methods for accessing common types such as floats, doubles, integers, Booleans, and URLs. A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData. For more details, see Preferences and Settings Programming Guide.

wm.p1us
  • 2,019
  • 2
  • 27
  • 38
0

As others had pointed out using exit() and generaly exiting your app yourself is really bad idea in iOS.

But I probably know what do you have to deal with. We did develop an enterprise application as well and even though we tried to convince the client that in iOS it is against all the rules and best practices, they insisted that we close the app at one point.

Instead of exit() we used this piece of code:

UIApplication *app = [UIApplication sharedApplication];
[app performSelector:@selector(suspend)];

As the name suggests it only suspends the app as if the user pressed home button. Therefore your saving methods might be able to finish correctly.

I haven't tested this solution for your particular case though, and I'm not sure if suspend is enough for you, but for us it worked.

micromanc3r
  • 569
  • 1
  • 8
  • 16
0

I have solved similar issues by making changes to NSUserDefaults only in the main thread.

Radu Simionescu
  • 4,518
  • 1
  • 35
  • 34
-4

Calling exit () in an iOS application is a criminal offence, and as you noticed, it got punished. You never quit an iOS application yourself. Never.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
  • I fully agree. Except... This is an enterprise application that does not exist on the commercial app store. For our use case, it is required that we do it this way. – HaemEternal Sep 23 '14 at 12:27
  • 7
    @gnasher how exactly is this an answer to OP question? This has nothing to do with his problem – Sam B Sep 23 '14 at 12:50
  • 2
    It has everything to do with his problem. exit kills the app right there and then. Any threads that are running are killed as well. Now if you make assumptions like synchronize running immediately when you call it and not performing the synchronization on a background thread, then these assumptions will byte you in the arse. – gnasher729 Sep 23 '14 at 13:04
  • @HaemEternal: The rules that Apple makes are there for a reason. Is your product manager more intelligent and does he know more about user interface design than a few hundred developers working at Apple? I don't think so. – gnasher729 Sep 23 '14 at 13:06
  • @gnasher729 Thanks for the suggestion. As I understand it, the reason to not exit the application in this fashion, is that it appears to the user as a crash. That is not relevant for my use case. I don't think it has anything to do with being more intelligent than a hundred developers. It's about knowing more about the specific task that our application is required to complete. – HaemEternal Sep 23 '14 at 13:11
  • 1
    It's a really annoying API limitation. We wanted this as well and never figured out a good way to do it. You can open another app like Safari by opening a URL, but you can't get back to the home screen. Since it's an Enterprise app and not an App Store app, you might be able to find a private method on NSApplication that will work via performSelector. You can list them via http://stackoverflow.com/questions/2899716/iphone-os-get-a-list-of-methods-and-variables-from-anonymous-object – EricS Nov 13 '14 at 20:38