Create Live Activities and Dynamic Island With ActivityKit on iOS 16

Get real-time updates on your Lock Screen and in the Dynamic Island

Batikan Sosun
Better Programming

--

API support iOS 16.1+ Beta iPadOS 16.1+ Beta Mac Catalyst 16.1+ BetaThe final code has been updated for the iPhone 14 series that supports Dynamic Island.

If you are a strict developer who’s been following Apple’s innovation, you must have heard about the “Displaying live data on the Lock Screen and in the Dynamic Island with Live Activities” recently.

Apple announced it a few weeks ago, and then I heard the news about it.

Apple provides it with a new framework called ActivityKit. With the ActivityKit, you can share live updates from your app as Live Activities on the iPhone Lock Screen and in the Dynamic Island.

Live Activities use WidgetKit functionality and SwiftUI for their user interface on the Lock Screen.

ActivityKit’s role is to handle the life cycle of each Live Activity: You use its API to request, update, and end a Live Activity. — Apple

Let’s imagine a sports app. This app user might want to track the soccer matches goals and other statistics: passes, shots, etc., on the iPhone Lock Screen.

Another example could be a food delivery app. You know that pizza consumers might be impatient users. Without opening the app, app users can follow the pizza courier on the iPhone lock screen. Another one could be a groceries app.

That’s enough for the examples. OK, let’s get the Live Activities coding started.

How to Code a Live Activities App

Photo by James Yarema on Unsplash

We have to create a Widget to configure ActivityKit. It’s similar to Widget Extensions.

We will use SwiftUI and WidgetKit to create the user interface of the Live Activity. Live Activities works like Widget Extension and enables code sharing between your widgets and Live Activities.

However, Live Activities use a different mechanism to receive updates compared to widgets.

Instead of using a timeline mechanism, Live Activities receive updated data from your app with ActivityKit or by receiving remote push notifications. — Apple

Important Notes

Live Activities are only available on iPhone.

Live Activities and ActivityKit won’t be included in the initial publicly released version of iOS 16 but will be publicly available in an update later this year(planned with iOS 16.1). Once they’re publicly available, you can submit your apps with Live Activities to the App Store.

I am going to create a delivery app for groceries. And then, I am going to create an ActivityKit Widget for Live Activities of the delivery progress.

If you already offer widgets in your app, you can add user interface code for the Live Activity to your existing widget extension and may be able to reuse code between your widgets and Live Activities.

Don’t forget that, although Live Activities leverage WidgetKit’s functionality, they aren’t widgets. Live Activities are not using a timeline mechanism to provide the user interface.

Creating the App

Photo by Yura Fresh on Unsplash

I will allow users to start Live Activities through a button for tracking delivery progress on the Lock Screen and in the Dynamic Island.

  • Follow the navigation to create a single app with SwiftUI, and then name it GroceryDeliveryApp.

Xcode -> File -> New -> Project -> App

  • Add the following key to your app’s info.plist and set to YES.

NSSupportsLiveActivities

This key will support the app for Live Activities.

  • After configuring the app, we have to create a Widget Extension.

Follow the navigation to create a Widget Extension, and then name it DeliveryTrackWidget.

Project Navigator -> Select the project -> Add a target from target list -> Widget Extension

  • Add the following key to the extension info.plist and set it to YES.

NSSupportsLiveActivities

This key will support the extension for Live Activities.

Once you have created your app and your Widget Extension, you can design it as you wish.

First of all, we need to create an ActivityAttributes.

We use ActivityAttributes to identify a Live Activity. And, then we use ContentState to specify the dynamic content. I created an ActivityAttributes struct with the following code:

import SwiftUI
import ActivityKit

struct GroceryDeliveryAppAttributes: ActivityAttributes {
public typealias LiveDeliveryData = ContentState

public struct ContentState: Codable, Hashable {
var courierName: String
var deliveryTime: Date
}
var numberOfGroceyItems: Int
}

And then, we need to create a Live Activity request with the Activity class. The Activity class takes a generic type for the ActivityAttributes struct.
Note that if you want to update or end this activity, you must pass the pushType value here.

The pushType token value represents updating your activity on a particular iOS device through Apple Push Service (APS) system. Your application will receive a push token following the activity request to set up the APS protocol, and then you’ll be able to send activity update payloads through APS.

Note that the updated dynamic data for both ActivityKit updates and remote push notification updates can not exceed 4KB in size.

I created an ActivityAttributes struct with the following code:

let attributes = GroceryDeliveryAppAttributes(numberOfGroceyItems: 12)
let contentState = GroceryDeliveryAppAttributes.LiveDeliveryData(courierName: "Mike", deliveryTime: .now + 120)
do {
let _ = try Activity<GroceryDeliveryAppAttributes>.request(
attributes: attributes,
contentState: contentState,
pushType: nil)
} catch (let error) {
print(error.localizedDescription)
}

Make sure Live Activities are available. Do not forget that users can choose to deactivate Live Activities for an app in the Settings app on iPhone.

If you want to detect that Live Activities are available and the user allowed your app to use Live Activities, you can do this with areActivitiesEnabled and activityEnablementUpdates.

Furthermore, to observe the state of an ongoing Live Activity, you can use activityStateUpdates.

Once you create a Live Activity, you can update or end it through ContentState.

I updated and ended a Live Activity with the following code:

let updatedStatus = GroceryDeliveryAppAttributes.LiveDeliveryData(courierName: "Adam",
deliveryTime: .now + 150)
await activity.update(using: updatedStatus)
await activity.end(dismissalPolicy: .immediate)

You can use .after(_ date: Date) instead of .immediate to end a Live Activity later.

Every activity has a pushTokenUpdates to update its content by remote push notification. You can use pushToken data to update the content of a Live Activity. In this case, your app must be registered with APNS for remote push notifications. If you’re new to remote push notifications, read the documentation for the User Notifications framework.

The remote push notification payload should match your dynamic data attributes part. An example push notification payload should look like the JSON below:


{
"aps": {
"timestamp": 1660435557,
"event": "update",
"content-state": {
"courierName": "Adam",
"deliveryTime": 1660435557
}
}
}

Live Activity requirements and constraints

A Live Activity can be active for up to eight hours unless your app or the user ends it. After this limit, the system automatically ends it. When a Live Activity ends, the system immediately removes it from the Dynamic Island. However, the Live Activity remains on the Lock Screen until the user removes it or for up to four additional hours before the system removes it.

As a result, a Live Activity remains on the Lock Screen for a maximum of twelve hours.

Live Activities is suitable for the Lock Screen and the Dynamic Island. The Lock Screen view appears on all devices which upgraded to iOS 16.1 and above.

Devices that support the Dynamic Island display Live Activities using the following views: A compact leading view,
A compact trailing view,
A minimal view,
An expanded view

The expanded view appears when a person touches and holds a compact or minimal view in the Dynamic Island and when a Live Activity updates. On an unlocked device that doesn’t support the Dynamic Island, the expanded view appears as a banner for Live Activity updates.
To make sure the system can display your Live Activity in each position, you must support all views.

When we update a Live Activity, ActivityConfiguration returns ActivityAttributes to update the Widget user interface. ActivityConfiguration provides a context to access dynamic data(ContentState) and static data of the ActivityAttributes.

I created the user interface of the Live Activity with the code below:

//
// DeliveryTrackWidget.swift
// DeliveryTrackWidget
//
// Created by Batikan Sosun on 13.08.2022.
//

import ActivityKit
import WidgetKit
import SwiftUI

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

@available(iOSApplicationExtension 16.1, *)
struct GroceryDeliveryApp: Widget {

var body: some WidgetConfiguration {
ActivityConfiguration(for: GroceryDeliveryAppAttributes.self) { context in
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
dynamicIslandExpandedLeadingView(context: context)
}

DynamicIslandExpandedRegion(.trailing) {
dynamicIslandExpandedTrailingView(context: context)
}

DynamicIslandExpandedRegion(.center) {
dynamicIslandExpandedCenterView(context: context)
}

DynamicIslandExpandedRegion(.bottom) {
dynamicIslandExpandedBottomView(context: context)
}

} compactLeading: {
compactLeadingView(context: context)
} compactTrailing: {
compactTrailingView(context: context)
} minimal: {
minimalView(context: context)
}
.keylineTint(.cyan)
}
}


//MARK: Expanded Views
func dynamicIslandExpandedLeadingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
VStack {
Label {
Text("\(context.attributes.numberOfGroceyItems)")
.font(.title2)
} icon: {
Image("grocery")
.foregroundColor(.green)
}
Text("items")
.font(.title2)
}
}

func dynamicIslandExpandedTrailingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
Label {
Text(context.state.deliveryTime, style: .timer)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
} icon: {
Image(systemName: "timer")
.foregroundColor(.green)
}
.font(.title2)
}

func dynamicIslandExpandedBottomView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
let url = URL(string: "LiveActivities://?CourierNumber=87987")
return Link(destination: url!) {
Label("Call courier", systemImage: "phone")
}.foregroundColor(.green)
}

func dynamicIslandExpandedCenterView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
Text("\(context.state.courierName) is on the way!")
.lineLimit(1)
.font(.caption)
}


//MARK: Compact Views
func compactLeadingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
VStack {
Label {
Text("\(context.attributes.numberOfGroceyItems) items")
} icon: {
Image("grocery")
.foregroundColor(.green)
}
.font(.caption2)
}
}

func compactTrailingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
Text(context.state.deliveryTime, style: .timer)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
}

func minimalView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
VStack(alignment: .center) {
Image(systemName: "timer")
Text(context.state.deliveryTime, style: .timer)
.multilineTextAlignment(.center)
.monospacedDigit()
.font(.caption2)
}
}
}





@available(iOSApplicationExtension 16.1, *)
struct LockScreenView: View {
var context: ActivityViewContext<GroceryDeliveryAppAttributes>
var body: some View {
VStack(alignment: .leading) {
HStack {
VStack(alignment: .center) {
Text(context.state.courierName + " is on the way!").font(.headline)
Text("You ordered \(context.attributes.numberOfGroceyItems) grocery items.")
.font(.subheadline)
BottomLineView(time: context.state.deliveryTime)
}
}
}.padding(15)
}
}

struct BottomLineView: View {
var time: Date
var body: some View {
HStack {
Divider().frame(width: 50,
height: 10)
.overlay(.gray).cornerRadius(5)
Image("delivery")
VStack {
RoundedRectangle(cornerRadius: 5)
.stroke(style: StrokeStyle(lineWidth: 1,
dash: [4]))
.frame(height: 10)
.overlay(Text(time, style: .timer).font(.system(size: 8)).multilineTextAlignment(.center))
}
Image("home-address")
}
}
}

Reference: Apple Documentation

Note that the system may truncate a Live Activity if its height exceeds 160 points.

Here is a link to the entire code on Github.

Thanks for reading.

Want to Connect?Let’s connect on Twitter @batikansosun.

--

--

Tweeting tips and tricks about #swift #xcode #apple Twitter @batikansosun Weekly Swift Blogging