How to Implement ActivityKit Live Activities in iOS 16

Apple's iOS 16 introduced Live Activities powered by the ActivityKit framework, enabling apps to display real-time, glanceable updates directly on the Lock Screen and in the Dynamic Island. Implementing this feature requires developers to define shared data models, build dynamic user interfaces using SwiftUI and WidgetKit, and control the activity lifecycle through Swift code or remote push notifications. The following guide outlines the core steps third-party developers must take to integrate ActivityKit into their iOS applications.

1. Configure the Project Prerequisites

Before writing code, ensure your project meets the deployment requirements:

  • Set the iOS Deployment Target to iOS 16.1 or later, as the public ActivityKit API was officially released to developers in iOS 16.1.
  • Add the NSSupportsLiveActivities key to your main app's Info.plist file and set its boolean value to YES.
  • Add a Widget Extension to your Xcode project if your app does not already have one. Ensure the "Include Live Activity" checkbox is selected, or manually import ActivityKit into your extension.

2. Define the Activity Attributes

Live Activities rely on the ActivityAttributes protocol to define both the static data (unchanging information) and dynamic data (frequently updated information) of an activity.

Create a shared Swift file accessible by both your main application target and your Widget extension:

import ActivityKit
import Foundation

struct DeliveryActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic state that updates over time
        var estimatedDeliveryTime: Date
        var status: String
    }

    // Static properties set when the activity starts
    var orderNumber: String
    var customerName: String
}

3. Build the UI in WidgetKit

Live Activities render their UI using WidgetKit and SwiftUI. You must configure the presentation for both the Lock Screen banner and the Dynamic Island.

In your Widget extension, declare an ActivityConfiguration:

import WidgetKit
import SwiftUI

struct DeliveryActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in
            // Lock Screen presentation
            VStack(alignment: .leading) {
                Text("Order #\(context.attributes.orderNumber)")
                    .font(.headline)
                Text(context.state.status)
                    .font(.subheadline)
            }
            .padding()
        } dynamicIsland: { context in
            // Dynamic Island presentations
            DynamicIsland {
                // Expanded UI (long press)
                DynamicIslandExpandedRegion(.leading) {
                    Text("Order #\(context.attributes.orderNumber)")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text(context.state.status)
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("ETA: \(context.state.estimatedDeliveryTime, style: .time)")
                }
            } compactLeading: {
                Image(systemName: "box.truck")
            } compactTrailing: {
                Text(context.state.status)
            } minimal: {
                Image(systemName: "box.truck")
            }
        }
    }
}

4. Manage the Activity Lifecycle in the Main App

Your main application initiates, updates, and dismisses Live Activities using the Activity class.

Verify Support

Ensure the user has enabled Live Activities before attempting to start one:

guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }

Start the Activity

Provide the initial attributes and content state:

let attributes = DeliveryActivityAttributes(orderNumber: "12345", customerName: "Jane Doe")
let initialState = DeliveryActivityAttributes.ContentState(
    estimatedDeliveryTime: Date().addingTimeInterval(1800),
    status: "Preparing"
)

do {
    let activity = try Activity<DeliveryActivityAttributes>.request(
        attributes: attributes,
        contentState: initialState,
        pushType: nil // Set to .token to receive remote APNs updates
    )
} catch {
    print("Error starting activity: \(error.localizedDescription)")
}

Update the Activity

Update the display as events progress in your app:

let updatedState = DeliveryActivityAttributes.ContentState(
    estimatedDeliveryTime: Date().addingTimeInterval(900),
    status: "Out for delivery"
)

Task {
    await activity.update(using: updatedState)
}

End the Activity

Terminate the activity when the event finishes, choosing an appropriate dismissal policy:

let finalState = DeliveryActivityAttributes.ContentState(
    estimatedDeliveryTime: Date(),
    status: "Delivered"
)

Task {
    await activity.end(using: finalState, dismissalPolicy: .default)
}

5. Support Remote Updates via APNs

If your app tracks events that occur on a server (such as food delivery or ride-sharing), request an APNs push token during activity creation by setting pushType: .token. Observe activity.pushTokenUpdates to transmit the device token to your backend. Your server can then send payload-driven APNs alerts directly to ActivityKit, updating or ending the Live Activity without requiring the user to open your application.