I'm trying to understand how the API works to perform a function that can continue running if the user closes the app. For a very simple example, consider a function that increments a number on screen every second, counting from 1 to 100, reaching completion at 100. The user can stay in the app for 100s watching it work to completion, or the user can close the app say after 2s and do other things while watching it work to completion in the Live Activity.
To do this when the user taps a Start Counting button, you'd
1 Call BGTaskScheduler.shared.register(forTaskWithIdentifier:using:launchHandler:)
.
Question 1: Do I understand correctly, all of the logic to perform this counting operation would exist entirely in the launchHandler
block (noting you could call another function you define passing it the task
to be able to update its progress)? I am confused because the documentation states "The system runs the block of code for the launch handler when it launches the app in the background." but the app is already open in the foreground. This made me think this block is not going to be invoked until the user closes the app to inform you it's okay to continue processing in the background, but how would you know where to pick up. I want to confirm my thinking was wrong, that all the logic should be in this block from start to completion of the operation, and it's fine even if the app stays in the foreground the whole time.
2 Then you'd create a BGContinuedProcessingTaskRequest
and set request.strategy = .fail
for this example because you need it to start immediately per the user's explicit tap on the Start Counting button.
3 Call BGTaskScheduler.shared.submit(request)
.
Question 2: If the submit
function throws an error, should you handle it by just performing the counting operation logic (call your function without passing a task)? I understand this can happen if for some reason the system couldn't immediately run it, like if there's already too many pending task requests. Seems you should not show an error message to the user, should still perform the request and just not support background continued processing for it (and perhaps consider showing a light warning "this operation can't be continued in the background so keep the app open"). Or should you still queue it up even though the user wants to start counting now? That leads to my next question
Question 3: In what scenario would you not want the operation to start immediately (the queue behavior which is the default), given the app is already in the foreground and the user requested some operation? I'm struggling to think of an example, like a button titled Compress Photos Whenever You Can, and it may start immediately or maybe it won't? While waiting for the launchHandler
to be invoked, should the UI just show 0% progress or "Pending" until the system can get to this task in the queue? Struggling to understand the use cases here, why make the user wait to start processing when they might not even intend to close the app during the operation?
Thanks for any insights! As an aside, a sample project with a couple use cases would have been incredibly helpful to understand how the API is expected to be used.
I've answered your specific questions below, but I also talked about these issues on this thread, which you might find useful.
Question 1: Do I understand correctly, all of the logic to perform this counting operation would exist entirely in the launchHandler block...
No, that's not required and, in many cases, it's very likely that very little or even "none" of the work actually happened inside the "task" itself. The pattern used by all of our different task types is that the launchHandler is there to "tell" you the task has started and provide you the BGTask object you use to manage that work. No task type actually requires the work to occur inside the task.
However, what does make BGContinuedProcessingTask different is related to this issue:
Question 2: If the submit function throws an error, should you handle it by just performing the counting operation logic (call your function without passing a task)?
All of the other task types generally "start" work inside the launchHandler because all of those other task types involve performing work in the background, so the launchHandler is telling you "now is the time you should do this background work". That ISN'T true of BGContinuedProcessingTask- your app is already in the foreground and, as we discuss in the documentation and the WWDC presentation, BGContinuedProcessingTask is specifically intended to be used for "critical work" the user has explicitly requested your app "do". Part of that definition clearly implies that deferring/"not doing" the work isn't acceptable, because if that WAS acceptable then you probably shouldn't use BGContinuedProcessingTask!
I think the best way to understand this is to think of this in terms of how an app running on iOS 18 would work and then extend that "flow" to iOS 26. So, an iOS 18 app might have an interaction that worked something like this:
- User presses button to start some "longer running work".
- App starts long running work.
- App enters background.
- App saves state to preserve the work it's done (so it can continue later) and suspends.
In many cases, that same flow on iOS 26 should basically work the same. Basically, something like this:
- User presses button to start some "longer running work".
- App starts long running work.
- App starts BGContinuedProcessingTask
- App enters background.
- BGContinuedProcessingTask expires (or failed to start at all).
- App saves state to preserve the work it's done (so it can continue later) and suspends.
In other words, on iOS 26 what happens is either:
- BGContinuedProcessingTask provides an improved user experience by allowing the work to finish or further progress compared to iOS 18.
OR
- The app behaves exactly as it did on iOS 18.
Question 3: In what scenario would you not want the operation to start immediately (the queue behavior which is the default), given the app is already in the foreground and the user requested some operation?
Getting the options clear here, the two strategies are "fail" (the default) and "queue". Fail means the task either starts immediately or fails, while queue means that task will wait to start at some future point.
The first thing to understand here is that request failure shouldn't be common. We don't guarantee that the foreground app will "always" be able to start a task; however, in practice, I think it should/will be able to, and, more to the point, if there is some situation where the foreground app is "regularly" unable to start tasks, then that's an issue we'd want to investigate. That means that, in practice, the main reason an app task submission might fail is that the app itself had already started "enough" takes to fill the queue.
With that context, returning to here:
In what scenario would you not want the operation to start immediately?
Off the top of my head, I can think of two different scenarios:
(1) The app tends to generate lots of relatively "short" tasks (say 1s-5s), and the scheduling tends to happen in "bursts". Using "queue" means you can use BGContinuedProcessingTask as your direct work management queue without having to worry about the scheduling of other work types. Yes, that means that sometimes work might take slightly longer to start executing than it otherwise “could," but for very short run work, that often doesn't really matter.
(2) At the other extreme, the app has a significant number of very long-running tasks ("minutes" or even "hours") which are all going to run in parallel and don't really have a predictable endpoint. As a concrete example of this, imagine an iPad acting as the controller/monitor for a collection of external hardware. Instead of trying to manage all of those different work "sources" through a single task, the other option is that they each have their own task identifiers which they independently manage. So, each of them submits their task identifier when they start working and they cancel that task identifier if they happen to finish their work without the continued task having started. Finally, if the task does start then they take over reporting progress to the system but "jumping" their progress state if/when their particular launchHandler fires. This lets you ensure your app stays awake "across" all of those tasks, without having to artificially submit new tasks or creating some complicated mechanism to represent "all" of the work.
On that last point, I think it's worth noting that there doesn't necessarily need to be a "hard" binding between a particular task identifier and a particular operation your app is doing. Another scenario here is if your app is managing a "pool" (say 20 operations) which are happening in parallel but where it's difficult to predict how fast progress will occur until after the job is well underway. One way to do that would be something like the following:
-
Submit 20 tasks, one for each operation.
-
Arbitrarily pick operations to provide initial progress to however many tasks actually start.
-
(Optional) Once "enough" progress has occurred to predict progress, swap the operations chosen in #2 with the fastest operations.
-
When a task completes and a new task launches, that new task is bound to whatever operation is closest to completion.
The larger point I'd make here is that the "point" of this API isn't really to provide the systems with absolutely accurate information about specific work your app is doing. It's to give the USER good information about what your app is doing on their behalf.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware