Fate of user defaults during background sessions

My app uses background fetching of web data. Once the data is downloaded during a background session, a processing function is called and, when it's all done, a few key values need to be saved. I'm currently using the UserDefaults to store the updated data. When everything has been processed and the latest data stored in the usual way you use the defaults, I attempt run a synchronize:


//  This is at the end of function that runs after a background web session download
defaults.set(x, forKey: "whatever")  //  There are several of these

if defaults.synchronize() {   // Attempt to sycnh
      self.logSB.error("Defaults failed to synchronize")  //  This often prints, maybe all the time
}


(The "self.logSB.error" is similar to "print" but employs SwiftyBeaver logging, which can log in the background.)


This synchromization attempt may work sometimes but definitely fails frequently in background sessions. I've read that when the app is in deep background, the UserDefaults (UD) system is unavailable. I don't fully understand what that means, and I need a workaround.


1. Setting up my background fetch session requires URLs and other details that I've stored in the UD. There is no problem getting these details out of the UD during a background session. Does this mean the UDs already in memory when the app went into the background are still available to the app in the background session? Does the statement about unavailability of the UDs in background sessions mean that the UDs in memory are still available but become disconnected from the ones stored on disk?

2. If I change UDs in a background session by using the typical defaults.set, I suppose only the UD copies in memory are affected and the ones on disk do not change. So what happens when the app comes out of the background? Is there an automatic synchronization? Which UDs take precedence, memory or disk?


I know you can persist data by writing it to a file in a background session and then reading that file when the app becomes active. I do that already with the initial web download. Once that download completes (URLSessionDownloadTask, didFinishDownloadingTo location: URL), the data in that file is processed and that's where I'm having this problem. Do I need to write to a file or simply eliminate the synchronization step?


UPDATE: I found a related post over at StackOverflow, question 20269116. It's several years old and I'm not sure it's still valid. https://stackoverflow.com/questions/20269116/nsuserdefaults-losing-its-keys-values-when-phone-is-rebooted-but-not-unlocked


I've also read a lot of Quinn's posts on this, such as

https://forums.developer.apple.com/thread/15685


Maybe I'm too dense to get it, but I don't understand how the UD seem somewhat available but not fully.

Accepted Reply

Did your code get copied over accurately to your post, or was there an unintended edit?

The defaults.synchronize() method should return true if it suceeds, but the code above seems to log that as a failure.


Assuming the user defaults still aren't actually updating with the new information you are trying to synchronize, if you aren't already doing it with a dispatched call to the main thread, I would try it that way and see if the info gets synchronized correctly that way.

Replies

Oh nuts, I just realized that my background fetches likely use hard-coded default values when the UserDefaults are unavailable (and come back nil when I try to read them). The hard-coded defaults work fine, so I didn't realize this was happening. That's why my fetches appear to be working fine in the absence of access to UserDefaults. So my problem is bigger now. 😟


UPDATE: No, this does not seem to be the problem. The UserDefaults do appear to be available in the background session. My background functions use the proper values that I expect to see in the UserDefaults and not just my coded defaults (to avoid nil). So now I'm really confused again. Is the only problem here that the .synchronize() function fails in the background?

Did your code get copied over accurately to your post, or was there an unintended edit?

The defaults.synchronize() method should return true if it suceeds, but the code above seems to log that as a failure.


Assuming the user defaults still aren't actually updating with the new information you are trying to synchronize, if you aren't already doing it with a dispatched call to the main thread, I would try it that way and see if the info gets synchronized correctly that way.

I’m going to repeat my advice from the post you mentioned: you should avoid using NSUserDefaults in the background but instead put your preferences in a file whose data protection mode you control. That opts you out of the entire question of how NSUserDefaults handles data protection, which a big complex ball o’ wax.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I'm half way along in doing what you suggest. I've written a routine that writes all the user defaults to a file after deleting any previous copy. Now that I've corrected stupid mistakes (see above) and things are working again, I've paused this approach and haven't written the routine to read them back.


I remain confused on this issue. My background fetches get the appropriate URLs they need by reading them from the UserDefaults. These appear to execute just fine, so I'm not sure there is any problem. My fetches are typically very fast, so the web data gets retrieved and processed during the background session, including saving to the UserDefaults. Again, no problem.


I understand that, if the fetch takes too long, the web data will come back after the app has returned to a suspended state. The data cannot be processed until my app gets more background time, but when that happens, the UserDefaults are once again available. Does this description make sense? Is there a problem with this?

I'm embarassed 😊 to admit that my code was backwards and logged a failure every time it succeeded. Good Grief. I'm glad I found it before your answer but if I hadn't, your answer would have been invaluable.


I discovered a more subtle problem as well. I am doing the synchronize within a dispatch to the main thread as you mentioned. I create a new instance of the UserDefaults within that dispatch. To distinguish it from any other instance in the code, I called it "defaultsD". In one case I had left off the "D" when calling defaultsD.synchronize(). It looks fine when you proofread it and there was no indication of an error, but of course this didn't give reliable results.

I remain confused on this issue.

Right. The problem with user defaults is that there’s a lot going on behind the scenes. For example, user defaults does a lot of in-memory caching, it syncs with iCloud, it can be shared between processes, and so on. That’s all good stuff, but it makes it hard to guarantee that it will behave as expected in all scenarios, especially those that involve background execution.

Consider a scenario like this:

  1. Your app’s container is marked as

    NSFileProtectionComplete
  2. The user defaults backing files inherit their protection from that

  3. Your app is run by the user

  4. It reads some user defaults, causing it to cache the values in memory

  5. Your app is suspended

  6. Your app is resumed while the device is locked

  7. Your app reads your settings; this works because your settings got cached at step 4

Now consider what happens with this sequence:

  1. Repeat steps 1 though 5 above

  2. Your app is terminated

  3. Your app is relaunched in the background while the device is locked

  4. Your app reads your settings

In this case the read will fail because the cache is lost when the app is terminated.

IMPORTANT I should stress that this probably isn’t how things work in real life, at least on modern systems; this is a thought experiment designed to show why using user defaults in the background is so tricky.

This is why I recommend that you opt out of user defaults when dealing with critical settings that must be accessed from the background. Storing those critical settings in your own file is super easy (just read and write the file using the NSDictionary APIs) and lets you guarantee that the settings are gated via the appropriate data protection mode.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

"Your app reads your settings; this works because your settings got cached at step 4"


That's a critical piece . Here's my flow:

  1. The UserDefaults get read when the app opens.
  2. UserDefaults.synchronize() is called in applicationWillResignActive in the AppDelegate. (always works)
  3. After time, the app has been suspended
  4. A quick background fetch occurs. I can't swear the next steps always gets completed during the fetch period. At home on wifi I think it does but perhaps not with a slow cell connection.
  5. Processing the new web results requires values from the UserDefaults
  6. UserDefaults are updated with new values
  7. A UserDefaults.synchronize() is attempted and apparently always succeeds
  8. A completion notification triggers an alert to the user

This appears to work fine. Is there any reason to doubt this flow? My concern is that the cached UserDefaults are truly maintaining synch with the ones on disk.

"Your app is relaunched in the background while the device is locked."

I was about to write that this cannot happen, but I've recently added a geofence function to my app and it can in fact launch while locked after being dismissed by the user. So, I'll have to keep plugging away at this to deal with the launching-while-locked scenario.

Thankfully, I'm seeing good results virtually all the time with my current setup when the app remains launched at all times. Background fetches run during the night and send (silent) alerts to the user with accurate information.