Home / Blog / Live Activities and Dynamic Island: lessons from 2 production apps

Live Activities and Dynamic Island: lessons from 2 production apps

Live Activities shipped with iOS 16, the Dynamic Island with the iPhone 14 Pro. Here are the ActivityKit patterns I picked up while building them into a fitness app and a delivery tracker.

Live Activities went production-ready with iOS 16.1. They let you surface a live slice of your app on the lock screen and in the Dynamic Island. Perfect for sports scores, Uber driver location, food order status, timers, and similar.

I’ve implemented Live Activities on 2 projects: workout duration tracking in a fitness app, and courier location in a delivery app. Here’s what I learned.

What a Live Activity is, and isn’t

A Live Activity:

  • Shows up on the lock screen or in the Dynamic Island
  • Updates continuously (via push notification or locally)
  • Stays alive for a maximum of 8 hours (or 12 hours idle)
  • Doesn’t require your app to be active

It isn’t:

  • A widget (widgets are static, timeline-based; Live Activities are dynamic, event-driven)
  • A persistent notification (different visuals, different behavior)
  • A background execution mechanism (background tasks are separate)

The ActivityKit framework

SwiftUI-based, declarative. Four main pieces:

  1. ActivityAttributes: a struct defining the activity shape (both static and dynamic state)
  2. ActivityKit.Activity: manages the runtime instance (start, update, end)
  3. WidgetConfiguration (ActivityConfiguration): defines the activity UI
  4. Dynamic Island: expanded, compact leading/trailing, and minimal views are each defined separately

Simple example:

struct WorkoutAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var duration: TimeInterval
        var caloriesBurned: Int
    }
    var workoutType: String
}

Starting: the app has to trigger it

You can’t start a Live Activity behind the user’s back. The user has to open the app and take an action (“start workout”, “confirm order”).

let attributes = WorkoutAttributes(workoutType: "Running")
let state = WorkoutAttributes.ContentState(duration: 0, caloriesBurned: 0)

do {
    let activity = try Activity<WorkoutAttributes>.request(
        attributes: attributes,
        content: .init(state: state, staleDate: nil),
        pushType: .token
    )
    print("Activity started: (activity.id)")
} catch {
    print("Failed: (error)")
}

pushType: .token means the activity will receive remote push updates. You’ll need to collect the APNS token on your side to push updates to it.

Updates: 3 different paths

1. Local update: while your app is open, you call Activity.update() directly.

await activity.update(using: newState)

Works when the user is in the app. Once the app is suspended, local updates stop.

2. Push notification: send a Live Activity-specific payload through APNS. Apple has a dedicated APNS endpoint: liveactivity-sandbox.push.apple.com.

Payload:

{
    "aps": {
        "timestamp": 1234567890,
        "event": "update",
        "content-state": {
            "duration": 1200,
            "caloriesBurned": 150
        }
    }
}

This is the most powerful path but it takes backend work. APNS p12 cert or token-based auth, the apns-push-type: liveactivity header, and the apns-topic: com.example.app.push-type.liveactivity topic.

3. Background task: periodic updates from the app. BGTaskScheduler or location-based background fetch. Limited rate.

Designing Dynamic Island UI

The Dynamic Island shows up in 3 states:

  • Minimal: just an icon on the left or right. Happens when another Live Activity is also active
  • Compact: short content on both the leading and trailing side
  • Expanded: long-press to reveal more information

Each state needs to be designed separately:

DynamicIsland {
    expandedRegion(.leading) { ... }
    expandedRegion(.trailing) { ... }
    expandedRegion(.center) { ... }
    expandedRegion(.bottom) { ... }
} compactLeading: {
    ...
} compactTrailing: {
    ...
} minimal: {
    ...
}

Compact demands real design discipline: fit the signal into a 15x15pt or at most 20x20pt area. An icon plus 1 or 2 characters usually works. The system handles the animation into and out of the Dynamic Island for you.

Update frequency and rate limits

Push updates are rate-limited. Apple docs aren’t crystal clear, but from production:

  • Don’t push more than once per second
  • If there’s no update for 4 to 8 minutes, the system flags it as stale
  • Push on meaningful state changes, not every atomic tick

In the delivery app, pushing a Live Activity update for every GPS ping (every 5 seconds) was overkill and burned battery. We switched to once every 30 seconds, plus an extra update when distance crossed a meaningful threshold. Battery impact dropped by roughly 80%.

End: closing the activity

When the activity is done, you have to end it explicitly, otherwise it stays on the lock screen until the 8-hour cap.

await activity.end(
    using: finalState,
    dismissalPolicy: .after(.now + 3600) // dismiss 1 hour later
)

dismissalPolicy options:
.default: system decides
.immediate: remove right away
.after(date): at a specific time

In the delivery app we wanted the “Delivered” state to linger for 10 minutes after the order was dropped off, then disappear. .after(.now + 600) did the job.

Error states and fallback

A user can grant or deny Live Activity permission from Settings. You have to check ActivityAuthorizationInfo().areActivitiesEnabled. If permission isn’t there, a fallback UI that points the user to Settings is a good pattern.

Once you’ve registered a token with APNS, Apple may return an invalid token error. The activity might be ended but the token still live on your backend. If you get a 410 Gone on an update attempt, drop the token.

Testing is painful

The simulator doesn’t show Live Activities. Dynamic Island needs an iPhone 14 Pro or newer. To test remote pushes you need an APNS cert, a push payload, a real device, and an active Live Activity all lined up. The test loop is slow.

Xcode 15+ improved Live Activity debug tooling a bit, but there’s no escaping real-device testing.

Impact on user satisfaction

Both projects saw real wins after Live Activities shipped:

  • App open rate went up (users can check status from the lock screen without opening the app)
  • More than that, glancing at the lock screen and confirming status gave them extra confidence
  • App Store reviews started mentioning “Live Activities are super useful”

If your product has a genuine “live status” moment, Live Activities pay back fast. If it doesn’t, don’t force it.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch