4

I know that UserDefaults are meant simply to save preferences, but these preferences are persistent - the values saved in UserDefaults are maintained over an unlimited number of app launches and are always available to be read as long as the app remains installed... right?

Is it possible that these values will be cleared or will not be correctly accessed at any point? After years of using UserDefaults and depending on the consistency of the values they hold, I have now seen twice in one day of work that when my app launched and checked a simple boolean value, the value was not correct.

if defaults.bool(forKey: "beenLaunched") {

This code runs each time the app launches. If the value is true, I do nothing more, but if it is false, I set a few values as this is the user's very first launch of the app and then I call defaults.set(true, forKey: "beenLaunched") and defaults.set(0, forKey: "eventsCompleted") and a few other values.

I found this thread on the Apple forums in which Eskimo said "For the central NSUserDefaults method, -objectForKey:, a result of nil means that the value is unavailable, but there’s no way to distinguish between this key is not present and this value can’t be fetched because the user defaults are offline." (This appears to be in reference to a specific case of background launching while a device is locked)

I can look into a more secure way of saving simple data such as a Bool value, an Int, or a String, but using UserDefaults for these types of values has always been simple, straightforward, and reliable. Can anybody chime in on the matter and if I was wrong to believe in UserDefaults' persistence?

Thank you!

Community
  • 1
  • 1
RanLearns
  • 4,086
  • 5
  • 44
  • 81
  • 1
    Did you call `NSUserDefaults::synchronize()` after setting the values? – Brandon May 21 '17 at 01:38
  • 2
    Been using the Cocoa framework for 17 years and that's *not* how it works. :-) Didn't bother reading that thread but no, it's never going to be "offline" (not even sure what that is supposed to *mean*). – Joshua Nozzi May 21 '17 at 02:39
  • 2
    @Brandon no, I typically don't ever call synchronize, but have never had a problem – RanLearns May 21 '17 at 02:41
  • @RanLearns; So you've been using `NSUserDefaults` for 7 years and never once called it? God help you now because that is exactly why this issue is happening.. Synchronize is supposed to immediately cause your changes to be written to the disk. However, if you don't call it, it may or may not be written to the disk by the time you re-launch. This happens especially if `NSUserDefaults` is using a backing store (buffered IO) or delayed flushing/writing. – Brandon May 21 '17 at 02:43
  • 2
    @Brandon I'll synchronize, but I don't think this was the issue. I already had the same value in there that had persisted over multiple launches of the app during its development over the past week and then today it launched and failed to get that value. It wasn't a variable that had just recently been written to disk. – RanLearns May 21 '17 at 02:48
  • @JoshuaNozzi right?! Searching "nsuserdefaults accessible" pretty much just brings up that specific Forum thread. I also had a vertical line through my tableView earlier today (it was the right edge of the UITableViewLabel that for some reason became visible) and it only happened at a certain frame size (gave the tableView a different width and it was gone, returned it to the offending width and the vertical line returned) so maybe all the glitches are happening just to me just today. I want to remain confident in UserDefaults, they are great for what I need them: Bools, Ints, Strings, simple. – RanLearns May 21 '17 at 02:54
  • @JoshuaNozzi this doesn't seem like what happened to me, because I'm running the app directly from Xcode, but the second answer here talks about applicationProtectedDataDidBecomeAvailable which supposedly impacts whether UserDefaults are available or not in the odd case of background launching while a device is locked: http://stackoverflow.com/questions/20269116/nsuserdefaults-losing-its-keys-values-when-phone-is-rebooted-but-not-unlocked – RanLearns May 21 '17 at 03:06
  • I have faced similar issue. Most of the time during development I used to press “⌘R” or “⌘.” . So your UserDefaults updates won’t save if you press “⌘R” or “⌘.” just after setting the UserDefaults. You can use `NSUserDefaults::synchronize()` to avoid this. – Bilal May 21 '17 at 03:26
  • 1
    @RanLearns I'm not sure about your layout issue but I've attempted to address your UserDefaults question in my answer. It's **not** `synchronize()`. – Joshua Nozzi May 21 '17 at 03:36
  • 3
    I'm using UserDefaults in many tighten situations and I can assure you that synchronize() is **never needed** and this has been the case for several OS releases now. Using and advertising synchronize nowadays is nothing else than cargo cult programming. Run tests, you will see for yourself. This madness has to stop. – Eric Aya May 21 '17 at 11:17
  • I don't have an answer as to why this is happening, but I can validate that it is indeed a phenomenon I have experienced, and I have outlined that experience in this question that nobody can answer: https://stackoverflow.com/questions/76975347/why-does-this-scenario-break-userdefaults-userdefaults-values-not-available-on I have a suspicion that there may be some kind of thread synchronization issue with userDefaults that was recently introduced. – smakus Aug 29 '23 at 18:59

2 Answers2

4

UserDefaults isn't a "service"; it's never not available to your application. The file it writes to is a PLIST (and therefore all values are stored according to the PLIST standard). For example, all numbers (including booleans) are stored as an NSNumber to the file and can be retrieved either by object(forKey:) or bool(forKey:). If you use the object method and nothing is set for that value you get nil, whose boolean value is false (or 0). Same if you use the boolean method (you get false). This means no matter which way you go, you'll always get false if there's no value or a value of false. Design your logic around that (which you already have - "beenLaunched" will be empty and therefore false if it's never been launched) and you should be fine.

As for the suggestion of synchronize(), ignore it. Unless you're doing something really weird with threads and preference access or you've interrupted the application immediately after setting a value/object for the problem key, it's got nothing to do with this. Per the very first paragraph of the docs, synchronize() is called periodically as needed. In practice, it's called pretty much immediately after a change occurs.

For context, none of my apps have ever called synchronize() and some of them are old enough to drive. Never a single problem. If you don't have a very good justification for calling synchronize() yourself you almost certainly don't need it and attempts to explain why you do need to sprinkle it everywhere are ... often amusing.

In your specific case, the value stuck by after first run multiple times then suddenly didn't once. Is it possible you changed your app's bundle identifier or name? The defaults are stored by identifier+name so a change would effectively "reset" your app's defaults. Have you been running your app in the simulator and did you just reset-content-and-settings in the simulator? On your device and deleted the app before re-running it on-device?

Joshua Nozzi
  • 60,946
  • 14
  • 140
  • 135
  • Thanks @JoshuaNozzi for your explanation and backing up that there isn't a need for manual calls to synchronize() though I'm sure they wouldn't hurt. I did not change the identifier/name, but that's good to point out. I was testing the app in rapid succession on simulators with various device skins as well as on actual devices. I am flustered by not knowing what caused this but I can't replicate the issue (even though it happened twice) and I don't want to doubt what I know has always been a reliable method for saving to a PLIST file. – RanLearns May 21 '17 at 16:40
  • Ever since Swift introduced bool(forKey:), integer(forKey:), and string(forKey:) I have tried to use those specifically as opposed to object(forKey:) when the variable type is clearly known – RanLearns May 21 '17 at 16:41
-1

If you are working in swift then returning nil means objectforkey has not been assigned any value at all . In other case it always returns proper value if you casted saved value properly.

And userdefaults is always available to use, it can never goes offline.

Jitendra Tanwar
  • 249
  • 2
  • 11