iOS Live Activities
A Live Activity displays current data from your app on the iPhone Lock Screen and in the Dynamic Island. AXP iOS SDK 16.10+React Native Module 19.4+Capacitor Module 2.3+Flutter Module 7.9+
Live Activities overview
A Live Activity can be displayed and updated for up to eight hours until it expires. After it expires, an activity can continue to be displayed for up to four hours before it is automatically dismissed. Multiple Live Activities can be started by an app, and a device can show multiple activities from different apps, the maximum number of which may depend on a range of factors.
Updates to Live Activities are made through push notifications and/or background tasks in the app. For more timely updates, Airship recommends using push notifications or push notifications in addition to background tasks.
When updating through push notifications, APNs allows a certain budget of updates per hour. If an app exceeds this budget, notifications will be throttled. To avoid being throttled, a mix of low priority (priority 5) and high priority notifications (priority 10) can be used.
In addition to priorities and background tasks, if a use case requires frequent push updates, an app can also set the plist flag NSSupportsLiveActivitiesFrequentUpdates to prevent being throttled. However, the end user is able to disable frequent updates in the app settings.
For more information on implementing Live Activities, see Apple documentation:
Implementing Live Activities
Airship’s Live Activity support allows starting Live Activities through a push and tracking each Live Activity’s unique push token by a name on the app’s channel. The name can then be used to send updates to a Live Activity. The name can be unique to the device or shared across multiple devices. Airship handles mapping the name back to the token for the specified audience and sends an update to each token. All tokens are tracked under the device channel. They do not increase your billable audience. In order to support starting a Live Activity from both a push notification and locally, it is recommended that you provide the name you will use to track the activity in the activity’s attributes, and required if you are trying to support Live Activities from a framework.
For example, to provide updates for tracking a sports game, create a Live Activity with name sports-game-123
. Add the id of the game to the Activity’s attributes as gameID
:
struct SportsActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Mutable content
val status: String
}
// GameID
var gameID: String
}
Live Activities require token-based authentication for APNs. See iOS Channel Configuration.
App setup
Configuring Live Activities
To support Live Activities, you must call restore once after takeOff during application(_:didFinishLaunchingWithOptions:)
with all the Live Activity types that you might track with Airship. This allows Airship to resume tracking any previously tracked activities across app inits and to automatically track the pushToStartToken
that allows starting activities through a push notification.
Airship.takeOff(config, launchOptions: launchOptions)
Airship.channel.restoreLiveActivityTracking { restorer in
await restorer.restore(forType: Activity<SportsActivityAttributes>.self)
await restorer.restore(forType: Activity<SomeOtherAttributes>.self)
}
After the restore
call above, Airship will track the pushToStartTokens
for the activity’s attribute types. You can then start a Live Activity through a
push notification. Starting a Live Activity does not automatically track it. Instead, the app will be woken up and you must call through to Airship with the activity
instance and the name.
There is no entry point into the app when it is started for a Live Activity being created. Instead, you need to query Live Activities on init and when a pushToStartToken
update is received to track
them through Airship. Airship provides an extension Activity<T>.airshipWatchActivities(activityBlock:)
that can be used to do this for you.
In this example, we assume the gameID
on our SportsActivityAttributes
will be used to send updates through Airship after it is created:
Airship.channel.restoreLiveActivityTracking { restorer in
await restorer.restore(forType: Activity<SportsActivityAttributes>.self)
}
Activity<SportsActivityAttributes>.airshipWatchActivities { activity in
Airship.channel.trackLiveActivity(activity, name: activity.attributes.gameID)
}
Using the AirshipPluginExtender, make a call to LiveActivityManager.shared.setup
to configure any Live Activities for the app. Call configurator.register
for each Live Activity type that your application defines and
include a block on how to parse the name of the activity that you will use to track on Airship. This name will be used to send updates through APNS.
import Foundation
import AirshipKit
import AirshipFrameworkProxy
import ActivityKit
// This class header is required to be automatically picked up by the Airship plugin:
@objc(AirshipPluginExtender)
public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol {
public static func onAirshipReady() {
if #available(iOS 16.1, *) {
// Will throw if called more than once
try? LiveActivityManager.shared.setup { configurator in
// Call for each Live Activity type
await configurator.register(forType: Activity<SportsActivityAttributes>.self) { attributes in
// Track this property as the Airship name for updates
attributes.gameID
}
}
}
// other setup
}
}
Using the AirshipPluginExtender, make a call to LiveActivityManager.shared.setup
to configure any Live Activities for the app. Call configurator.register
for each Live Activity type that your application defines and
include a block on how to parse the name of the activity that you will use to track on Airship. This name will be used to send updates through APNS.
import Foundation
import AirshipKit
import AirshipFrameworkProxy
import ActivityKit
// This class header is required to be automatically picked up by the Airship plugin:
@objc(AirshipPluginExtender)
public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol {
public static func onAirshipReady() {
if #available(iOS 16.1, *) {
// Will throw if called more than once
try? LiveActivityManager.shared.setup { configurator in
// Call for each Live Activity type
await configurator.register(forType: Activity<SportsActivityAttributes>.self) { attributes in
// Track this property as the Airship name for updates
attributes.gameID
}
}
}
// other setup
}
}
Using the AirshipPluginExtender, make a call to LiveActivityManager.shared.setup
to configure any Live Activities for the app. Call configurator.register
for each Live Activity type that your application defines and
include a block on how to parse the name of the activity that you will use to track on Airship. This name will be used to send updates through APNS.
import Foundation
import AirshipKit
import AirshipFrameworkProxy
import ActivityKit
// This class header is required to be automatically picked up by the Airship plugin:
@objc(AirshipPluginExtender)
public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol {
public static func onAirshipReady() {
if #available(iOS 16.1, *) {
// Will throw if called more than once
try? LiveActivityManager.shared.setup { configurator in
// Call for each Live Activity type
await configurator.register(forType: Activity<SportsActivityAttributes>.self) { attributes in
// Track this property as the Airship name for updates
attributes.gameID
}
}
}
// other setup
}
}
Starting Live Activities
When you start a Live Activity from a push notification, you should limit which devices receive the push by specifying the audience
on the push request unless you want your entire iOS audience to start the Live Activity.
Live Activities can be started via the Push API by specifying a live_activity
payload for the event start
. In this example, a Live Activity will be started for all iOS devices that have the tag "sports-scores"
:
{
"audience": {
"tag": "sports-score",
},
"device_types": [
"ios"
],
"notification": {
"ios": {
"live_activity": {
"event": "start",
"attributes_type": "SportsActivityAttributes",
"attributes": {
"gameID": "sports-game-123"
},
"content_state": {
"status": "Game about to begin!"
},
"alert": {
"title": "Game about to begin",
"body": "Tune in!"
},
"priority": 10
}
}
}
}
Live Updates can also be started from within the app when:
- Anytime the app is in the foreground
- From activity intent
- From a notification action button
Starting Live Activities
To start a Live Activity from the app, make sure to set the pushType to .token
. After it is started, immediately track it with Airship.channel.trackLiveActivity(_:name:)
.
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token
)
Airship.channel.trackLiveActivity(
activity,
name: attributes.gameID
)
For any Live Activities configured, you can start a new one using the start
method:
Airship.iOS.liveActivityManager.start({
attributesType: 'SportsActivityAttributes',
content: {
state: {
status: 'Game Pending',
},
relevanceScore: 0.0,
},
attributes: {
gameID: 'sports-game-123',
},
});
For any Live Activities configured, you can start a new one using the start
method:
Airship.iOS.liveActivityManager.start({
attributesType: 'SportsActivityAttributes',
content: {
state: {
status: 'Game Pending',
},
relevanceScore: 0.0,
},
attributes: {
gameID: 'sports-game-123',
},
});
For any Live Activities configured, you can start a new one using the start
method:
if (Platform.isIOS) {
LiveActivityStartRequest startRequest = LiveActivityStartRequest(
attributesType: 'SportsActivityAttributes',
attributes: {
"gameID": sports-game-123,
},
content:
LiveActivityContent(status: 'Game Pending', relevanceScore: 0.0));
await Airship.liveActivityManager.start(startRequest);
}
Updating Live Activities
Now that the Live Activity is started, the content can be updated via the Push API with the following payload. This example will send an update to all iOS channels that have a Live Update named sports-game-123
. The audience
field can be specified to restrict who receives the update.
{
"audience": "all",
"device_types": [
"ios"
],
"notification": {
"ios": {
"live_activity": {
"name": "sports-game-123",
"event": "update",
"content_state": {
"status": "Game starting!"
},
"dismissal_date": 1666261020,
"alert": {
"title": "Game is starting",
"body": "Tune in!"
},
"priority": 10,
"stale_date": 1666261020,
"relevance_score": 50
}
}
}
}
Updates can also be made within the app.
Updating Live Activities
To update, user the standard ActivityKit APIs. First find the Activity instance then call update
content on it:
guard
let activity = Activity<SportsActivityAttributes>.activities.first(where: { $0.id == "sports-game-123" })
else {
// not found
return
}
activity.update(contentUpdate)
To update, use update
, but you will need the activity ID.
const activities = await Airship.iOS.liveActivityManager.listAll();
const activity = activities.find(
(activity) => activity.attributes.gameID === 'sports-game-123'
);
if (activity) {
Airship.iOS.liveActivityManager.update({
activityId: activity.id,
content: {
state: {
status: "Game starting!"
}
relevanceScore: 0.0,
},
});
}
To update, use update
, but you will need the activity ID.
const activities = await Airship.iOS.liveActivityManager.listAll();
const activity = activities.find(
(activity) => activity.attributes.gameID === 'sports-game-123'
);
if (activity) {
Airship.iOS.liveActivityManager.update({
activityId: activity.id,
content: {
state: {
status: "Game starting!"
}
relevanceScore: 0.0,
},
});
}
To update, use update
, but you will need the activity ID.
if (Platform.isIOS) {
List<LiveActivity> activities = await Airship.liveActivityManager.listAll();
LiveActivity? activity = activities
.where((activity) => activity.attributes.gameID == 'sports-game-123')
.firstOrNull;
if (activity != null) {
LiveActivityContent content = LiveActivityContent(
state: {'status': 'Game starting!'},
relevanceScore: 0.0,
);
LiveActivityUpdateRequest updateRequest = LiveActivityUpdateRequest(
attributesType: 'SportsGameAttributes',
activityId: activity.id,
content: content,
);
await Airship.liveActivityManager.update(updateRequest);
}
}
Ending Live Activities
Live Activities will automatically expire after 8 hours and dismiss after 12. You can end and or dismiss a Live Activity earlier via the Push API with the following payload. This example will send end to all iOS channels that have a Live Update named sports-game-123
. The audience
field can be specified to restrict who receives the update.
{
"audience": "all",
"device_types": [
"ios"
],
"notification": {
"ios": {
"live_activity": {
"name": "sports-game-123",
"event": "end",
"priority": 10
}
}
}
}
You can also end an activity within the app.
Ending Live Activities
To update, user the standard ActivityKit APIs. First find the Activity instance then call update
content on it:
guard
let activity = Activity<SportsActivityAttributes>.activities.first(where: { $0.id == "sports-game-123" })
else {
// not found
return
}
activity.end(contentUpdate, dismissalPolicy: .default)
To end is similar to update
. Use end
with the activity ID:
const activities = await Airship.iOS.liveActivityManager.listAll();
const activity = activities.find(
(activity) => activity.attributes.gameID === 'sports-game-123'
);
if (activity) {
Airship.iOS.liveActivityManager.end({
activityId: activity.id,
dismissalPolicy: {
type: "default"
}
});
}
To end is similar to update
. Use end
with the activity ID:
const activities = await Airship.iOS.liveActivityManager.listAll();
const activity = activities.find(
(activity) => activity.attributes.gameID === 'sports-game-123'
);
if (activity) {
Airship.iOS.liveActivityManager.end({
activityId: activity.id,
dismissalPolicy: {
type: "default"
}
});
}
To end is similar to update
. Use end
with the activity ID:
if (Platform.isIOS) {
List<LiveActivity> activities = await Airship.liveActivityManager.listAll();
LiveActivity? activity = activities
.where((activity) => activity.attributes.gameID == 'sports-game-123')
.firstOrNull;
if (activity != null) {
LiveActivityStopRequest stopRequest = LiveActivityStopRequest(
attributesType: 'SportsGameAttributes',
activityId: activity.id,
dismissalPolicy: LiveActivityDismissalPolicyDefault(),
);
await Airship.liveActivityManager.end(stopRequest);
}
}
Categories