Creating Live Activities on iOS 16 iOS 03.11.2023

Starting from iOS 16.1 Apple provides a framework called ActivityKit which allows to create lock screen live widgets. One of the biggest problems with home screen widgets is that they do not show actual real-time updates. However, with Live Activities, you can provide real-time updates on the lock screen. This is helpful for apps that need to show time-critical information, like an ongoing workout activity, progress tracking in a food delivery app, or the score of a live-action game.

ActivityKit and Live Activities are kinda similar to WidgetKit and is used to present dynamic information to the user via widgets on the lock screen and Dynamic Island. ActivityKit is availabel on iOS straring since 16.1 version. Actually, it's better to start from iOS 16.2 because Apple changed some vital methods between 16.1 and 16.2. Some methods introduced in 16.1 is now deprecated in 16.2.

Let's familiarize with some technical restrictions

  • Live Activities are only available on iPhone.
  • Live Activities use SwiftUI for their user interface on the Lock Screen.
  • Live Activities can be active for up to 8 hours unless your app or the user explicitly stops it. After 8 hours the system automatically ends a Live Activities. In this ended state, the Live Activities remains on the Lock Screen for up to 4 hours before the system kills it. As a result, a Live Activities remains on the Lock Screen for a maximum of 12 hours.
  • It's impossible to access network or location updates in Live Activities.
  • To update a Live Activities you can use ActivityKit while the app is running or Remote Push Notifications.

To add Live Activities to the existed project start with adding the widget extension by selecting the File > New > Target... menu option. Next select the Widget Extension option. On the following screen, enter desired name. Before clicking the Finish button, ensure the Include Live Activity option is selected. When prompted, click the Activate button to ensure the widget extension is included in the project.

Also we must enabled Live Activities support in the Info tab. Select your app's target at the top of the Project Navigator panel. On the Info screen, locate the bottom entry in the list of properties and hover the mouse pointer over the item. When the plus button appears, click it to add a new entry to the list. From within the drop-down list of available keys, locate and select the Supports Live Activities option or paste NSSupportsLiveActivities value.

Also you can manually add following key to the Info.plist file

<key>NSSupportsLiveActivities</key>
<true/>

Let's break totorial in following steps

  • Data source
  • View
  • Start the Live Activity
  • Update the Live Activity
  • Stop the Live Activity

Data source

The Live Activity attributes declare the data structure to be presented and are created using ActivityKit’s ActivityAttributes class. The attributes have two properties. One property remains static and does not change throughout one Live Activity. The other property is dynamic and may constantly change throughout the Live Activity.

Within the ActivityAttributes declaration, the dynamic attributes are embedded in a ContentState structure using the following syntax.

import ActivityKit
import SwiftUI

// Data

struct Quotation: Codable, Hashable {
    let quiotation: String
    let author: String

    static func preview() -> [Quotation] {
        return [
            Quotation(quiotation: "Do one thing every day that scares you.", author: "Eleanor Roosevelt"),
            Quotation(quiotation: "If you can dream it, you can do it.", author: "Walt Disney"),
            Quotation(quiotation: "Success is not final, failure is not fatal: it is the courage to continue that counts.", author: "Winston Churchill"),
            Quotation(quiotation: "The pessimist complains about the wind. The optimist expects it to change. The leader adjusts the sails.", author: "John Maxwell"),
            Quotation(quiotation: "It is amazing what you can accomplish if you do not care who gets the credit.", author: " Harry Truman")
        ]
    }

    static func random() -> Quotation {
        preview().randomElement()!
    }
}

// Attributes

struct QuotationAttributes: ActivityAttributes {
    // Dynamic values
    public struct ContentState: Codable, Hashable {
        var quotation: Quotation
    }

    // Static values
    var quotationName: String
}

View

Live Activities present data on lock screen or Dynamic Island. These views are created using SwiftUI views. The lock screen view consists of a single layout, the Dynamic Island views are separated into areas.

import WidgetKit
import SwiftUI
import ActivityKit

struct QuotationWidgetView : View {
    let context: ActivityViewContext<QuotationAttributes>

    var body: some View {
        VStack(alignment: .leading) {
            Text(context.state.quotation.quiotation)
            HStack {
                Spacer()
                Text(context.state.quotation.author).font(.caption)
            }
        }
        .padding()
        .activityBackgroundTint(Color.cyan)
        .activitySystemActionForegroundColor(Color.black)
    }
}

struct QuotationWidget: Widget {
    let kind: String = "QuotationWidget"

    var body: some WidgetConfiguration {
        ActivityConfiguration(for: QuotationAttributes.self) { context in
            QuotationWidgetView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T")
            } minimal: {
                Text("M")
            }
        }
    }
}

Each element has a context object where static and dynamic data values can be accessed for inclusion in the views.

The Live Activity will display data using compact layouts on devices with a Dynamic Island. However, a long press performed on the island will display the expanded widget. Unlike the lock screen widget, the expanded Dynamic Island presentation is divided into four regions.

Start the Live Activity

Once the data model has been defined and the presentations designed, the next step is to request and start the Live Activity. We can do it by calling the Activity.request() method. We have to pass two params an activity attributes instance and a push type. The push type should be set to token if the data updates will be received via push notifications or nil if updates are coming from the app.

An optional stale date may also be included. It is a date to indicate the iOS when the Live Activity will become outdated. If no staleDate is passed, after 8 hours, the iOS will end the Live Activity. To check if the Live Activity is out of date, check the isStale property.

Let's start with checking of Live Activity availability

guard ActivityAuthorizationInfo().areActivitiesEnabled else {
    print("Activities are not enabled")
    return
}

Next step is to create an activity attributes object and initialize any static properties, for example:

let attributes = QuotationAttributes(quotationName: "Motivation")

The second requirement is a ContentState instance configured with initial dynamic values

let quotation = Quotation.random()
let state = QuotationAttributes.ContentState(quotation: quotation)

activity = try? Activity<QuotationAttributes>.request(
    attributes: attributes, 
    content: .init(state: state, staleDate: nil), 
    pushType: nil
)

If the request is successful, the Live Activity will launch and be ready to receive updates.

Update the Live Activity

There three ways to update Live Activity with new data

  • From the master app
  • Check staleDate and run AppIntent
  • Remote push notification

To update a Live Activity from the app you need to call the update() method of the activity instance returned by the Activity.request() method. The update call must be passed an ActivityContent instance containing a ContentState object with new dynamic data values and an optional stale date value.

let quotation = Quotation.random()
let state = QuotationAttributes.ContentState(quotation: quotation)

Task {
    await activity?.update(.init(state: state, staleDate: nil, relevanceScore: 0))
}

The iOS will display the Live Activity with the highest relevanceScore.

Stop the Live Activity

Live Activity is stopped by calling the end() method of the activity instance. You need to pass a ContentState object with the final data values and a dismissal policy setting.

let quotation = Quotation.random()
let state = QuotationAttributes.ContentState(quotation: quotation)

Task {
    await activity?.end(.init(state: state, staleDate: nil), dismissalPolicy: .immediate)
}

When the dismissalPolicy is set to default, the Live Activity widget will remain on the lock screen up to 12 hours unless the user removes it. Use immediate to instantly remove the Live Activity from the lock screen.

You can check code on Github.