iOS Live Activities
Live Activities are realtime widgets that appear on your iOS Lock Screen. They are continuously updated so that you can display an app's live state as changes happen. Examples include showing sports scoreboard updates, delivery trackers, and the progress of long-running tasks.
Live Activities are updated through APNs rather than through an app itself. Rather than having to collect and refresh a push token for every activity on every device, Ably holds your APNs credentials and manages APNs broadcast channels on your behalf.
Your server creates a broadcast channel once, then starts, updates, and ends Live Activities with three lifecycle calls; each state change is a single publish that APNs fans out to every activity subscribed to on the broadcast channel.
Devices register themselves using the same push activation flow used for ordinary push notifications.
Prerequisites
- An Ably app with APNs configured: upload your APNs .p8 authentication key so Ably can push on your behalf.
- An API key or token with the
push-admincapability for the server-side calls on this page. - An iOS app with the
NSSupportsLiveActivitiesInfo.plist key, a Live Activity widget extension, and anActivityAttributesmodel. - Devices running iOS 18 or later. Live Activities themselves are available from iOS 16.1 and push-to-start from iOS 17.2, but the broadcast channels that Ably uses to deliver updates require iOS 18.
Register the device
Activate the device with Ably using the standard push activation flow, then add the Live Activity push-to-start token to the registration. iOS delivers push-to-start tokens through ActivityKit's pushToStartTokenUpdates stream:
1
2
3
4
5
6
7
let options = ARTClientOptions()
options.authUrl = URL(string: "https://example.com/api/auth")
options.pushRegistererDelegate = self
let rest = ARTRest(options: options)
// Standard activation: registers the device and its APNs device token.
rest.push.activate()Forward the APNs device token from your UIApplicationDelegate as usual:
1
2
3
4
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
ARTPush.didRegisterForRemoteNotifications(withDeviceToken: deviceToken, rest: rest)
}Once activation completes (didActivateAblyPush), register the push-to-start token. The token is raw Data, exactly as delivered by ActivityKit; completion is reported through the didUpdateAblyPush delegate callback:
1
2
3
4
5
Task {
for await token in Activity<GameAttributes>.pushToStartTokenUpdates {
rest.push.registerPushToStartToken(token)
}
}Finally, make the device targetable for push-to-start. Either subscribe it to an Ably channel, which is only used to select which devices receive push-to-start and is unrelated to the APNs broadcast channel that carries updates:
1
2
3
rest.channels.get("games:lal-bos").push.subscribeDevice { error in
// Subscribed: the server can now target this channel
}or use the device's Ably deviceId (available as rest.device.id after activation) to target it directly.
Create a broadcast channel
On the server, create the APNs broadcast channel through Ably's push admin API:
1
2
3
4
5
const rest = new Ably.Rest({ key: process.env.ABLY_API_KEY });
const { id, apnsChannelId } = await rest.push.admin.createApnsBroadcast({
messageStoragePolicy: 1,
});Keep both identifiers: id drives every later server call, and apnsChannelId is what devices subscribe to. messageStoragePolicy: 1 tells APNs to cache the most recent broadcast for late joiners.
Start a Live Activity
There are two ways to start a Live Activity. Use push-to-start to start it remotely from your server on devices that have registered a push-to-start token, for example when a game kicks off or an order is dispatched. Alternatively, start it locally in the app when the activity begins with a user action, such as placing an order. In both cases the activity subscribes to the broadcast channel, so subsequent updates are delivered the same way regardless of how it was started.
Push-to-start from the server
Start a Live Activity on every device subscribed to the given Ably channels, or on a specific device by deviceId. Ably uses each matched device's registered push-to-start token, and input-push-channel subscribes the new activity to the broadcast channel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
await rest.push.admin.liveActivity.start({
recipient: { channels: ['games:lal-bos'] }, // or { deviceId: '...' }
apnsBroadcast: id,
apns: {
aps: {
timestamp: Math.floor(Date.now() / 1000),
event: 'start',
'input-push-channel': apnsChannelId,
'content-state': {
homeScore: 0,
awayScore: 0,
gameStatus: 'scheduled',
period: 'Q1',
clock: '12:00',
lastPlay: 'Tip-off soon',
},
'attributes-type': 'GameAttributes',
attributes: { homeTeam: 'Lakers', awayTeam: 'Celtics' },
alert: {
title: 'Lakers vs Celtics',
body: 'Game starting!',
},
},
},
headers: { 'apns-priority': '10' },
});Start locally in the app
Alternatively, the app starts the activity itself and subscribes it to the broadcast channel with pushType: .channel:
1
2
3
4
5
let activity = try Activity.request(
attributes: GameAttributes(homeTeam: "Lakers", awayTeam: "Celtics"),
content: ActivityContent(state: initialState, staleDate: nil),
pushType: .channel(apnsChannelId)
)Update a Live Activity
Each update is one broadcast against the Ably broadcast id; APNs delivers it to every subscribed activity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
await rest.push.admin.liveActivity.update({
apnsBroadcast: id,
apns: {
aps: {
timestamp: Math.floor(Date.now() / 1000),
event: 'update',
'content-state': {
homeScore: 102,
awayScore: 98,
gameStatus: 'live',
period: 'Q4',
clock: '2:14',
lastPlay: 'Curry 3PT (25 PTS)',
},
},
},
headers: {
'apns-priority': '10',
'apns-expiration': String(Math.floor(Date.now() / 1000) + 3600),
},
});End a Live Activity
Ending works the same way with event: 'end'. An optional dismissal-date controls when iOS removes the activity from the lock screen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
await rest.push.admin.liveActivity.end({
apnsBroadcast: id,
apns: {
aps: {
timestamp: Math.floor(Date.now() / 1000),
event: 'end',
'content-state': {
homeScore: 112,
awayScore: 104,
gameStatus: 'finished',
period: 'Final',
clock: '',
lastPlay: 'Final',
},
'dismissal-date': Math.floor(Date.now() / 1000) + 3600,
},
},
headers: { 'apns-priority': '10' },
});The underlying REST endpoint is POST /push/apnsBroadcastChannels/{id}/end.
How it works
A Live Activity integration has three moving parts:
-
Create a broadcast channel: your server asks Ably to create an APNs broadcast channel. Ably returns a pair of identifiers: an opaque broadcast
id, which your server uses in every subsequent start, update, and end call, and anapnsChannelId, which iOS devices use to subscribe an activity to the channel. -
Start the Live Activity, either:
- From the server with push-to-start: devices that have registered a Live Activity push-to-start token with Ably can have activities started remotely. The start payload includes the
apnsChannelId(asinput-push-channel), so the started activity is subscribed to the broadcast channel immediately. - Locally in the app: the app starts the activity itself with
pushType: .channel(apnsChannelId), subscribing it to the broadcast channel.
- Update and end by broadcast: each state change is a single publish against the broadcast
id. APNs fans it out to every subscribed activity on every device, with no per-activity tokens involved. Creating the channel withmessageStoragePolicy: 1caches the most recent update so late-joining activities receive the current state as soon as they subscribe.
Payload reference
Ably passes the apns payload to APNs as-is. The following aps fields are used by Live Activities:
| Field | Used in | Description |
|---|---|---|
timestamp | all | Unix epoch seconds; APNs discards updates older than the activity's current state. |
event | all | start, update, or end. |
content-state | all | The activity's dynamic state; keys must match the Swift ContentState properties. |
attributes-type | start | The exact name of the Swift ActivityAttributes struct. |
attributes | start | The activity's fixed attributes; Date values as Unix epoch seconds. |
input-push-channel | start | The apnsChannelId the started activity subscribes to for broadcast updates. |
alert | start, update | The user-visible notification shown when the activity starts, or when an update is significant enough to alert the user. |
dismissal-date | end | Unix epoch seconds after which iOS removes the ended activity from the lock screen. |
Supported request headers:
| Header | Description |
|---|---|
apns-priority | 10 for immediate delivery, 5 for opportunistic delivery. |
apns-expiration | Unix epoch seconds until which APNs stores the broadcast for delivery. |