Add Live Activities to your iOS app in 4 steps

Enhance user experience and real-time interactivity with Live Activities on iOS

Bruno Lorenzo
5 min readAug 8, 2023

If you haven’t already incorporated Live Activities into your apps, it’s time you consider their powerful potential.

Live activities constitute a dedicated UI that seamlessly appears on both the lock screen and the dynamic island for compatible iPhones. By leveraging this feature, you can maintain essential interactions with your users even when they navigate away from your app’s main scope.

We can send updates or show live information without the need to send them push notifications. And the best part? It’s incredibly easy to add it.

Here is how to do it.

Photo by Lala Azizli on Unsplash

We’ll be implementing live activities for the Coffee Demo App from my previous articles. We want a live activity to show the user the current state and progress of their order once it has been placed.

Step 1 — Define our Live Activity content

The very first step is to define what information we want to show in our Live Activity. Given the limited space available, we need to focus on the most important data for our users.

With that in mind, the next thing we need to do is implement the ActivityAttributes protocol.

public protocol ActivityAttributes : Decodable, Encodable {

/// The associated type that describes the dynamic content of a Live Activity.
///
/// The dynamic data of a Live Activity that's encoded by `ContentState` can't exceed 4KB.
associatedtype ContentState : Decodable, Encodable, Hashable
}

As you can notice, we can divide our information into two categories:

  1. Static → Properties of the model that implement ActivityAttributes.
  2. Dynamic → Stored in the ContentState type.

In our case, we can organize the information as follows:

  1. Static → The order’s number. Once the user places an order, the number won’t change.
  2. Dynamic → The order’s status, indicating the current phase of the order.
struct OrderAttributes: ActivityAttributes {

struct ContentState: Codable, Hashable {
enum OrderStatus: Float, Codable, Hashable {
case inQueue = 0
case aboutToTake
case making
case ready

var description: String {
switch self {
case .inQueue:
return "Your order is in the queue"
case .aboutToTake:
return "We're about to take your order"
case .making:
return "We're preparing your order"
case .ready:
return "Your order is ready to pick up!"
}
}
}

let status: OrderStatus
}

let orderNumber: Int
}

Step 2 — Create the UI

The Live Activity UI lives in the app’s widget scope. So, if we don’t have a widget extension in our app, we first need to create one.

You can create the widget extension just to implement your live activity. It’s not mandatory to create a widget

Create a new Widget view and return an instance of ActivityConfiguration in the widget implementation. We'll need to use the ActivityAttributes model that we created in the previous step.

struct CoffeeShopWidgetLiveActivity: Widget {

var body: some WidgetConfiguration {
ActivityConfiguration(for: OrderAttributes.self) { context in
// Lock screen/banner UI goes here
LiveActivityView(state: context.state)
} dynamicIsland: { context in
// Here goes the dynamic island implementation
// Out of topic for this article
...
}
}
}

Inside LiveActivityView is where we build the Live Activity UI.

For our example, let’s show a progress bar with every phase of the order.

struct LiveActivityView: View { 
let state: OrderAttributes.ContentState

var body: some View {
VStack {
HStack {
Image(systemName: "cup.and.saucer")
ProgressView(value: state.status.rawValue, total: 3)
.tint(.black)
.background(Color.brown)
Image(systemName: "cup.and.saucer.fill")
}
.padding(16)

Text("\(state.status.description)")
.font(.system(size: 18, weight: .semibold))
.padding(.bottom)
Spacer()
}
.background(Color.brown.opacity(0.6))
}
}

Finally, we need to add our Live Activity widget to our widget bundle. In our case, as we won’t be providing any Widgets, we just use the Live Activity.

@main
struct CoffeeShopWidgetBundle: WidgetBundle {
var body: some Widget {
CoffeeShopWidgetLiveActivity()
}
}

Step 3 — Initialize the Live Activity

Interacting with our Live Activity involves using ActivityKit APIs.

First, we must create the initial content for our Live Activity using ActivityContent’s constructor: init(state:staleDate:relevanceScore:)

  • state: Is the initial ActivityAttributes.ContentState for the Live Activity.
  • staleDate: A Date to indicate the OS when the Live Activity will become outdated. If no staleDate is passed, after 8 hours, the OS will end the Live Activity.
  • relevanceScore: If we have more than one Live Activity, relevanceScore will indicate the priority to show on the dynamic island and the order in the lock screen.

Then, we can request the new Live Activity start by calling Activity method request(attributes:content:pushType:)

  • attributes: An instance of our ActivityAttributes that we created in Step 1.
  • content: The initial content for our Live Activity
  • pushType: Indicates if the updates of the Live Activity will be from ActivityKit push notifications. We can pass nil if we only update the Live Activity throw the update function.

📣 Important note: the app must be in the foreground when we request a new Activity.

let orderAttributes = OrderAttributes(orderNumber: 1)    
let initialState = OrderAttributes.ContentState(status: .inQueue)
let content = ActivityContent(state: initialState, staleDate: nil, relevanceScore: 1.0)

do {
let orderActivity = try Activity.request(
attributes: orderAttributes,
content: content,
pushType: nil
)
} catch {
print(error.localizedDescription)
}

Step 4 — Update the Live Activity

Whenever we want to update our Live Activity, we can use update(_:) function. In the same way that we created our initial state, we can create the ActivityContent with the updates.

await orderActivity?.update(
ActivityContent<OrderAttributes.ContentState>(
state: state,
staleDate: nil
)
)

Additionaly, we can pass an AlertConfiguration instance to show an alert with the update:

- iPhones with Dynamic Island 👉 it will be shown in the Dynamic Island region as an expanded Live Activity.

- iPhones without Dynamic Island 👉 it will be shown on the Lock Screen as a banner presentation.

To put an end to the Live Activity, we use end(_:dismissalPolicy:) function. Depending on the dismissalPolicy used, the Live Activity may remain in the lock screen until the user explicitly removes it. That's why we still need to pass the final state of the Live Activity, to keep the UI updated with the latest state.

We have three options for the dismissalPolicity

  1. default The Live Activity will remain in the lock screen for up to four hours (or until the user removes it)
  2. inmediate The OS removes the Live Activity right away
  3. after(_ date:) We can indicate a date to dismiss the Live Activity (must be in a four-hour window from the moment that the Live Activity ends)

Where to go from here?

Consider your app’s use cases carefully. While Live Activities are particularly suited for real-time applications like delivery apps or sports events, they can also serve as an appealing alternative to push notifications, offering users a more engaging experience.

For a richer user experience, you can go a step further and incorporate deep links into your Live Activities, ensuring a seamless user journey when tapping on the Live Activity.

Have any questions? Feel free to drop me a message! 🙂

  • 🤓 Join me on Twitter for regular content on iOS development tips and insights
  • 🚀 Check out my GitHub where I share all my example projects

--

--

Bruno Lorenzo
Bruno Lorenzo

Written by Bruno Lorenzo

Software Engineer | Innovation Manager at www.houlak.com | Former iOS Tech Lead | I write about iOS, tech, and producitivy