Live Activity: Ending Activities

Part 6: Ending Expired Live Activities

Shawn Roller

December 1, 2024

GuideSuccess

All good things must come to an end.

In the previous posts we’ve designed, planned, and executed on a Live Activity without the support of push notifications. But the activity can remain on the lock screen for 12 hours, unless the user dismisses it. It is very likely to no longer be relevant at some point, so we should try to dismiss it.

Background task

One strategy is to schedule a background task which wakes up our main app and kills an expired activity. In SwiftUI we can do something like this:

LiveActivityOfflineDemoApp.swift
1let BG_TASK_ID = "my_background_task"
2
3func scheduleBackgroundTask() {
4    do {
5        print("setting background task")
6        let today = Calendar.current.startOfDay(for: .now)
7        let thirtySecondsFromNow = Calendar.current.date(byAdding: .second, value: 30, to: today)!
8        
9        let request = BGAppRefreshTaskRequest(identifier: BG_TASK_ID)
10        request.earliestBeginDate = thirtySecondsFromNow
11        try BGTaskScheduler.shared.submit(request)
12        
13        // For testing, break on this line and force a background task to be executed
14        // in LLDB: e -l objc -- (void)[[BGTaskScheduler shared] _simulateLaunchForTaskWithIdentifier:@"test"]
15        print("task scheduled")
16    } catch {
17        print(error.localizedDescription)
18    }
19}

And then we can do something when the background task executes:

LiveActivityOfflineDemoApp.swift
1@main
2struct LiveActivityOfflineDemoApp: App {
3    var body: some Scene {
4        WindowGroup {
5            ContentView().onOpenURL { url in
6                guard let url = URLComponents(string: url.absoluteString) else { return }
7                print(url)
8            }
9        }
10        .backgroundTask(.appRefresh(BG_TASK_ID)) {
11            print("Do something...")
12        }
13    }
14}

However background task execution can be unreliable, so we also need another method to check and terminate our expired live activities.

Terminate on Scene Change

We can put a simple check in place, so that every time the our app changes “scenes” (foreground, background, etc.) we will check for expired activities and, if we find any, we’ll end them.

First we’ll attach an onChange modifier to our main content view body:

LiveActivityExtension.swift
1.onChange(of: scenePhase) { (newScenePhase, _) in
2    endStaleActivities()
3}

And then we’ll add the function to find and end stale activities:

ContentView.swift
1func endStaleActivities() {
2    Task {
3        for activity in activities {
4            if let staleDate = activity.content.staleDate, staleDate <= Date.now {
5                let endTime = Date.now + 1
6                let state = LiveActivityExtensionAttributes.LiveActivityState(emoji: "😄")
7                let content = ActivityContent(state: state, staleDate: endTime)
8                await activity.end(content, dismissalPolicy: .immediate)
9            }
10        }
11        // update the UI
12        refreshActivitiesState()
13    }
14}

Now when our app is opened or put into the background, we’ll quickly iterate through all the activities and end any that are “stale”, based on the “end date” that we specified when we created the activity.

Stale state

When we create the live activity we set a staleDate which is when the activity becomes “stale”, e.g. its content is no longer up-to-date. For our activity, because we set all of the content and state when we create the activity, it’s not really relevant.

However I would be remiss if I didn’t mention how to handle a stale activity. In your extension you can access the context.isStale property and key off of that to show a different UI, for example: ActivityContentView(context: context, isStale: context.isStale)

For example, in Apple’s example project they add a badge to the Live Activity which makes it obvious the data may be out of date.

Wrap up

That wraps up our series on an offline Live Activity. I hope you learned something along the way. Remember to check out the full project on Github.