Execute completionHandler in UIBackgroundTask

When my app is about to be terminated, I receive a notification (UIApplicationWillTerminateNotification) so I can fire off a URLSession to have some data sent to a remote server. I'm trying to do this using UIBackgroundTask, which according the Apple documentation gives me 5 seconds to perform a URLSesstionDataTask. The problem, however, is that my code runs completely fine, except for the fact that the completionHandler of my data task doesn't get executed. It has nothing to do with the 5 seconds expiring, because I tried executing some code AFTER the data task and that code runs fine as well. I also used DispatchQueue to run the completionHandler in the main thread and it didn't work either. Does anyone know first of all if this is possible, and if so, how to do it?

Accepted Reply

I implemented the same data task in both

-applicationDidEnterBackground
… so even if the app gets terminated from the background/suspended state, the required data task will already have been performed the moment my app was moved to the background.

Cool. Most folks who ask me about this want to stay logged in while suspended in the background, which makes things much harder.

So the only problem there's left is that the completionHandler of the actual data task doesn't get executed on app termination.

OK. To fix this you have to block the main thread within

-applicationWillTerminate:
method (or, equivalently, in your
UIApplicationWillTerminateNotification
observer) waiting for the network request to complete. The standard approach for this is to use a dispatch semaphore. For example:
let sem = DispatchSemaphore(value: 0)
startSomethingAsync(completionHandler: {
    sem.signal()
})
sem.wait()

IMPORTANT There’s two key things to note here:

  • You have to make sure that the async work doesn’t rely on any work to be done on the main thread (or queue). If it does, you’ll deadlock, because the main thread is blocked on line 5.

  • In real code you should wait with a timeout so that you don’t block for too long if the network is not working. Without the timeout, you could block indefinitely, and you’ll eventually be killed by the watchdog (which will generate a crash log, which you don’t need).

Finally, be aware that this doesn’t guarantee that you’ll successfully log out of your server when quitting. For example:

  • The network could be offline when you terminate (or move to the background)

  • You might crash, at which point none of this code will run

You’ll have to engineer your server to be resilient in the face of such failures.

Share and Enjoy

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

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

Replies

Last I checked

UIApplicationWillTerminateNotification
only kicks in if the user terminates your app from the multitasking API while your app is running. In the more common case, where the user terminates your app while the app is suspended in the background, you never receive
UIApplicationWillTerminateNotification
. Given that, I’m not sure whether it’s worthwhile you handling this case at all.

Regardless, I think the problem you’re having is that, after you return from your

UIApplicationWillTerminateNotification
observer, the process terminates, and takes your networking request with it. To avoid this, you’ll have to block inside your observer. This is tricky for two reasons:
  • If the app is in the foreground, you’ll lock up its UI.

  • If you block too long, the watchdog will summarily kill your process.

Share and Enjoy

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

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

So then what would you suggest is the 'safest' way to catch the app terminating event? It's very important the data task is performed both when my app is terminated from the foreground AND from the background/suspended state. And also, how do I "block inside my observer"? Right now I'm just having my observer perform a selector method as soon as the notification is received. In my didFinishLaunchingWithOptions delegate method of the AppDelegate I have the notification set up like this:

NotificationCenter.default.addObserver(self, selector:#selector(AppDelegate.onAppWillTerminate(notification:)), name:NSNotification.Name.UIApplicationWillTerminate, object:nil)

And as soon as the notification is received, onAppWillTerminate(notification:) gets called:

func onAppWillTerminate(notification:Notification) {
        // Data task is executed right here
    }

I've tested running big chunks of code in this method and it never got interrupted, so I assume I never ran out of time. It seems very unlikely I would run out of time, since the data task takes very little time to be executed. Basically, what I'm saying is I don't think the process gets terminated before it's supposed to: it finishes the entire process all the way down to the last line of code. Most likely, the REAL problem is that it 'conciously' skips completion handlers and other processes that are normally supposed to be executed in a background thread. This is an issue I ran into before when I was trying to perform a data task in my AppDelegate's applicationDidEnterBackground method, but that issue got solved using the UIBackgroundTaskIdentifier class, which doesn't seem to be doing anything in the method posted above.


Anyway, I'd very much appreciate it if you could first of all tell me if there's a safer alternative to using this UIApplicationWillTerminateNotification in order to ALWAYS catch the app terminating event. I know there's tons of iOS apps out there that require some sort of server connection when the app gets terminated (for example, WhatsApp updates your "last seen" status whenever you either enter the background state or terminate your app altogether) so it seems very unlikely to me it's IMPOSSIBLE to do this. I hope you can point me in the right direction on this. Thanks in advance!

Note: I moved this thread over to App Frameworks > iOS Multitasking because there’s nothing Swift specific here.

Also, I’m happy to go into the details of how to wait in the

UIApplicationWillTerminateNotification
but first let’s deal with the core problem, namely:

It's very important the data task is performed both when my app is terminated from the foreground AND from the background/suspended state.

The latter is simply not possible. iOS will never resume a suspended app solely to inform it that it’s about to be terminated. Doing that would run counter to the system’s overall memory management design. Remember that iOS tries to keep an app suspended and in memory as long as possible. It only dumps the out of memory when the system is short of memory. Resuming the app to deliver the

UIApplicationWillTerminateNotification
event would increase memory usage at a time when the system is already short of memory.

As a rule, the time to do this sort of clean up is when your app becomes eligible for suspension, which typically means on the

UIApplicationDidEnterBackgroundNotification
event.

Share and Enjoy

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

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

That's okay. I implemented the same data task in both applicationDidEnterBackground() and the UIApplicationWillTerminateNotification method, so even if the app gets terminated from the background/suspended state, the required data task will already have been performed the moment my app was moved to the background. I'm pretty sure I have a catch set up for all scenario's right now, so the only problem there's left is that the completionHandler of the actual data task doesn't get executed on app termination. I've searched everywhere and I even found a topic on an external forum from someone who was facing the exact same issue as I am right now, but no one was able to answer his question, so I'm starting to get the suspicion that this is simply not possible, which leaves me wondering how the earlier mentioned apps handle their data tasks on app termination. There HAS to be one way or another to do this, right?

I implemented the same data task in both

-applicationDidEnterBackground
… so even if the app gets terminated from the background/suspended state, the required data task will already have been performed the moment my app was moved to the background.

Cool. Most folks who ask me about this want to stay logged in while suspended in the background, which makes things much harder.

So the only problem there's left is that the completionHandler of the actual data task doesn't get executed on app termination.

OK. To fix this you have to block the main thread within

-applicationWillTerminate:
method (or, equivalently, in your
UIApplicationWillTerminateNotification
observer) waiting for the network request to complete. The standard approach for this is to use a dispatch semaphore. For example:
let sem = DispatchSemaphore(value: 0)
startSomethingAsync(completionHandler: {
    sem.signal()
})
sem.wait()

IMPORTANT There’s two key things to note here:

  • You have to make sure that the async work doesn’t rely on any work to be done on the main thread (or queue). If it does, you’ll deadlock, because the main thread is blocked on line 5.

  • In real code you should wait with a timeout so that you don’t block for too long if the network is not working. Without the timeout, you could block indefinitely, and you’ll eventually be killed by the watchdog (which will generate a crash log, which you don’t need).

Finally, be aware that this doesn’t guarantee that you’ll successfully log out of your server when quitting. For example:

  • The network could be offline when you terminate (or move to the background)

  • You might crash, at which point none of this code will run

You’ll have to engineer your server to be resilient in the face of such failures.

Share and Enjoy

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

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

Yes! It works! Thank you so much! I honestly did not expect the solution to be something as simple as this. I've been digging for the answer for a really long time and since no one was able to help me out, I'd lost all hope that this was even possible to do. Thank you for the clear explanation, and as for the heads-ups you gave me, I've already taken care of most scenario's of app termination and suspension, including scenario's in which the device is not connected to the internet when moving to the background. The only problem I was left dealing with was the problem described above, but thanks to your help I got that covered too, so now I'm all done!

hello, i have a question about this.

i use alamofire to download a file, and i cancel the task when the app terminate like this:


public func applicationWillTerminate(_ application: UIApplication) {

let sem = DispatchSemaphore(value: 0)

Account.current?.downloadManager?.clean(completionHandler: {

sem.signal()

})

sem.wait()

}


i want to get the resumeData and save it to local when the completionHandler of the request called.

below is the 'clean' method and the completionHandler:


public func clean(completionHandler: (() -> Void)? = nil) {

self.completionHandler = completionHandler

request?.cancel()

}


let queue = DispatchQueue.init(label: "test")

request?.response(queue: queue, completionHandler: { (resp) in

if self.state != .initial { // all tasks had been cleared

queue.async {

Account.current?.userDefaults.set(resp.resumeData, forKey: self.resumeDataKey)

Account.current?.userDefaults.synchronize()

self.completionHandler?()

}

}

}

this method works fine if user kill the app and relaunch it. but when the app enter background and be killed by the system a few minutes later, it doesn't work.


thank you for your time, i would be very appreciate if you can help me!

but when the app enter background and be killed by the system a few minutes later, it doesn't work.

Right. On modern systems (iOS 4 and later) the

applicationWillTerminate(_:)
method is only called under very specific circumstances. In most cases the sequence is as follows:
  1. The user presse the Home button.

  2. Your app moves to the background.

  3. Shortly thereafter the system suspends your app.

  4. At some point in the future, the gets short on memory and recovers some by removing your app from memory.

The critical thing here is that, in step 4, the system doesn’t resume your app before removing it from memory, because doing so would be counter productive (it would increase memory pressure). Thus, the

applicationWillTerminate(_:)
isn’t called.

How you handle this depends on the nature of your network request:

  • For interactive requests, you can use a

    UIApplication
    background task to prevent your app from being suspended in the background. Your network request will then take one of the following paths:
    • If the request completes promptly, you end the

      UIApplication
      background task and your done.
    • If you run out of background task time, you can cancel the network request in your expiry handler.

    See my UIApplication Background Task Notes post for more on this tech.

  • For long-running requests, like a large download, you should use an

    NSURLSession
    background session. This will allow your request to make progress when your app is suspended (and even when it’s removed from memory). When the request completes the system will either resume (or relaunch) your app in the background to process the results.

Share and Enjoy

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

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