You can combine pawello222's answer with Mojitaba Hosseini's answer from another question on how to make synchronous tasks to synchronously process asynchronous code on app termination event, like so:
NotificationCenter.default.addObserver(forName: UIApplication.willTerminateNotification, object: nil, queue: .main) { [self] _ in
Task.synchronous { [self] in
await asyncWork()
}
}
Task.synchronous extension (credit to Mojitaba Hosseini):
extension Task where Failure == Error {
/// Performs an async task in a sync context.
///
/// - Note: This function blocks the thread until the given operation is finished. The caller is responsible for managing multithreading.
static func synchronous(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) {
let semaphore = DispatchSemaphore(value: 0)
Task(priority: priority) {
defer { semaphore.signal() }
return try await operation()
}
semaphore.wait()
}
}
If you use actors in your code at all, awaiting any actor method or variable will cause the Task to never complete/to be killed. Instead, you should resolve and store any actor variables locally, then capture them in the Task closure to use them without any actor access waiting.
Example:
let peripheral = await localPeripheral
peripheralTerminateNotification = NotificationCenter.default.addObserver(forName: UIApplication.willTerminateNotification, object: nil, queue: .main) { [self] _ in
Task.synchronous { [self] in
await stop(peripheral: peripheral)
}
}