3

I have been developing an app that stores user data (username, etc.) between launches of the app; I stored this data in UserDefaults. However, I have recently noticed a problem: Sometimes, when I run the app, I will get some value back from UserDefaults.standard.object(forKey:), but other times I will get nil when I know for a fact that there is something there.

This makes no sense to me.

I have searched for the answer to this question on SO but only found this, which did not help.

In order to test this, I made an empty app and put the following in viewDidAppear, and then I ran the app once:

UserDefaults.standard.setValue("aValue", forKey: "aKey")

The above line is just to ensure that there is in fact a value stored. I then deleted the above line from viewDidAppear, and then I put this in didFinishLaunchingWithOptions:

print(UserDefaults.standard.object(forKey: "aKey") as? String)

I then ran the app 20 times. Here is my data for if the print(...) printed nil or "aValue":

✓    ✓  ✓ ✓ ✓ ✓ ✓ ✓  ✓ ✓ ✓  ✓ ✓ 

It printed nil 35% of the time and it seems to me fairly random too.

I have two questions:

Why would this happen, and how can I fix/prevent it?

Community
  • 1
  • 1
IHaveAQuestion
  • 789
  • 8
  • 26
  • Can you try adding `UserDefaults.standard.synchronize()` after the part that created the key. Or maybe try to print it in in viewDidLoad, it might not have been loaded before. – Eric Nov 26 '16 at 21:02
  • Are you deleting the app after running it? or you're just stopping it ? – J. Koush Nov 26 '16 at 21:04
  • @Eric, the `.setValue...` code was there only in the first run just so that there would be a value there. All subsequent runs only had the `.object...` code – IHaveAQuestion Nov 26 '16 at 21:14
  • @J.Koush, I am just stopping it, not deleting. – IHaveAQuestion Nov 26 '16 at 21:17
  • Could it be this http://stackoverflow.com/a/2622940/1187415 ? – Martin R Nov 26 '16 at 21:25
  • @MartinR, no, that didn't help. The problem is that sometimes the defaults are read correctly and sometimes not – IHaveAQuestion Nov 26 '16 at 21:36
  • @IHaveAQuestion, please clarify whether you are explicitly calling `synchronize()` on `UserDefaults`, as Eric pointed out. If you are not explicitly calling it, the system or other code may call it, but you have no guarantees your data will be persisted without calling `synchronize()`. – Kekoa Nov 28 '16 at 05:47
  • Possible duplicate of [Why is NSUserDefaults not saving my values?](http://stackoverflow.com/questions/2622754/why-is-nsuserdefaults-not-saving-my-values) – Kekoa Nov 28 '16 at 05:49
  • @Kekoa, no because that did not help and a different solution solved the different problem. – IHaveAQuestion Nov 28 '16 at 23:09

2 Answers2

1

It could happen if didFinishLaunchingWithOptions in AppDelegate is called after viewDidAppear in the view controller.

The recommended way (by Apple) is to register all key / value pairs as soon as possible (awakeFromNib or applicationWillFinishLaunching)

let defaultValues = ["aKey" : "aValue"]
UserDefaults.standard.register(defaults: defaultValues)

The default value "aValue" is considered until it is overwritten the first time. If you reset the data of the app, the default value is read again.

This is the most reliable way.

PS: Never use valueForKey: and setValue:forKey: with UserDefaults

vadian
  • 274,689
  • 30
  • 353
  • 361
  • Sorry I think it may have been unclear how I described my situation: the `.setValue...` code was there only in the first run just so that there would be a value there. All subsequent runs only had the `.object...` code. The trials I have in my question are for those subsequent runs – IHaveAQuestion Nov 26 '16 at 21:15
  • I have edited my question because the way I wrote it before was misleading. I'm very sorry. It should be very clear now – IHaveAQuestion Nov 26 '16 at 21:18
  • Anyway, you could prevent the unexpected behavior registering the key / value pair. – vadian Nov 26 '16 at 21:21
  • So the correct way to set user defaults is to say `.register(defaults:)`, and the correct way to get user defaults is to say `.object(forKey:)`? – IHaveAQuestion Nov 26 '16 at 21:35
  • No, registering is the task to set **default** values (as the name of the class implies). There are `set(:forKey` to write and `object/string/integer/bool/etc(forKey:` to read. Please read the documentation of the class. – vadian Nov 26 '16 at 21:38
1

It is crazy to put the set in viewDidAppear but the get in didFinishLaunching, because they are unrelated and you don't know anything about the order in which they will happen or even whether viewDidAppear will ever happen (not every view controller ever appears, after all).

Put them in the same place to run your test. For example, let's put them both in didFinishLaunching:

let val = UserDefaults.standard.object(forKey: "aKey") as? String
if val != nil {
    print("got it")
} else {
    print("didn't get it, setting it")
    UserDefaults.standard.set("aValue", forKey: "aKey")
}

You'll find that this works just as you would expect.

EDIT Okay, thanks to the movie you posted, we see that there's a complication: you're testing this on the device. You are testing by repeatedly hitting the Run button in Xcode without stopping the app in a coherent way, and this possibly confuses Xcode so that it does a complete replacement, or so that it never gets a chance to write the defaults to disk in the first place. Instead, Run, switch to the device and hit the Home button, now switch back to Xcode and Stop. Now Run again. I think you'll find that you'll get a more consistent result.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I edited my question. The way I wrote it before was misleading. Sorry. – IHaveAQuestion Nov 26 '16 at 21:18
  • Well, I'm not going to edit my answer. If you do what I said, all will be well. Try it. Just put this code and _don't_ change it. Just run the app over and over and watch what happens. – matt Nov 26 '16 at 21:18
  • I pasted your code in `didFinishLaunching`, ran it twice, and both times it printed `didn't get it, setting it` – IHaveAQuestion Nov 26 '16 at 21:22
  • Either you're mistaken or you're doing something else (there was other code or something). I created the app, pasted in that code, ran it, saw "didn't get it, setting it" as expected. I then immediately ran again and again and again and saw "got it" every time. – matt Nov 26 '16 at 21:28
  • I just ran it 4 times and only on the forth did it say "got it". Your code is the only thing I added to the project. – IHaveAQuestion Nov 26 '16 at 21:33
  • Wait, I'll make you a movie of me doing it. – matt Nov 26 '16 at 21:36
  • [I don't know if it's something wrong with my device or...](https://www.dropbox.com/s/8m1nf97m2byd9ae/UDTest.mov?dl=0) – IHaveAQuestion Nov 26 '16 at 22:44
  • Okay but without changing anything else, switch the destination to the simulator and try it. – matt Nov 26 '16 at 22:55
  • That's odd. It seems to work perfectly on the simulator but not correctly on the actual device – IHaveAQuestion Nov 26 '16 at 23:00
  • What is the different between `set(_:forKey:)` and `setValue(_:forKey:)` when using UserDefaults? – Zhou Haibo Nov 02 '20 at 10:26