An introduction to Live Activities

Helder Pinhal
Helder Pinhal
Nov 11 2022
Posted in Engineering & Technology

Getting to know how a Live Activity works

An introduction to Live Activities

A couple of weeks ago, Apple released iOS 16.1, making Live Activities a reality for every developer. Consider adopting this new functionality for some of your communications, as most users will expect that as they become more familiar with it.

What is a Live Activity?

After all, what is a Live Activity in practice? Apple couldn't have said it better.

Live Activities display your app's most current data on the iPhone Lock Screen and in the Dynamic Island. This allows people to see live information at a glance.

Benefits of using Live Activities

Being able to show rich, more prominent content on a user's lock screen may be reason enough to leverage the power of Live Activities. However, there's more.

Consider the scenario where an application lets a user purchase a product. It's relatively typical to inform the user about any changes in the state of the order. Many apps choose to send transactional emails, but many others do so via remote notifications. The latter may be a quicker way to deliver the information but can cause an overflow of notifications in the user's lock screen when the time interval is short. Using a Live Activity for this purpose minimises the content, preventing stale content from being shown and focusing the user's attention on what's most important at that time.

Heads-up of some constraints

Although Live Activities don't have many constraints, the few it has, will heavily influence what you can build.

1. The Timeout!

A Live Activity can be active for up to 8 hours unless your app or the user stops it. After this time, the OS stops it automatically. When a Live Activity ends, it will be immediately removed from the Dynamic Island. However, the Live Activity remains on the Lock Screen until the user removes it or for up to 4 additional hours when the OS removes it automatically.

2. The Design

There are several recommendations and best practices regarding the visual specifications of a Live Activity. Although this article focuses on the Lock Screen Live Activity, there are many formats, each with its guidelines.

Regarding high-impact design concerns, the maximum height of a Live Activity in the Lock Screen is the most relevant - 160 points.

You can check out Apple's documentation for a detailed guide on the visual guidelines.

3. Networking

Each Live Activity runs in a dedicated sandbox. Unlike Widgets, it cannot access the network. A simple way to run into this constraint is to try loading a remote image you want to display in the Live Activity. There are ways to work around this caveat. We can create an Access Group shared by the Application and Widgets target, fetch the images before starting the Live Activity and store the file locally with the shared Access Group.

Another critical remark is the amount of data you can pass to a Live Activity. It cannot exceed 4KB. The same applies to the contents of remote notifications used to update the Live Activity.

Getting started

There are a few requirements to get started with Live Activities. It's convenient if you're a developer 😉, have Xcode 14.1 installed and have a push-capable iOS simulator or an iPhone.

Building the UI (basic version)

Leveraging SwiftUI's rendering engine, building the UI couldn't be more straightforward. First and foremost, create a Widget Extension if you don't already have one.

Push notification on the iOS simulator

The following code can quickly achieve the view above — nothing too fancy here.

@available(iOSApplicationExtension 16.1, *)
struct OrderStateView: View {
    let attributes: OrderActivityAttributes
    let contentState: OrderActivityAttributes.ContentState

    private var title: String {
        switch contentState.state {
        case .preparing: return "Order in progress"
        case .shipped: return "On it's way!"
        case .delivered: return "Arrived!"
        }
    }

    private var message: String {
        switch contentState.state {
        case .preparing: return "Preparing your goodies"
        case .shipped: return "At the speed of light"
        case .delivered: return "On your doorstep"
        }
    }

    var body: some View {
        VStack(alignment: .leading) {
            HStack(alignment: .center) {
                Image("artwork_logo_badge")
                    .resizable()
                    .scaledToFit()
                    .frame(height: 32)

                VStack(alignment: .leading) {
                    HStack(alignment: .firstTextBaseline) {
                        Text(title)
                            .font(.headline)
                            .lineLimit(1)

                        Spacer()

                        Text("#\(String(attributes.orderNumber))")
                            .font(.subheadline)
                    }

                    Text(message)
                        .font(.footnote)
                        .lineLimit(1)
                }
            }
        }
        .padding()
    }
}

We created a view for the desired UI, but iOS needs to know the view is supposed to represent a widget. There are two parts to this, creating the widget itself and exposing it in the widgets' configuration block.

@main
struct NotificareWidgets: WidgetBundle {
    var body: some Widget {
        if #available(iOS 16.1, *) {
            OrderStatusActivityWidget()
        }
    }
}

@available(iOSApplicationExtension 16.1, *)
struct OrderStatusActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: OrderActivityAttributes.self) { context in
            OrderStateView(attributes: context.attributes, contentState: context.state)
        } dynamicIsland: { context in
            // Stay tuned for our next article!
        }
    }
}

Starting the Live Activity

You may have noticed a reference to OrderActivityAttributes in the previous step. This represents the data structure for the Live Activity. The structure below should be added to your app's sources and made available to both targets — app and widgets extension.

import ActivityKit

public struct OrderActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var state: OrderState
    }

    let orderNumber: Int

    public enum OrderState: String, Codable {
        case preparing
        case shipped
        case delivered
    }
}

The object itself contains immutable properties describing your Live Activity. The ContentState represents the mutable properties you want to update via the app or remote notifications.

At this point, we have everything we need to start the Live Activity.

do {
    let attributes = OrderActivityAttributes(orderNumber: 46247)
    let contentState = OrderActivityAttributes.ContentState(state: .preparing)

    let activity = try Activity.request(attributes: attributes, contentState: contentState)
    print("Requested a Live Activity \(activity.id).")
} catch (let error) {
    print("Error requesting Live Activity \(error.localizedDescription).")
}

Acquiring a push token

For cases where you want to update a Live Activity based on an action executed outside your app, you can request a push token for that specific Live Activity. You must register the token in your backend and send a particular kind of notification via APNS. To acquire the push token, amend the example above with the following instead.

do {
    let attributes = OrderActivityAttributes(orderNumber: 46247)
    let contentState = OrderActivityAttributes.ContentState(state: .preparing)

    let activity = try Activity.request(attributes: attributes, contentState: contentState, pushType: .token)
    print("Requested a Live Activity \(activity.id).")

    if let data = activity.pushToken {
        let token = data.toHexString()
        // TODO: send to the backend.
    }
} catch (let error) {
    print("Error requesting Live Activity \(error.localizedDescription).")
}

Note that your app must already be push-capable and that the user granted the necessary permission.

Sending updates via push messages

You must make a few adjustments to your routine for sending APNS remote notifications to target a Live Activity.

  1. Set the value of the apns-push-type header to liveactivity.
  2. Set the apns-topic header to <your-bundle-id>.push-type.liveactivity.
  3. Send the body in the format below.
{
    "aps": {
        "timestamp": 1168364460,
        "event": "update",
        "content-state": {
            "state": "shipped"
        }
    }
}

Without going into too much detail, there are more customisation possibilities. You can find the complete guide to remote notifications here.

Stopping the Live Activity

It's essential to keep in mind that users are capable of stopping any Live Activity should they choose to do so.

You can also stop the Live Activity through the app or with a remote notification.

To stop the Live Activity via the app, you must keep track of the Activity object and call end().

Lastly, to end the activity via a remote notification, you must send a similar content as the update, but instead you will set event to end. Be aware that you should send the latest content-state to ensure the OS renders the newest content before ending the activity.

Wrapping up

Live Activities hold much promise for the future, and we'll begin to witness what we can achieve with it.

In this article, we touched on the foundations of Live Activities. We plan on writing a series of articles detailing the build process of several use cases leveraging the combined power of Notificare and Live Activities. Stay tuned!

As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news