This article has been updated for iOS 16.1 and Xcode 14.1.
Live Activities are a new feature introduced in iOS 16 which allows users to track live events from their lock screens. You can think of them as notifications but with a much more appealing user interface. This will enable developers to enhance their notification experience with crafted and useful UI instead of relying on simple text.
While they will not be available when iOS 16 launches this autumn, we can already start creating our Live Activities.
Creating your first Live Activity for the Lock Screen
Live Activities use WidgetKit and SwiftUI to render their user interface, so the first step is to create a new widget extension if your app doesn’t already have one. Then in your app’s Info.plist
, add a new entry with the key NSSupportsLiveActivities
and the Boolean
value YES
. This will enable the Live Activity feature for your app and allow users to opt out from your app’s settings.
While Live Activities use WidgetKit for their interface, they are not powered by the timeline mechanism behind widgets. Instead, to manage the lifecycle of a Live Activity, we use ActivityKit.
ActivityKit is available starting from Xcode 14.1 beta.
Introducing ActivityKit
Unlike widgets, Live Activities run in a sandbox, which means they cannot access the network or receive location updates. They can be updated locally from your app or through push notifications.
To display information in our Live Activity, we need to provide it with some data. We do that by defining its ActivityAttributes
.
ActivityAttributes
is a protocol defined by ActivityKit that informs the system about the static data we intend to display. Here we can store information that is not supposed to change throughout the lifetime of the Live Activity. For example, we can keep track of the number of items the customer orders.
import ActivityKit
/// The attributes of a Delivery Live Activity.
struct DeliveryAttributes: ActivityAttributes {
/// The number of items in the order.
let numberOfItems: Int
}
The main requirement of the ActivityAttributes
protocol is the associated type ContentState
, which conforms to Codable
and Hashable
. This type will contain information that can be updated from the app or push notifications.
extension DeliveryAttributes {
typealias ContentState = State
/// The state of the Delivery activity.
struct State: Codable, Hashable {
/// The current step of the delivery.
let currentStep: DeliveryStep
/// The ETA of the groceries delivery.
let estimatedTimeOfArrival: Date
}
}
Now that we have the attributes, we can create our widget using the ActivityConfiguration
type. This object provides a closure where we can create the view of the widget. We are also given an ActivityViewContext
which provides the view with the attributes and current state of our Live Activity.
@main
struct DeliveryWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
LockScreenDeliveryView(
numberOfItems: context.attributes.numberOfItems,
currentStep: context.state.currentStep,
estimatedTimeOfArrival: context.state.estimatedTimeOfArrival
)
} dynamicIsland: { context in
...
}
}
}
Although the system does not impose a size, a Live Activity could be truncated if its height exceeds 160 pixels, so keep this in mind when designing your widget.
Managing the lifecycle of a Live Activity
Since an app can run multiple Live Activities at once, we need a simple way of managing them. The Activity
object allows us to request and start a new Live Activity and update or end the ones already running.
To start a Live Activity, we use the request(attributes:contentState:pushType:)
method on the Activity
object and provide an initial state. The value of the pushType
parameter defaults to .token
, which allows the Live Activity to be updated through push notifications. Since we will update it from our app for this example, we will set it to nil
.
Once started, a Live Activity can be active for up to 8 hours, then it will be ended by the system.
/// Starts a new grocery delivery Live Activity.
func startGroceryDeliveryTracking() {
let initialAttributes = DeliveryAttributes(numberOfItems: numberOfItems)
let initialState = DeliveryAttributes.State(
currentStep: .preparing,
estimatedTimeOfArrival: estimatedTimeOfArrival
)
do {
activity = try Activity<DeliveryAttributes>.request(
attributes: initialAttributes,
contentState: initialState,
pushType: nil
)
} catch {
print("Failed to request a new live activity: \(error.localizedDescription)")
}
}
When requesting it, we get the started Live Activity in return. Otherwise, we can retrieve an activity from the activities
property on Activity
.
To update a running Live Activity, we use the update(using:)
method, which requires a new ContentState
object that represents the updated state of the activity.
/// Updates the currently running Live Activity.
func updateGroceryDeliveryTracking() async {
let updatedState = DeliveryAttributes.State(
currentStep: .delivering,
estimatedTimeOfArrival: activity.contentState.estimatedTimeOfArrival
)
await activity.update(using: updatedState)
}
Similarly, we can end the activity using the end(using:dismissalPolicy:)
method, which allows us to update it for one last time.
We can also specify when it will be removed from the users’ lock screens by setting the dismissalPolicy
. The default
policy will allow the activity to be glanceable for some time but will be removed from the system after 4 hours. We can remove the activity immediately or after some time with the immediate
and after(_:)
policies.
/// Ends the currently running live activity.
func endGroceryDeliveryTracking() async {
let updatedState = DeliveryAttributes.State(
currentStep: .delivered,
estimatedTimeOfArrival: activity.contentState.estimatedTimeOfArrival
)
await activity.end(using: updatedState, dismissalPolicy: .default)
}
Regardless, users can choose to remove a Live Activity at any time, just like any other notification.
Conclusions
Live Activities are a powerful way to create engaging notification experiences, and I can't wait to see how apps implement their own.
Thanks for reading! 🚀