Implementing ActivityKit for Live Activities in iOS 16

This guide provides a step-by-step walkthrough for developers looking to integrate ActivityKit into iOS 16 applications to power Live Activities. Live Activities allow apps to display real-time, glanceable updates on the iPhone Lock Screen and within the Dynamic Island. Below, you will learn how to configure your project, define data models using ActivityAttributes, design the presentation interface with WidgetKit, and control the activity lifecycle through Swift code.

1. Configure the Project

Before writing Swift code, enable Live Activities in your app's configuration:

  1. Open your main app target's Info.plist file.
  2. Add the key NSSupportsLiveActivities (Supports Live Activities) and set its Boolean value to YES.
  3. If your project does not already have one, add a Widget Extension target via File > New > Target > Widget Extension, ensuring the Include Live Activity checkbox is selected.

2. Define the Activity Attributes

ActivityKit uses the ActivityAttributes protocol to structure both static and dynamic data. Static data remains constant throughout the activity, while dynamic data updates periodically.

import ActivityKit
import Foundation

struct DeliveryAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic data that updates frequently
        var status: String
        var estimatedDeliveryTime: Date
    }

    // Static data defined when the activity starts
    var orderNumber: String
}

3. Build the User Interface with WidgetKit

Live Activities rely on SwiftUI and WidgetKit for rendering. You define presentations for both the Lock Screen banner and the Dynamic Island (expanded, compact leading/trailing, and minimal views).

import WidgetKit
import SwiftUI

struct DeliveryActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            // Lock Screen and banner UI
            VStack(alignment: .leading) {
                Text("Order #\(context.attributes.orderNumber)")
                    .font(.headline)
                Text(context.state.status)
                    .font(.subheadline)
            }
            .padding()
        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded UI
                DynamicIslandExpandedRegion(.leading) {
                    Text("Order #\(context.attributes.orderNumber)")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text(context.state.status)
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Arriving: \(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

You manage Live Activities within your main app using the Activity<Attributes> API to request, update, and end sessions.

Starting an Activity

Ensure the user has enabled Live Activities using ActivityAuthorizationInfo().areActivitiesEnabled, then request a new session:

func startDeliveryActivity() {
    guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }

    let attributes = DeliveryAttributes(orderNumber: "12345")
    let initialContentState = DeliveryAttributes.ContentState(
        status: "Preparing Order",
        estimatedDeliveryTime: Date().addingTimeInterval(1800)
    )

    do {
        _ = try Activity.request(
            attributes: attributes,
            contentState: initialContentState,
            pushType: nil // Use .token for APNs remote updates
        )
    } catch {
        print("Failed to start activity: \(error.localizedDescription)")
    }
}

Updating an Activity

To update the dynamic data, provide a new ContentState to the running activity instance:

func updateDeliveryActivity(activity: Activity<DeliveryAttributes>, newStatus: String) {
    Task {
        let updatedContentState = DeliveryAttributes.ContentState(
            status: newStatus,
            estimatedDeliveryTime: Date().addingTimeInterval(900)
        )
        await activity.update(using: updatedContentState)
    }
}

Ending an Activity

When the task is complete, end the activity and specify a dismissal policy:

func endDeliveryActivity(activity: Activity<DeliveryAttributes>) {
    Task {
        let finalContentState = DeliveryAttributes.ContentState(
            status: "Delivered",
            estimatedDeliveryTime: Date()
        )
        // .default keeps the view visible for up to 4 hours; .immediate removes it instantly
        await activity.end(using: finalContentState, dismissalPolicy: .default)
    }
}

5. Remote Updates via Push Notifications

For background updates initiated from your server, set pushType: .token when calling Activity.request. Observe the activity.pushTokenUpdates asynchronous sequence in your app, send the received push token to your backend server, and use Apple Push Notification service (APNs) payloads containing the updated ContentState dictionary to refresh the interface remotely.