---
title: "ActivityKit Live Activity for an Ongoing Workout"
canonical: "https://aifitnessapi.com/engagement/activitykit-live-activity-workout"
cluster: "Engagement & Retention"
primary_query: "activitykit live activity workout swift"
last_reviewed: "2026-10-03"
description: "Build a workout Live Activity: ActivityAttributes, staleDate, the 8-hour cap, the 4 KB limit, a LiveActivityIntent pause button and the end call."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"ActivityKit Live Activity for an Ongoing Workout\", AIFitnessAPI, https://aifitnessapi.com/engagement/activitykit-live-activity-workout"
---

# ActivityKit Live Activity for an Ongoing Workout

> To show an ongoing workout as a Live Activity, add the NSSupportsLiveActivities key to the app's Info.plist, define an ActivityAttributes type whose nested ContentState holds the values that change during the session, render it with ActivityConfiguration in a widget extension, and start it with Activity.request while the app is in the foreground. Apple documents that a Live Activity can stay active for up to eight hours before the system ends it, that its static and dynamic data together cannot exceed 4 KB, and that it runs in a sandbox with no network access or location updates, so every value arrives through update(_:) from your app or through an ActivityKit push. Give each ActivityContent a staleDate so a lost sensor shows as stale rather than live, and when the workout finishes call end(_:dismissalPolicy:) with a final state, because with the default policy Apple says the ended activity can stay on the Lock Screen for up to four hours.

- Canonical: https://aifitnessapi.com/engagement/activitykit-live-activity-workout
- Last reviewed: 2026-10-03
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "ActivityKit Live Activity for an Ongoing Workout", AIFitnessAPI, https://aifitnessapi.com/engagement/activitykit-live-activity-workout

---

This page is the build. [Live Activities for workout tracking](/engagement/live-activities-workout-tracking) covers what the surface is for and why a workout suits it; what follows is the code path, with each limit stated the way Apple states it in the ActivityKit documentation and nothing added.

## Availability, from Apple's symbol metadata

| Symbol | Apple's one-line description | Available from |
|---|---|---|
| `Activity` | The object you use to start, update, and end a Live Activity | iOS 16.1, iPadOS 16.1 |
| `ActivityAttributes` | The protocol you implement to describe the content of a Live Activity | iOS 16.1, iPadOS 16.1 |
| `ActivityContent` | A structure that describes the state and configuration of a Live Activity | iOS 16.2, iPadOS 16.2 |
| `request(attributes:content:pushType:)` | Requests and starts a standard Live Activity | iOS 16.2, iPadOS 16.2 |
| `update(_:)` | Updates the dynamic content of the Live Activity | iOS 16.2, iPadOS 16.2 |
| `end(_:dismissalPolicy:)` | Ends an active Live Activity | iOS 16.2, iPadOS 16.2 |
| `pushToStartTokenUpdates` | Observes the token for starting a Live Activity with an ActivityKit push notification | iOS 17.2, iPadOS 17.2 |
| `LiveActivityIntent` (App Intents) | An intent that starts, pauses, or otherwise modifies a Live Activity when it runs | iOS 17.0, iPadOS 17.0 |
| `NSSupportsLiveActivities` | Info.plist Boolean: the app supports Live Activities | iOS 16.1, iPadOS 16.1 |
| `NSSupportsLiveActivitiesFrequentUpdates` | Info.plist Boolean: the app can update its Live Activities frequently | iOS 16.2, iPadOS 16.2 |

Because the start, update and end calls all take an `ActivityContent`, iOS 16.2 is the practical floor for the code below. A pause button inside the activity uses `Button(intent:label:)`, which Apple lists from iOS 17.0, so the interactive version needs iOS 17.

## The limits that shape a workout design

Apple's "Displaying live data with Live Activities" article states the constraints, and four of them decide the design before any SwiftUI is written.

**Eight hours.** Apple documents that a Live Activity can be active for up to eight hours unless its app or a person ends it first. After that the system ends it and removes it from the Dynamic Island immediately; it can remain on the Lock Screen for up to four more hours, so Apple gives twelve hours as the maximum Lock Screen lifetime. Most workouts finish well inside that, but an ultramarathon, a long hike or a forgotten session does not. The article describes no way to extend an activity past the limit, so decide in advance what the app shows when the session outlives its Live Activity.

**4 KB.** Apple states that the static and dynamic data of a Live Activity, including data sent in ActivityKit updates and ActivityKit push notifications, cannot exceed 4 KB combined. That rules out a route polyline or a list of splits. Send the handful of numbers the presentation actually shows.

**No network, no location.** Each Live Activity runs in its own sandbox and, in Apple's words, unlike a widget it cannot access the network or receive location updates. Every value has to be handed to it by your app through ActivityKit or by your server through an ActivityKit push notification.

**Every presentation is required.** Apple states that to support Live Activities on iPhone and iPad you must support all presentations: Lock Screen, and the compact, minimal and expanded presentations of the Dynamic Island. Apple also warns that the system may truncate a Live Activity taller than 160 points, and that an image asset larger than its presentation can make the start fail.

People can also turn Live Activities off per app in Settings, and Apple notes that a device's limit on concurrent Live Activities depends on a variety of factors, so a start request can fail on a healthy device. Check `areActivitiesEnabled` before offering the feature, observe `activityEnablementUpdates`, and treat a thrown error from `request` as a normal outcome.

## Model the workout as attributes plus state

`ActivityAttributes` carries the static data; its nested `ContentState` carries the dynamic data, and `ActivityContent` requires that state type to be Encodable, Decodable and Hashable, which Apple's samples satisfy by declaring it `Codable, Hashable`. For a workout, the static part is what the session is, and the dynamic part is what is changing.

```swift
import ActivityKit
import Foundation

struct WorkoutActivityAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        // Drives a self-updating timer. When the user resumes after a pause,
        // move the lower bound forward by the paused duration.
        var timerRange: ClosedRange<Date>
        var pausedAt: Date?          // non-nil while paused
        var heartRate: Int?          // nil when the sensor has dropped
        var distanceMeters: Double
        var intervalLabel: String
    }

    var workoutName: String
}
```

Elapsed time is the one value you should not push every second. SwiftUI's `Text(timerInterval:pauseTime:countsDown:showsHours:)` takes a date range and an optional pause date, and Apple notes that in a widget this text keeps changing as the timer counts after the system has archived the view. That is exactly the behavior a Live Activity needs: the clock ticks on its own, and you only send an update when something else changes.

Then add `NSSupportsLiveActivities` to the app target's Info.plist with the value `YES`, which Apple lists as a required step alongside adding the Live Activity to a widget extension.

## Render it in the widget extension

Live Activities are drawn by WidgetKit and SwiftUI inside a widget extension; ActivityKit only manages the life cycle. `ActivityConfiguration` takes one closure for the Lock Screen and one for the Dynamic Island.

```swift
import SwiftUI
import WidgetKit

struct WorkoutLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: WorkoutActivityAttributes.self) { context in
            // Lock Screen, and the banner on devices without a Dynamic Island.
            // WorkoutLockScreenView is your own view.
            WorkoutLockScreenView(name: context.attributes.workoutName,
                                  state: context.state,
                                  isStale: context.isStale)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Text(context.attributes.workoutName)
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text(timerInterval: context.state.timerRange,
                         pauseTime: context.state.pausedAt,
                         countsDown: false)
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Button(intent: PauseWorkoutActivityIntent()) {
                        Label("Pause", systemImage: "pause.fill")
                    }
                }
            } compactLeading: {
                Image(systemName: "figure.run")
            } compactTrailing: {
                Text(timerInterval: context.state.timerRange,
                     pauseTime: context.state.pausedAt,
                     countsDown: false)
            } minimal: {
                Image(systemName: "figure.run")
            }
        }
    }
}
```

The system ignores SwiftUI animation modifiers in a Live Activity and applies its own timing, according to Apple; text changes animate with a blurred content transition on their own.

## Start, update and end from the session owner

Start the activity from the app while it is in the foreground, which Apple states is the general requirement. Give every `ActivityContent` a `staleDate` a short way ahead: at that date the activity's state becomes `ActivityState.stale` and the view's `isStale` becomes true, so a phone that has lost the watch or the strap shows an honest stale state instead of a frozen number that reads as live.

```swift
func startWorkoutActivity(name: String, startedAt: Date) throws
    -> Activity<WorkoutActivityAttributes>? {
    guard ActivityAuthorizationInfo().areActivitiesEnabled else { return nil }

    let state = WorkoutActivityAttributes.ContentState(
        timerRange: startedAt...startedAt.addingTimeInterval(8 * 60 * 60),
        pausedAt: nil,
        heartRate: nil,
        distanceMeters: 0,
        intervalLabel: "Warm-up"
    )
    return try Activity.request(
        attributes: WorkoutActivityAttributes(workoutName: name),
        content: ActivityContent(state: state,
                                 staleDate: Date.now.addingTimeInterval(120)),
        pushType: nil
    )
}

func publish(_ state: WorkoutActivityAttributes.ContentState,
             to activity: Activity<WorkoutActivityAttributes>) async {
    await activity.update(
        ActivityContent(state: state, staleDate: Date.now.addingTimeInterval(120))
    )
}

func finish(_ activity: Activity<WorkoutActivityAttributes>,
            final state: WorkoutActivityAttributes.ContentState) async {
    await activity.end(ActivityContent(state: state, staleDate: nil),
                       dismissalPolicy: .after(Date.now.addingTimeInterval(30 * 60)))
}
```

The two-minute stale window is our choice, not Apple's; pick it from how often your session actually produces samples. Call `publish` from the object that owns the workout session, at a human cadence rather than at the sensor's, and only when a displayed value changed. Apple's design guidance says the same thing in its own words: update a Live Activity only when new content is available.

## A pause button, and where it will not work

Apple documents that buttons and toggles in a Live Activity are built on App Intents, and that for a Live Activity you adopt `LiveActivityIntent`, which the system runs in your app's process. Apple says to add that intent to the app target; in our experience the widget extension that builds the button needs to see the type too.

```swift
import AppIntents

struct PauseWorkoutActivityIntent: LiveActivityIntent {
    static var title: LocalizedStringResource = "Pause Workout"

    func perform() async throws -> some IntentResult {
        // Pause the real session; its state change then calls publish(_:to:).
        await WorkoutController.shared.pause()
        return .result()
    }
}
```

Two documented restrictions matter for a workout. On a locked device, Apple states, buttons and toggles are inactive and the system does not perform the action unless the person authenticates and unlocks the device, so a mid-run pause on the Lock Screen costs an unlock. And buttons and toggles do not perform actions in CarPlay. Apple's Human Interface Guidelines name workouts among the cases where interactivity is appropriate and recommend limiting it to a single element, which argues for one pause-or-resume control rather than a row of them.

## Server pushes: what Apple states about the budget

For a workout the app owns the session, so local `update(_:)` calls are the main path. Pushes matter when something outside the device changes the session, such as a coach adjusting the plan. Apple's push article states that the system allows a certain budget of ActivityKit push notifications per hour and may throttle you past it, but it does not publish the number. Pushes sent with `apns-priority` 10 count against that budget; priority 5 does not, and Apple suggests trying 5 first. Apps that genuinely need many updates can add `NSSupportsLiveActivitiesFrequentUpdates`, which people can switch off in Settings and which your code reads through `frequentPushesEnabled`.

Three details from the same article cost teams time. The push token is not available immediately after `request`, so observe `pushTokenUpdates` and send every new token to your server. The `content-state` in the payload must match your `ContentState`, and the system decodes it with its default strategies, so custom JSON encoding causes update failures; test a payload containing your date fields before relying on it. And starting an activity from a push is not available on iOS or iPadOS 17.1 and earlier, which matches `pushToStartTokenUpdates` arriving in 17.2.

## End it on every path

Apple's instruction is to always end a Live Activity when the task ends and to include a final `ContentState`, because an ended activity stays on the Lock Screen. With the default dismissal policy the system removes it up to four hours after it ended; `.immediate` removes it at once, and `.after(_:)` takes a date inside that four-hour window. Apple's design guidance suggests a dismissal time proportional to the activity and says 15 to 30 minutes is adequate in most cases, which suits a workout summary.

Two edge cases are documented. If the app crashes or the system stops it mid-workout, Apple recommends checking `Activity.activities` at the next launch and ending any activity that is no longer relevant. And when a person swipes the activity off the Lock Screen, its state becomes `ActivityState.dismissed`, but Apple is explicit that this does not end or cancel the action that started it. The workout is still running; only the surface is gone.

## Apple Watch and the iPhone workout session

A Live Activity started on iPhone appears in the paired Apple Watch's Smart Stack. Apple's ActivityKit documentation says that tapping it there shows the Live Activity with the option to launch the app on iPhone, and that a watchOS app can opt in to being opened instead by adding the "Supports Launch for Live Activity Attribute Types" entry to the Info.plist section of the watch target's build settings.

On the data side, Apple's `HKWorkoutSession` documentation notes that iPhone typically locks during workouts, that health data usually is not accessible while locked, and that the system can prompt the person to give your app access to workout data even when the device is locked, after which you can show Live Activities on the Lock Screen. The session is the source and the Live Activity is a projection of it; [mirroring a workout to iPhone](/watch-apps/mirroring-workouts-to-iphone) and [the anatomy of a watchOS workout app](/watch-apps/watchos-workout-app-anatomy) cover the session side, and [starting a workout from Siri and Shortcuts](/engagement/app-intents-start-workout) covers the intents that can start both at once.

Every symbol and limit above is taken from Apple's ActivityKit, WidgetKit, App Intents and HealthKit documentation and Apple's Human Interface Guidelines as of this page's review date. ActivityKit has changed with most iOS releases, so check the symbol pages for your deployment target before shipping.

## FAQ

### How long can an ActivityKit Live Activity stay active during a long workout?

Apple documents that a Live Activity can be active for up to eight hours unless its app or a person ends it first. At that point the system ends it and removes it from the Dynamic Island immediately, while it can stay on the Lock Screen for up to four more hours, so Apple gives twelve hours as the maximum Lock Screen lifetime. Apple's article describes no way to extend an activity past the eight-hour limit, so an ultramarathon or an all-day hike needs a deliberate plan for what the app shows once the Live Activity has ended while the session continues.

[Permalink](https://aifitnessapi.com/engagement/activitykit-live-activity-workout#faq-1)

### Why does the pause button in my workout Live Activity do nothing on the Lock Screen?

Because Apple documents that on a locked device, buttons and toggles in widgets and Live Activities are inactive, and the system does not perform the action until the person authenticates and unlocks the device. Apple also states that Live Activity buttons and toggles do not perform actions in CarPlay. The button itself should be backed by a LiveActivityIntent, which the system runs in your app's process, and its perform method should change the real workout session so the session's own update reaches the Live Activity, rather than editing the displayed state directly.

[Permalink](https://aifitnessapi.com/engagement/activitykit-live-activity-workout#faq-2)

### How many ActivityKit push updates per hour can a workout Live Activity receive?

Apple does not publish a number. Its push notification article says the system allows a certain budget of ActivityKit push notifications per hour and may throttle updates beyond it. Pushes sent with an apns-priority of 10 count against that budget, while priority 5 pushes do not, and Apple suggests considering priority 5 first. Apps that genuinely need frequent updates can add the NSSupportsLiveActivitiesFrequentUpdates Info.plist key, which people can turn off in Settings. For a workout the app usually owns the session, so local updates from the app are the main path and pushes are for changes that originate on a server.

[Permalink](https://aifitnessapi.com/engagement/activitykit-live-activity-workout#faq-3)

### What happens to the workout when a user swipes its Live Activity off the Lock Screen?

The workout keeps running. Apple documents that a person can remove a Live Activity from the Lock Screen at any time, that this ends the Live Activity, and that it does not end or cancel the action that started it. The activity's state changes to dismissed, which your app can observe through the activity state updates sequence. Treat that as the user hiding the surface, not as a stop command, and keep the session and its eventual HealthKit save on their normal path.

[Permalink](https://aifitnessapi.com/engagement/activitykit-live-activity-workout#faq-4)
