# iOS Live Activities

Add and manage iOS Live Activities using the push API. {{< badge "axp" >}}

For SDK methods, see the [Live Activities](https://www.airship.com/docs/sdk-topics/live-activities/) developer documentation. See also the [iOS Live Activities](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide.

## About implementation

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](https://developer.apple.com/documentation/bundleresources/information_property_list/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:

* [ActivityKit developer guide](https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities)
* [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines)

### Naming 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`:

```swift
struct SportsActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Mutable content
        val status: String
    }

    // GameID
    var gameID: String
}
```


> **Important:** Live Activities require token-based authentication for APNs. See [iOS Channel Configuration](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration).


## App setup


#### iOS Swift



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.

```swift
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:

```swift
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)
}
```




#### React Native



Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/react-native/installation/extending-airship/), 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.

```swift
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

  }
}
```


> **Important:** If you are using Expo, you must copy-paste your exact `ActivityAttributes` struct into your `AirshipPluginExtender.swift` so the compiler can find a definition in order to register the Live Activity.




#### Capacitor



Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/capacitor/installation/extending-airship/), 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.

```swift
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

  }
}
```




#### Flutter



Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/extending-airship/), 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.

```swift
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

> **Note:** 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](https://www.airship.com/docs/developer/rest-api/ua/operations/push/#sendpush) by specifying a [`live_activity` payload](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject) for the event `start`. In this example, a Live Activity will be started for all iOS devices that have the tag `"sports-scores"`:

```json
{
    "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


#### iOS Swift



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:)`.

```swift
let activity = try Activity.request(
    attributes: attributes,
    content: content,
    pushType: .token
)

Airship.channel.trackLiveActivity(
    activity,
    name: attributes.gameID
)
```




#### React Native



For any Live Activities configured, you can start a new one using the `start` method:

```typescript
Airship.iOS.liveActivityManager.start({
  attributesType: 'SportsActivityAttributes',
  content: {
    state: {
      status: 'Game Pending',
    },
    relevanceScore: 0.0,
  },
  attributes: {
    gameID: 'sports-game-123',
  },
});
```




#### Capacitor



For any Live Activities configured, you can start a new one using the `start` method:

```typescript
Airship.iOS.liveActivityManager.start({
  attributesType: 'SportsActivityAttributes',
  content: {
    state: {
      status: 'Game Pending',
    },
    relevanceScore: 0.0,
  },
  attributes: {
    gameID: 'sports-game-123',
  },
});
```




#### Flutter



For any Live Activities configured, you can start a new one using the `start` method:

```dart
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](https://www.airship.com/docs/developer/rest-api/ua/operations/push/#sendpush) 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.

```json
{
    "audience": "all",
    "device_types": [
        "ios"
    ],
    "notification": {
        "ios": {
            "live_activity": {
                "name": "sports-game-123",
                "event": "update",
                "content_state": {
                    "status": "Game starting!" 
                },
                "alert": {
                    "title": "Game is starting",
                    "body": "Tune in!"
                },
                "priority": 10,
                "stale_date": 1666261020,
                "relevance_score": 50
            }
        }
    }
}
```



Updates can also be made within the app.


#### iOS Swift



To update, user the standard ActivityKit APIs. First find the Activity instance then call `update` content on it:

```swift
guard
    let activity = Activity<SportsActivityAttributes>.activities.first(where: { $0.id == "sports-game-123" })
else {
    // not found
    return
}
activity.update(contentUpdate)
```




#### React Native



To update, use `update`, but you will need the activity ID.

```typescript
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,
    },
  });
}
```




#### Capacitor



To update, use `update`, but you will need the activity ID.

```typescript
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,
    },
  });
}
```




#### Flutter



To update, use `update`, but you will need the activity ID.

```dart
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](https://www.airship.com/docs/developer/rest-api/ua/operations/push/#sendpush) 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.

```json
{
    "audience": "all",
    "device_types": [
        "ios"
    ],
    "notification": {
        "ios": {
            "live_activity": {
                "name": "sports-game-123",
                "event": "end",
                "priority": 10,
                "dismissal_date": 1666265020,
                "content_state": {
                    "status": "Game ended"
                }
            }
        }
    }
}
```



You can also end an activity within the app.


#### iOS Swift



To update, user the standard ActivityKit APIs. First find the Activity instance then call `update` content on it:

```swift
guard
    let activity = Activity<SportsActivityAttributes>.activities.first(where: { $0.id == "sports-game-123" })
else {
    // not found
    return
}

activity.end(contentUpdate, dismissalPolicy: .default)
```




#### React Native



To end is similar to `update`. Use `end` with the activity ID:

```typescript
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"
    }
  });
}
```




#### Capacitor



To end is similar to `update`. Use `end` with the activity ID:

```typescript
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"
    }
  });
}
```




#### Flutter



To end is similar to `update`. Use `end` with the activity ID:

```dart
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);
  }
}
```




