iOS: Issues getting beginBackgroundTaskWithName working reliably

We have tried using background tasks for file saving via (UIBackgroundTaskIdentifier) beginBackgroundTaskWithName:(NSString *) taskName expirationHandler:(void (^)(void)) handler; when our app goes into the background and/or is closed by the user. But we cannot make it work the way the documentation tells us it should.

While task creation never reports an issue (in fact it never calls our expiration handler at all) and the returned task id is always valid, when we ask for how much time we have left via backgroundTimeRemaining we always get 6s instead of the specified 30s. We tried to create the task when the app state goes to inactive or when our delegate is called via applicationDidEnterBackground but it makes no difference, besides the fact that the remaining time reported is basically max double, when the app is not in background yet which is by design as far we understand.

But we don't even get the 6s for saving when a user closes the app. Because almost immediately after applicationDidEnterBackground our delegate is called via applicationWillTerminate which will then again almost immediately end in the app receiving a SIGKILL. So we must be doing something wrong. Why would applicationWillTerminate be called at all when we have a valid background task that reports we have 6s left?

We tried blocking the thread in both background and terminate to at least give us the 5s the spec says we have before we get the SIGKILL. That works in general but doesn't feel like the correct approach and we do need more time than the 5s or 6s we get this way.

Are we supposed to add something to our plist in order for these background tasks to work correctly? It is very confusing that there is a second mechanism that's also called background tasks for running apps in the background in general, which is not applicable to us.

Are we supposed to block somewhere when we create the task? Or even spin up an extra thread for the task?

Why is our expirationHandler never called? The spec says that our handler should be called if it was unable to "grant the ask assertion" so it seems like we do not have that problem. But it's also supposed to be called just before we are running out of time but by that time the app is already dead.

This was all tested on iOS 26.3 and it is probably worth mentioning that our app is Qt-based.

Answered by DTS Engineer in 884507022

SO, let me start by sorting out a few specific details:

delegate is called via applicationWillTerminate which will then again almost immediately end in the app receiving a SIGKILL.

applicationWillTerminate is not part of the "normal" delegate lifecycle. It's a relatively late “add-on" that was recycled[1] to give apps SOME opportunity to save work if/when the user for quit them, so it's ONLY called if the app happens to be a wake at the point it's terminated by the user.

Most critically, you cannot do ANY "work" after receiving it. That's because:

  • Your app will call "exit()" immediately after returning from it.

  • If you block “too long", then the system will kill you anyway.

In practice, that means it needs to be handled as a separate edge case as you don't really have a lot of time or flexibility to do significant work. This is not your app being "closed" in the normal sense, it's actually your app being force-quit by the user.

With that out of the way, the important case is actually applicationDidEnterBackground, so let me return to here:

While task creation never reports an issue (in fact, it never calls our expiration handler at all) and the returned task id is always valid, when we ask for how much time we have left via backgroundTimeRemaining we always get 6s instead of the specified 30s.

First off, as background context, I suggest looking over the resources linked to in "Background Tasks Resources", particularly "UIApplication Background Task Notes". Much of what I'm describing is actually in those documents, so what follows is more of an informal summary. So, let me start here:

backgroundTimeRemaining we always get 6s instead of the specified 30s

backgroundTimeRemaining is an API trap. More specifically, what it actually tells you is approximately "what is the remaining time on the expiration of the longest-lived assertion the system is holding against my app". The answer it returns is technically correct, but basically useless.

The problem here is that the system is taking and ending assertions ALL the time, which means that value returned can and WILL fluctuate wildly. Case in point, what's happening here:

besides the fact that the remaining time reported is basically max double

...is that the foreground app can be kept awake indefinitely, which is what the max double value actually means. However, begin a background task in the foreground and check time remaining after you enter the foreground, you'll see the time shift to ~30s. That's because the foreground assertion ended and your background task assertion "took over".

Adding to the fun, the process of taking an assertion is inherently asynchronous, which means your app only has limited visibility into when any change ACTUALLY occurs. That dynamic is what's actually going on here:

While task creation never reports an issue (in fact it never calls our expiration handler at all) and the returned task id is always valid,

beginBackgroundTaskWithName can't "fail" because it doesn't really "do" any actual work. The ID it returns is actually just a bookkeeping entry used to manage a reference count against the underlying assertion it's actually managing. It does this:

The spec says that our handler should be called if it was unable to "grant the ask assertion" so it seems like we do not have that problem.

...because it doesn't "know" whether you'll get additional background time until later, when the assertion request completes. That can also cause this:

when we ask for how much time we have left via backgroundTimeRemaining we always get 6s

...because your app doesn't "have" that extra time immediately after beginBackgroundTaskWithName. That extra time shows up "later", once the assertion is live.

Why is our expirationHandler never called? The spec says that our handler should be called if it was unable to "grant the ask assertion" so it seems like we do not have that problem. But it's also supposed to be called just before we are running out of time but by that time the app is already dead.

My guess is that your testing has been focused entirely on the "force quit" case I talked about first, which bypasses all of the normal expiration logic. Again, that's because the applicationWillTerminate delegate was a "bonus method" intended to provide a small amount of additional context to your app, NOT an opportunity for your app to implement complex save logic.

[1] A Not So Brief History Digression:

applicationWillTerminate is actually one of the oldest delegate methods in iOS, having been part of UIKit's original iOS 2 implementation, 2 years before app suspension and multitasking was introduced in iOS 4. In the original system, apps were quit as soon as they entered the background and applicationWillTerminate was how they were told they had to quit.

That changed when app suspension was introduced in iOS 4. Apps were not quit at all, they were sent to the background (notified through "applicationDidEnterBackground") and then later suspended. Critically, applicationWillTerminate was NEVER called in that new lifecycle. When the user force-quit an app… then the app was immediately terminated without warning or notification. applicationWillTerminate was not formally deprecated at that time because the original lifecycle lingered on for many years (apps could opt out of the new lifecycle), it was just never sent to any "modern" app.

In any case, several years later (~iOS 6? maybe iOS 8?) we decided that there was some value in at least trying to tell apps that happened to be awake that they were being force-quit, so applicationWillTerminate was repurposed for that new role. However, all of that is why the time window is so short and your application has so little flexibility. The delegate was only intended to provide some insight (vs. just terminating the app) into what was going on, not provide a broader "quit" architecture.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you for the post, I see your app is Qt based and I don't have any experience on that technology. I would recommend, you should check with the support resources provided by the 3rd party to get assistance with their software.

Unless another developer in the forums has experience with the third-party and can provide assistance.

Good luck

Albert
  Worldwide Developer Relations.

SO, let me start by sorting out a few specific details:

delegate is called via applicationWillTerminate which will then again almost immediately end in the app receiving a SIGKILL.

applicationWillTerminate is not part of the "normal" delegate lifecycle. It's a relatively late “add-on" that was recycled[1] to give apps SOME opportunity to save work if/when the user for quit them, so it's ONLY called if the app happens to be a wake at the point it's terminated by the user.

Most critically, you cannot do ANY "work" after receiving it. That's because:

  • Your app will call "exit()" immediately after returning from it.

  • If you block “too long", then the system will kill you anyway.

In practice, that means it needs to be handled as a separate edge case as you don't really have a lot of time or flexibility to do significant work. This is not your app being "closed" in the normal sense, it's actually your app being force-quit by the user.

With that out of the way, the important case is actually applicationDidEnterBackground, so let me return to here:

While task creation never reports an issue (in fact, it never calls our expiration handler at all) and the returned task id is always valid, when we ask for how much time we have left via backgroundTimeRemaining we always get 6s instead of the specified 30s.

First off, as background context, I suggest looking over the resources linked to in "Background Tasks Resources", particularly "UIApplication Background Task Notes". Much of what I'm describing is actually in those documents, so what follows is more of an informal summary. So, let me start here:

backgroundTimeRemaining we always get 6s instead of the specified 30s

backgroundTimeRemaining is an API trap. More specifically, what it actually tells you is approximately "what is the remaining time on the expiration of the longest-lived assertion the system is holding against my app". The answer it returns is technically correct, but basically useless.

The problem here is that the system is taking and ending assertions ALL the time, which means that value returned can and WILL fluctuate wildly. Case in point, what's happening here:

besides the fact that the remaining time reported is basically max double

...is that the foreground app can be kept awake indefinitely, which is what the max double value actually means. However, begin a background task in the foreground and check time remaining after you enter the foreground, you'll see the time shift to ~30s. That's because the foreground assertion ended and your background task assertion "took over".

Adding to the fun, the process of taking an assertion is inherently asynchronous, which means your app only has limited visibility into when any change ACTUALLY occurs. That dynamic is what's actually going on here:

While task creation never reports an issue (in fact it never calls our expiration handler at all) and the returned task id is always valid,

beginBackgroundTaskWithName can't "fail" because it doesn't really "do" any actual work. The ID it returns is actually just a bookkeeping entry used to manage a reference count against the underlying assertion it's actually managing. It does this:

The spec says that our handler should be called if it was unable to "grant the ask assertion" so it seems like we do not have that problem.

...because it doesn't "know" whether you'll get additional background time until later, when the assertion request completes. That can also cause this:

when we ask for how much time we have left via backgroundTimeRemaining we always get 6s

...because your app doesn't "have" that extra time immediately after beginBackgroundTaskWithName. That extra time shows up "later", once the assertion is live.

Why is our expirationHandler never called? The spec says that our handler should be called if it was unable to "grant the ask assertion" so it seems like we do not have that problem. But it's also supposed to be called just before we are running out of time but by that time the app is already dead.

My guess is that your testing has been focused entirely on the "force quit" case I talked about first, which bypasses all of the normal expiration logic. Again, that's because the applicationWillTerminate delegate was a "bonus method" intended to provide a small amount of additional context to your app, NOT an opportunity for your app to implement complex save logic.

[1] A Not So Brief History Digression:

applicationWillTerminate is actually one of the oldest delegate methods in iOS, having been part of UIKit's original iOS 2 implementation, 2 years before app suspension and multitasking was introduced in iOS 4. In the original system, apps were quit as soon as they entered the background and applicationWillTerminate was how they were told they had to quit.

That changed when app suspension was introduced in iOS 4. Apps were not quit at all, they were sent to the background (notified through "applicationDidEnterBackground") and then later suspended. Critically, applicationWillTerminate was NEVER called in that new lifecycle. When the user force-quit an app… then the app was immediately terminated without warning or notification. applicationWillTerminate was not formally deprecated at that time because the original lifecycle lingered on for many years (apps could opt out of the new lifecycle), it was just never sent to any "modern" app.

In any case, several years later (~iOS 6? maybe iOS 8?) we decided that there was some value in at least trying to tell apps that happened to be awake that they were being force-quit, so applicationWillTerminate was repurposed for that new role. However, all of that is why the time window is so short and your application has so little flexibility. The delegate was only intended to provide some insight (vs. just terminating the app) into what was going on, not provide a broader "quit" architecture.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

iOS: Issues getting beginBackgroundTaskWithName working reliably
 
 
Q