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
}
 Important

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

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