Thanks for your detailed responses.
I took what you said in the first response and built a simple generic queue which integrates with BGContinuedProcessingTask. I'm attaching the code in case it helps others and because it illustrates some of my lingering questions/suggestions/difficulties.
I'm building it with:
Swift version 6
Nonisolated(nonsending) by default
Approachable concurrency/default MainActor disabled (new projects seem to have this enabled by default)
(this list not in order of importance)
Queues themselves get leaked. The registerWithScheduler method illustrates what I mean by this - the closure must retain self (the queue). It's not a dealbreaker because I imagine queues will not hold on to that many resources, just descriptions of jobs to run, but it's not ideal.
Ideally, the BGTask title/subtitle APIs would accept a LocalizedStringResource so localised strings can be submitted cross-process.
It may be interesting to elevate the priority of the queue-draining Task if running in the background (since in that scenario it's the most important thing running in the App's process). Swift concurrency offers an API for that, but unfortunately no API to de-escalate priority again.
If this API were better integrated with Swift concurrency, we could communicate to the system exactly which Task is performing the background processing, so it could perform that kind of priority control.
I'm sure the system will adjust the scheduling priority of the App's process, but this is about finer-level control within the App's process.
Personally, I'm leaning towards the ultimate form of this API being a global function that accepts an async closure and ideally no other parameters - just run this block and keep running it even if we get backgrounded - I don't even think the task identifier and Info.plist dance is really necessary if we don't use a register/submit-request pattern. The closure would receive some kind of progress reporting channel as an incoming parameter from the system.
I've also attached a little example View which displays the queue and allows adding jobs and running/interrupting the queue. This displays some more serious issues:
The system UI often shows the indeterminate progress view, even though progress information is being communicated. This demo view displays the exact same Progress instance in a ProgressView, yet for some reason shows detailed progress information where the system does not.
The system is very aggressive about killing background jobs. I'm testing on an 11-inch M4 iPad Pro (so a very high-end system), and if I switch to Safari and flip through a few tabs, even this demo job will be killed via the expiration handler. For reference, the device was at 80% charge (using the system charging limiter for battery longevity) and attached to power at the time.
This is a major issue. Our app concept heavily depends on the ability to run ML models on on-device data in jobs that may take 30+ minutes, and while we can accept cancellation from the user or in exceptional circumstances (e.g. battery about to die), we need this to be more reliable. I recall testing this in a previous beta and it didn't seem so aggressive, but admittedly I may not have tested the exact circumstances here.
When this happens, the system also doesn't give you a lot of information about why it decided to cancel a background task. The API says nothing, nothing is logged to the App's console output, and in the system console output all I find is:
dasd | bgContinuedProcessing-.ContinuedProcessingQueue..shared:D3CD03:[
{name: Activity Progress Policy, policyWeight: 0.001, response: {100, 0.00, [{tracker.health == 2}]}}
], Decision: AMNP}
dasd | Suspending bgContinuedProcessing-.ContinuedProcessingQueue..shared:D3CD03 - required criterion is not satisfied.
The system could be more helpful here - in particular, it would be helpful to know whether there may be anything I as a developer can do to alleviate the system's concerns. Is it maybe not understanding my attempts to communicate progress updates (see above) and thinks the Task has hanged? I have no idea. I can't even begin to diagnose from this.
I'm stressing about this a little bit because my understanding was that this significantly loosens the restrictions on background processing on iOS, with the condition that the user is kept well-informed via system UI. But as it is, I'm struggling to see what kinds of new App experiences this can really deliver unless the system is much less aggressive about killing these background jobs that the user is expecting to continue processing.
<I had to post the code to GitHub, because these forums have a max character limit: https://gist.github.com/panv-kw/b59fd537a78ee5a79cd2175be891ac3f>