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
NSSupportsLiveActivitieskey to your main app'sInfo.plistfile and set its boolean value toYES. - 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
ActivityKitinto 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.