Modern Semaphore
How to end an iOS Live Activity on app termination?

Most recently, when I was trying to end Live Activities completely and immediately and remove from both Dynamic Island and lock screen when the app terminated, I had a nondeterministic behavior. That Live Activity was sometimes removed sometimes not. After conducting some research, I discovered the solution using both modern Swift Concurrency and old Semaphore like below.
final class AppDelegate: NSObject, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) {
let semaphore = DispatchSemaphore(value: 0)
// Intentional: on termination we have a tiny window to end Live Activities.
// A detached task avoids main-actor inheritance while we do a short bounded wait.
Task.detached(priority: .high) {
for activity in Activity<WalkActivityAttributes>.activities {
let finalContent = ActivityContent(state: activity.content.state, staleDate: nil)
await activity.end(finalContent, dismissalPolicy: .immediate)
}
// the semaphore’s .signal method is called to increment the number of available resources
// so that whoever is waiting to access our resource can eventually gain access
semaphore.signal()
}
// Every time we call .wait on the semaphore, the number of available resources either
// decreases, or we wait for a resource to become available.
_ = semaphore.wait(timeout: .now() + 2)
}
}Let’s visualize that code in the Dining Philosopher Problem manner but from the restaurant staff perspective.
The main purpose here is to clear the Live Activity as quickly as possible before the app completely terminates.
But the problem is;
activity.end(...) works async,
the process may be interrupted,
Result: Live Activity stays remaining in both screens.
That semaphore solution does; wait for a while before the app termination and Live Activity cleaning ends.
Let’s visualize in the restaurant analogy 👇
Restaurant Analogy
You are the restaurant owner. Restaurant will close soon. But there are still customers eating:
Live Activities appear in lock screen
Live Activities in the Dynamic Island
You say: “Get out all customers before restaurant close 🤬”
1. Creating Semaphores
let semaphore = DispatchSemaphore(value: 0)It means, put a waiter to the door. But the waiter at first in “None of the customers have not completed their dinner.“ status. So, the restaurant owner (app) has to wait.
value: 0 means there is no allowance to close restaurant.
2. Cleaning starts in background
Task.detached(priority: .high) {}Send waiters to clean tables quickly.
Logic behind detached is “have a separate team work without involving the main restaurant manager”
3. Remove all customers
for activity in Activity<WalkActivityAttributes>.activities {
await activity.end(...)
}This means, the waiter goes to all tables and says “Restaurant is closing, you must leave.“
await is important because, the waiter is really expecting customers to leave. But, this doesn’t happen immediately.
4. Notify the waiter when the job is finished
semaphore.signal()It means, the waiter comes and tells “Finally, all customers are gone 😮💨“
DispatchSemaphore value becomes 0 → 1. So, we can close the restaurant.
5. App waits for a bit
_ = semaphore.wait(timeout: .now() + 2)The restaurant owner waits on the doorstep and yells to staff “I’ll wait for 2 sec until cleaning completes“.
If:
Staff finish their job → restaurant closes properly.
If they can’t → time’s up → restaurant closes.
Why this code is important?
Because iOS doesn’t wait async jobs while app is terminating.
So, normally if you do that app can directly close:
await activity.end()Semaphore solution says, “One minute, let’s finish this job before close”.
What does Semaphore represent here?
In the restaurant example:
wait(): The waiter waits on the doorstep.
signal(): The staff says “Job is done”.
value: 0: No close.
value: 1: Can be close now.
Briefly
App will terminate
Start closing Live Activities
Wait a short while so the app doesn’t close immediately
Continue when the work is finished
Close again after 2 sec max
Semaphore here works like a:
“Traffic police who manages closing road for a short time” 🙂
The Modern Way
DispatchSemaphore is an old school synchronization tool which comes from Grand Central Dispatch world. In the modern swift concurrency approach async/await and Task usage will be chosen.
In theory we can use something like below for terminating Live Activities:
func endAllActivities() async {
for activity in Activity<WalkActivityAttributes>.activities {
let finalContent = ActivityContent(
state: activity.content.state,
staleDate: nil
)
await activity.end(
finalContent,
dismissalPolicy: .immediate
)
}
}and we can call it:
Task {
await endAllActivities()
}However, there is an important problem here is the applicationWillTerminate is not an async lifecycle callback. iOS doesn’t guarantee the completion of async tasks when terminating and application. Therefore, while using only Task {} is more modern in theory, it may not be reliable in practice.
Hence, the most healthier approach is not doing Live Activity cleanup while app termination but doing it in either while switching to background or doing it before the scene has been inactive.
For instance:
func sceneDidEnterBackground(_ scene: UIScene) {
Task {
await endAllActivities()
}
}This approach:
It is natively compatible with Swift Concurrency
It does not cause thread blocking
It removes Semaphore requirement
It increases completion possibility of async tasks
Conclusion
Task + async/await is the modern approach but there is no 100% guarantee during the app termination. Hence, the best approach is doing cleanup at the early stages of the lifecycle.


