Claude
Skills
Sign in
Back

activitykit

Included with Lifetime
$97 forever

Implement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.

Data & Analytics

What this skill does


# ActivityKit

ActivityKit owns real-time, glanceable Live Activities displayed on the Lock
Screen and, on supported devices, Dynamic Island. StandBy, CarPlay, and a
paired Mac can also display Live Activities, but do not blur that core routing:
ordinary Home Screen/timeline widgets belong in `widgetkit`, and generic APNs
setup belongs in `push-notifications`. Live Activity push payload shape stays in
ActivityKit: device-token updates use `apns-push-type: liveactivity` and
`apns-topic: <bundle-id>.push-type.liveactivity`, while `aps.content-state` must
decode into the app's actual `ActivityAttributes.ContentState` `Codable` shape.
Do not assume `Date` or `ClosedRange<Date>` use Unix timestamp
`lowerBound`/`upperBound` dictionaries unless the Swift model and server
contract coordinate that encoding. Boundary answers that keep ActivityKit APNs
payloads, `content-state`, or Live Activity data contracts in scope should
include these payload-shape invariants even when routing generic APNs setup
elsewhere. Patterns target iOS 26+ with Swift 6.3;
modern `ActivityContent` lifecycle examples require iOS 16.2+ unless noted.

See [references/activitykit-patterns.md](references/activitykit-patterns.md) for complete code patterns including push payload formats, concurrent activities, state observation, and testing.

## Contents

- [Workflow](#workflow)
- [ActivityAttributes Definition](#activityattributes-definition)
- [Activity Lifecycle](#activity-lifecycle)
- [Lock Screen Presentation](#lock-screen-presentation)
- [Dynamic Island](#dynamic-island)
- [Push-to-Update](#push-to-update)
- [Recent Additions](#recent-additions)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Workflow

### 1. Create a new Live Activity

1. Add `NSSupportsLiveActivities = YES` to the host app's Info.plist.
2. Define an `ActivityAttributes` struct with a nested `ContentState`.
3. Create an `ActivityConfiguration` in the widget bundle with Lock Screen
   content and Dynamic Island closures.
4. Start the activity with `Activity.request(attributes:content:pushType:)`.
5. Update with `activity.update(_:)` and end with `activity.end(_:dismissalPolicy:)`.
6. Forward push tokens to your server for remote updates.

### 2. Review existing Live Activity code

Run through the Review Checklist at the end of this document.

## ActivityAttributes Definition

Define both static data (immutable for the activity lifetime) and dynamic
`ContentState` (changes with each update). Keep `ContentState` small because
the entire struct is serialized on every update and push payload.

```swift
import ActivityKit

struct DeliveryAttributes: ActivityAttributes {
    // Static -- set once at activity creation, never changes
    var orderNumber: Int
    var restaurantName: String

    // Dynamic -- updated throughout the activity lifetime
    struct ContentState: Codable, Hashable {
        var driverName: String
        var estimatedDeliveryTime: ClosedRange<Date>
        var currentStep: DeliveryStep
    }
}

enum DeliveryStep: String, Codable, Hashable, CaseIterable {
    case confirmed, preparing, pickedUp, delivering, delivered

    var icon: String {
        switch self {
        case .confirmed: "checkmark.circle"
        case .preparing: "frying.pan"
        case .pickedUp: "bag.fill"
        case .delivering: "box.truck.fill"
        case .delivered: "house.fill"
        }
    }
}
```

### Stale Date

Set `staleDate` on `ActivityContent` to tell the system when content becomes outdated. The system sets `context.isStale` to `true` after this date; show fallback UI (e.g., "Updating...") in your views.

```swift
let content = ActivityContent(
    state: state,
    staleDate: Date().addingTimeInterval(300), // stale after 5 minutes
    relevanceScore: 75
)
```

## Activity Lifecycle

### Starting

Use `Activity.request` to create and display a Live Activity. Pass `.token` as
the `pushType` to enable remote updates via APNs. The `ActivityContent` request
shown here requires iOS 16.2+.

```swift
let attributes = DeliveryAttributes(orderNumber: 42, restaurantName: "Pizza Place")
let state = DeliveryAttributes.ContentState(
    driverName: "Alex",
    estimatedDeliveryTime: Date()...Date().addingTimeInterval(1800),
    currentStep: .preparing
)
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)

do {
    let activity = try Activity.request(
        attributes: attributes,
        content: content,
        pushType: .token
    )
    print("Started activity: \(activity.id)")
} catch {
    print("Failed to start activity: \(error)")
}
```

### Updating

Update the dynamic content state from the app. Use `AlertConfiguration` to
trigger a visible banner and sound alongside the update.

```swift
let updatedState = DeliveryAttributes.ContentState(
    driverName: "Alex",
    estimatedDeliveryTime: Date()...Date().addingTimeInterval(600),
    currentStep: .delivering
)
let updatedContent = ActivityContent(
    state: updatedState,
    staleDate: Date().addingTimeInterval(300),
    relevanceScore: 90
)

// Silent update
await activity.update(updatedContent)

// Update with an alert
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
    title: "Order Update",
    body: "Your driver is nearby!",
    sound: .default
))
```

### Ending

End the activity when the tracked event completes. Choose a dismissal policy
to control how long the ended activity lingers on the Lock Screen.

```swift
let finalState = DeliveryAttributes.ContentState(
    driverName: "Alex",
    estimatedDeliveryTime: Date()...Date(),
    currentStep: .delivered
)
let finalContent = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)

// System decides when to remove (up to 4 hours)
await activity.end(finalContent, dismissalPolicy: .default)

// Remove immediately
await activity.end(finalContent, dismissalPolicy: .immediate)

// Remove after a specific time (max 4 hours from now)
await activity.end(finalContent, dismissalPolicy: .after(Date().addingTimeInterval(3600)))
```

Always end activities on all terminal code paths -- success, user/app
cancellation, sign-out/session stop, unrecoverable app error, and terminal server
failure. If the server says the tracked event can no longer continue or be
represented accurately, apply or send a final terminal state and end the activity
instead of leaving stale progress visible. When reviewing duration claims,
distinguish the active lifetime (up to 8 hours unless the app or user ends it
sooner), system-ended Lock Screen presence (up to 4 additional hours, for 12
hours total from start), and app-ended `.default` dismissal linger (up to 4 hours
after ending).

## Lock Screen Presentation

The Lock Screen is the primary Live Activity display surface. Every device with
iOS 16.1+ displays Live Activities here. Design this layout first, then adapt
for Dynamic Island where available.

```swift
struct DeliveryActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            VStack(alignment: .leading) {
                Text(context.attributes.restaurantName).font(.headline)

                if context.isStale {
                    Label("Updating...", systemImage: "arrow.trianglehead.2.clockwise")
                        .foregroundStyle(.secondary)
                } else {
                    Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
                        .monospacedDigit()
                }
            }
            .padding()
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.center) {
                    Text(context.attributes.restaurantName).font(.headline)
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text(timerInterval: context.state.estimatedDeliveryTime, c

Related in Data & Analytics