Getting Started with Live Activities

Updated September 15, 2022

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 Activity

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! 🚀