iosswiftswiftuiuserdefaults

UserDefaults value changes between app launches


I am using a user default values to observe notification preferences. I print the value on app launch:

func application(_ application: UIApplication, didFinishLaunchingWithOptionslaunchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    print("notifications are initially \(UserDefaults.standard.bool(forKey: UserDefaults.Keys.notificationsEnabled))")
}

And on app termination:

func applicationWillTerminate(_ application: UIApplication) {
    print("notifications are finally \(UserDefaults.standard.bool(forKey: UserDefaults.Keys.notificationsEnabled))")
}

This works fine most of the time, but occasionally the value just resets to what it was initially:

notifications are initially false
**update notifications to true then force quit app**
notifications are finally true
**reopen app**
notifications are initially false

I have spent hours debugging this and cannot figure out why. It works several times in a row correctly and will occasionally revert back to this for some unknown reason. I tried setting the UserDefault values on the main thread as well, but that doesn't seem to change anything. Has anyone seen this bug before? If so, how can I resolve this issue?


Solution

  • As the documentation says:

    The parameters are referred to as defaults because they’re commonly used to determine an app’s default state at startup or the way it acts by default.

    At runtime, you use UserDefaults objects to read the defaults that your app uses from a user’s defaults database. UserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value. When you set a default value, it’s changed synchronously within your process, and asynchronously to persistent storage and other processes.

    Thus, for performance reasons UserDefaults writes to persistent storage asynchronously. A prompt “force-quit” by the user may terminate the app before that is complete.

    Theoretically, one can synchronize. But, as the docs say, it “is unnecessary and shouldn't be used”. But if you’re really concerned about the force-quit edge-case scenario, this might mitigate the problem.

    Alternatively, you may want to simply reconsider whether you should be using “user defaults” at all for this information at all. It is intended for user defaults/preferences. E.g., if a user changes a preference and then promptly force-quits, it is not entirely unreasonable that the change might not apply. Force-quit effectively is the user’s way of saying, “kill this app and everything associated with it.”

    If you have some critical model data, then perhaps reconsider whether “user defaults” is the right place to store it. You could write it to persistent storage yourself. You might also consider wrapping it in a beginBackgroundTask, to ensure the write completes even if it is in progress as the user leaves the app.