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.
Thanks Kevin! So I need to think about this a little differently then. Instead of performing the work once you get a BGContinuedProcessingTask, just begin performing the work (the exact same way you do in iOS 18), if a BGContinuedProcessingTask starts persist it and update it as the work completes, and reset state when the work finishes or gets canceled.
Basically, something like this: 1-6
I put together code to demonstrate this approach. Did I understand correctly, this should be all good with this simple counting demo?
class ViewController: UIViewController {
private let taskIdentifier = "\(Bundle.main.bundleIdentifier!).count_task"
private let countToNumber: Int64 = 100
private var currentBackgroundContinuedProcessingTask: BGContinuedProcessingTask?
private var shouldStopCounting = false
@IBOutlet var progressLabel: UILabel!
@IBAction func startCountingTapped(_ sender: UIButton) {
performCountWork()
// Register to support background continued processing
// Ensure this is only registered once otherwise app crashes with exception: Launch handler for task with identifier has already been registered
if !(UIApplication.shared.delegate as! AppDelegate).didRegisterCountBackgroundContinuedTask {
(UIApplication.shared.delegate as! AppDelegate).didRegisterCountBackgroundContinuedTask = true
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: .main) { task in
guard let task = task as? BGContinuedProcessingTask else { return }
// The background continued processing task has started, use this task to manage the ongoing work
self.currentBackgroundContinuedProcessingTask = task
task.expirationHandler = {
self.shouldStopCounting = true
}
}
}
// Request a background continued processing task start
let request = BGContinuedProcessingTaskRequest(
identifier: taskIdentifier,
title: "Demo Task",
subtitle: progressText(completedUnitCount: 0, totalUnitCount: countToNumber)
)
request.strategy = .fail // Start the task immediately and fail if it cannot
do {
try BGTaskScheduler.shared.submit(request)
} catch {
// This should approximately never happen because the app doesn't submit any more tasks to exceed the system limit
// But no big deal if it does not start, the user will just have to keep the app open to make progress
print("Failed to submit task: \(error.localizedDescription)")
}
}
@IBAction func stopCountingTapped(_ sender: UIButton) {
shouldStopCounting = true
}
private func performCountWork() {
Task {
var countedToNumber: Int64 = 0
progressLabel.text = progressText(completedUnitCount: countedToNumber, totalUnitCount: countToNumber)
currentBackgroundContinuedProcessingTask?.progress.completedUnitCount = countedToNumber
currentBackgroundContinuedProcessingTask?.progress.totalUnitCount = Int64(countToNumber)
while !shouldStopCounting && countedToNumber < countToNumber {
try! await Task.sleep(nanoseconds: 1_000_000_000)
countedToNumber += 1
progressLabel.text = progressText(completedUnitCount: countedToNumber, totalUnitCount: countToNumber)
if let task = currentBackgroundContinuedProcessingTask {
task.progress.completedUnitCount = countedToNumber
task.updateTitle(task.title, subtitle: progressText(completedUnitCount: countedToNumber, totalUnitCount: countToNumber))
}
}
progressLabel.text = shouldStopCounting ? "Canceled" : "Success!"
currentBackgroundContinuedProcessingTask?.setTaskCompleted(success: !shouldStopCounting)
currentBackgroundContinuedProcessingTask = nil
shouldStopCounting = false
}
}
private func progressText(completedUnitCount: Int64, totalUnitCount: Int64) -> String {
return "\(completedUnitCount) / \(totalUnitCount)"
}
}
This seems to work, minus some behavior I did not expect:
The Live Activity appears upon Start Counting while the app is in the foreground, I expected it to not appear until the user backgrounds the app. I'm using iPad Pro 26.0 (23A341). But on iPhone Air with 26.1 (23B5044l) the Live Activity does not appear until I close the app. Perhaps a bug that got resolved?
The circular progress bar is spinning indeterminately in the Live Activity, not filling the circle as a percentage of completion (on both devices).
the two strategies are "fail" (the default) and "queue"
BGContinuedProcessingTaskRequest.strategy docs state queue is the default 😅