From a59c5b025978dbcd4250924b32defdc890b7689d Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 8 Jul 2026 13:54:03 +0100 Subject: [PATCH] docs(push): address Live Activities review feedback - Attribute update fan-out to APNs and clarify the one-time channel create plus three lifecycle calls in the intro - Fix the APNs config link (needs ?lang=swift) and use ".p8 authentication key" terminology - Add the push-admin capability prerequisite - Correct iOS requirements: broadcast channels need iOS 18, not 17.2 - Remove the activate(pushToStart:) section; that API is not yet in ably-cocoa - Clarify that the Ably channel only selects push-to-start targets and is unrelated to the APNs broadcast channel - Note that update() posts to /broadcast because an update is a single broadcast, and that alert is also valid in update events - Style guide fixes: remove em-dashes and bold list prefixes Co-Authored-By: Claude Fable 5 --- src/data/nav/pubsub.ts | 4 + src/pages/docs/push/live-activities.mdx | 244 ++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/pages/docs/push/live-activities.mdx diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index dee84c0db1..d72e1fa92a 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -296,6 +296,10 @@ export default { name: 'Publish', link: '/docs/push/publish', }, + { + name: 'Live Activities', + link: '/docs/push/live-activities', + }, ], }, { diff --git a/src/pages/docs/push/live-activities.mdx b/src/pages/docs/push/live-activities.mdx new file mode 100644 index 0000000000..1696d6727a --- /dev/null +++ b/src/pages/docs/push/live-activities.mdx @@ -0,0 +1,244 @@ +--- +title: iOS Live Activities +meta_description: "Start, update, and end iOS Live Activities with Ably using APNs broadcast channels, without managing per-activity push tokens." +meta_keywords: "Live Activity, Live Activities, ActivityKit, push-to-start, APNs broadcast channel, Dynamic Island, iOS push notifications" +--- + +Live Activities display an app's live state on the iOS lock screen and in the Dynamic Island, for example a sports scoreboard, a delivery tracker, or the progress of a long-running task. Apple updates Live Activities through APNs rather than through the app itself, which normally means collecting and refreshing a push token for every activity on every device. + +Ably removes that bookkeeping. Ably holds your APNs credentials and manages [APNs broadcast channels](https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications) 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 the broadcast channel. Devices register themselves using the same [push activation](/docs/push/configure/device?lang=swift#activate) flow used for ordinary push notifications. + + + +## How it works + +A Live Activity integration has three moving parts: + +1. 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 an `apnsChannelId`, which iOS devices use to subscribe an activity to the channel. + +2. 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` (as `input-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. + +3. 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 with `messageStoragePolicy: 1` caches the most recent update so late-joining activities receive the current state as soon as they subscribe. + + + +## Prerequisites + +* An Ably app with [APNs configured](/docs/push/configure/device?lang=swift#apns): upload your APNs .p8 authentication key so Ably can push on your behalf. +* An API key or token with the [`push-admin` capability](/docs/auth/capabilities) for the server-side calls on this page. +* An iOS app with the `NSSupportsLiveActivities` Info.plist key, a Live Activity widget extension, and an `ActivityAttributes` model. +* 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. +* Clients should authenticate with [token authentication](/docs/auth/token), for example an `authUrl` pointing at your server, so the API key never ships in the app. + +## Register the device + +Activate the device with Ably using the standard [push activation](/docs/push/configure/device?lang=swift#activate) flow, then add the Live Activity push-to-start token to the registration. iOS delivers push-to-start tokens through ActivityKit's `pushToStartTokenUpdates` stream: + + +```swift +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: + + +```swift +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: + + +```swift +Task { + for await token in Activity.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: + + +```swift +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: + + +```javascript +const rest = new Ably.Rest({ key: process.env.ABLY_API_KEY }); + +const { id, apnsChannelId } = await rest.push.admin.createApnsBroadcast({ + messageStoragePolicy: 1, +}); +``` + + +The underlying REST endpoint is `POST /push/apnsBroadcastChannels`. + +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 + +### 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: + + +```javascript +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' }, +}); +``` + + +The underlying REST endpoint is `POST /push/apnsBroadcastChannels/{id}/start`. + +### Start locally in the app + +Alternatively, the app starts the activity itself and subscribes it to the broadcast channel with `pushType: .channel`: + + +```swift +let activity = try Activity.request( + attributes: GameAttributes(homeTeam: "Lakers", awayTeam: "Celtics"), + content: ActivityContent(state: initialState, staleDate: nil), + pushType: .channel(apnsChannelId) +) +``` + + +## Update and end + +Each update is one broadcast against the Ably broadcast `id`; APNs delivers it to every subscribed activity: + + +```javascript +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), + }, +}); +``` + + +The underlying REST endpoint is `POST /push/apnsBroadcastChannels/{id}/broadcast`: an update is a single broadcast to the channel. + +Ending works the same way with `event: 'end'`. An optional `dismissal-date` controls when iOS removes the activity from the lock screen: + + +```javascript +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`. + +## 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. |