Welcome to Part 2 of our guide on integrating Live Activities and Dynamic Island in iOS. In Part 1, we explored the fundamentals—how to set up a Live Activity, manage its lifecycle, and design layouts that work seamlessly on the Lock Screen and Dynamic Island.
In this part, we’ll take things further by adding advanced functionality to elevate the user experience. We’ll learn how to:
Let's get started…
Animations in Live Activities make updates visually engaging and draw the user's attention to important changes. Starting with iOS 17, Live Activities automatically animate data updates using default animations, or we can customize them using SwiftUI animations.
You can replace default animations with SwiftUI's built-in transitions and animations. Options include:
.opacity
, .move(edge:)
, .slide
, or .push(from:)
.contentTransition(.numericText())
for numbers..animation(_:value:
) to views for more control.Text("\(position)")
.font(.system(size: 36, weight: .semibold)).lineSpacing(48).foregroundColor(Color("TextPrimary"))
.contentTransition(.numericText())
.animation(.spring(duration: 0.2), value: position)
To animate a view when data changes:
.id()
modifier to associate the view with a data model that conforms to Hashable.struct QueueIllustration :View {
let position: Int
var imageName: String {
if position < 5 {
return "queue4"
} else if position < 9 {
return "queue3"
} else if position < 25 {
return "queue2"
} else {
return "queue1"
}
}
var body: some View {
Image(uiImage: UIImage(named: imageName)!)
.resizable().frame(width: 100, height: 100)
.scaledToFit().id(imageName)
.transition(.push(from: .bottom))
}
}
isLuminanceReduced
environment value to detect when the Always-On display is active.move
or .slide
are supported, and custom animations like withAnimation
are ignored.If multiple views in your Live Activity are updated simultaneously, consider disabling animations for less important changes to focus user attention.
To disable animations:
.transition(.identity)
to prevent transitions.nil
to the animation parameter withAnimation(nil) {
// Updates without animations
}
One of the most powerful features of Live Activities is their ability to receive updates via push notifications. This enables us to keep the content of a Live Activity synchronized with real-time data, ensuring users always have the latest information without manual intervention.
With ActivityKit, we can update or end Live Activities directly from the server by using push tokens. Starting with iOS 17.2, we can even start new Live Activities using push notifications.
Push notification capability
Before we go ahead make sure you have added the push notification capability in your application target.
Push tokens are the key to communicating with Live Activities on a user’s device.
A push-to-start token is a secure identifier generated by the system, which enables the server to start a new Live Activity remotely via Apple Push Notification service (APNs). This token is device-specific and tied to the user’s current app state. When the server sends a valid payload using this token, the system creates a Live Activity on the user’s device.
We can use the pushToStartTokenUpdates
asynchronous API provided by ActivityKit to listen for and retrieve push-to-start tokens. This API notifies our app whenever a new token is available.
Here’s how to implement it:
func observePushToStartToken() {
if #available(iOS 17.2, *), areActivitiesEnabled() {
Task {
for await data in Activity<WaitTimeDemoAttributes>.pushToStartTokenUpdates {
let token = data.map {String(format: "%02x", $0)}.joined()
// Send token to the server
}
}
}
}
Activity<MyAttributes>.pushToStartTokenUpdates
: an asynchronous sequence that continuously listens for new push-to-start tokens generated by the system.
Once we have started a Live Activity on the user's device, we might need to update it periodically, such as to reflect progress, changes in status, or updated data. This is where push token updates come into play. When the app starts a Live Activity, it will receive a push token from ActivityKit, which we can use to send updates to the Live Activity via push notifications.
The pushTokenUpdates
API is used to retrieve the push token required for sending updates to an existing Live Activity. This token is a unique identifier for each Live Activity that the app can use to send push notifications to the device.
Here's how to get an update token:
func observePushUpdateToken() {
// Check if iOS version 17.2 or later is available and if activities are enabled
if #available(iOS 17.2, *), areActivitiesEnabled() {
// Start a task to observe updates from ActivityKit
Task {
// Listen for any updates from a Live Activity with WaitTimeDemoAttributes
for await activityData in Activity<WaitTimeDemoAttributes>.activityUpdates {
// Listen for updates to the push token associated with the Live Activity
Task {
// Iterate through the push token updates
for await tokenData in activityData.pushTokenUpdates {
// Convert the token data to a hexadecimal string
let token = tokenData.map { String(format: "%02x", $0) }.joined()
// Obtain the associated booking ID from the activity attributes
let bookingId = activityData.attributes.bookingId
// Prepare the data dictionary to pass to the callback
let data = [
"token": token,
"bookingId": activityData.attributes.bookingId
]
// TODO Send data to the server
}
}
}
}
}
}
After obtaining the update push token, we can use it to send push notifications to update the Live Activity. The token may change over time, so the server needs to be prepared to handle updates to the push token. Whenever a new token is received, it should be updated on the server and invalidated on previous tokens.
The format of the push notification is similar to the one we used to start a Live Activity, but this time, we're sending an update to the content or state of the existing Live Activity.
1. Payload for Starting a Live Activity
When the server needs to start a Live Activity, it sends a JSON payload to APNs, using the push-to-start token as an identifier. The system will create the Live Activity on the device upon receiving this payload.
Payloads(all fields are required):
"aps": {
"timestamp": '$(date +%s)',
"event": "start",
"content-state": {
"progress": 0.1,
"currentPositionInQueue": 8
},
"attributes-type": "WaitTimeDemoAttributes",
"attributes": {
"waitlistName": "For Testing",
"waitlistId": "",
"bookingId": ""
},
"alert": {
"title": "",
"body": "",
"sound": "default"
}
}
timestamp
: Represents the current time in UNIX timestamp format to indicate when the event occurred.event
: The type of event; in this case, we’re starting a Live Activity ("start
")content-state
: Holds information related to the current state of the activity. Note: The key should match with Live activity Attributes.attributes-type
: Specifies the type of attributes for the Live Activity. A Live activity data holder, which contains the state and attributes.attributes
: Contains the immutable data of live activity.alert
: Configures a notification that may appear to the user.2. Headers for Push Notification
When sending this push notification, the following headers are required:
apns-topic
: The topic indicates the bundle identifier and specifies the push type for Live Activities.apns-topic: <bundleId>.push-type.liveactivity
apns-push-type:
Specify that this is a push notification for a Live Activity.apns-push-type: liveactivity
apns-priority
: Set the value for the priority to 5 or 10 (We'll discuss this in next section). apns-priority: 10
authorization
: The bearer token used for authenticating the push notification request.authorization: bearer $AUTHENTICATION_TOKEN
1. Starting the Live Activity:
2. Token Refresh:
Payloads(all fields are required):
"aps": {
"timestamp": '$(date +%s)',
"event": "update",
"content-state": {
"progress": 0.941,
"currentPositionInQueue": 10
}
"alert": {
"title": "",
"body": " ",
"sound": "anysound.mp4"
}
}
Once we have the payload and headers set up, we'll send the push notification from the server to APNs, which will then deliver it to the user’s device. When the device receives the update notification, ActivityKit will apply the changes to the existing Live Activity and update the content on the Dynamic Island or Lock Screen.
Once a Live Activity has ended, the system ignores any further push notifications sent to that activity. This means if we send an update after the Live Activity is complete, the system won't process it, and the information may no longer be relevant. If a push notification is not delivered or is ignored after the activity ends, the Live Activity might display outdated or incorrect information.
To end a Live Activity, we can send a push notification with the event
field set to end
. This marks the activity as complete.
By default, a Live Activity stays on the Lock Screen for up to four hours after it ends, giving users a chance to refer to the latest information. However, we can control when the Live Activity disappears by adding a dismissal-date field in the push notification's payload.
For example, in a waitlist scenario, when the waitlist is over and users are served, we can send an update with an "end" event, like this:
{
"aps": {
"timestamp": 1685952000,
"event": "end",
"dismissal-date": 1685959200,
"content-state": {
"progress": 0.1,
"currentPositionInQueue": 1
}
}
}
To remove Live Activity immediately after it ends, set the dismissal-date
to a timestamp in the past. For example: "dismissal-date": 1663177260
. Alternatively, we can set a custom dismissal time within a four-hour window.
If we don’t include a dismissal-date
, the system will handle the dismissal automatically, using its default behavior.
When our app starts multiple Live Activities, we can control which one appears in the Dynamic Island by setting a relevance-score
in the JSON payload.
If we don’t set a score or if all Live Activities have the same score, the system will show the first one started in the Dynamic Island. To highlight more important updates, assign a higher relevance score (e.g., 100), and for less important ones, use a lower score (e.g., 50).
Make sure to track and update relevance scores as needed to control which Live Activity appears in the Dynamic Island. Here's an example with a high relevance score of 100:
{
"aps": {
"timestamp": 1685952000,
"event": "update",
"relevance-score": 100,
"content-state": {
....
}
}
}
This ensures that the most important Live Activity update is shown in the Dynamic Island.
ActivityKit push notifications come with certain limits on how frequently they can be sent to avoid overwhelming users. This limit is referred to as the ActivityKit notification budget, which restricts the number of updates our app can send per hour. To manage this budget and prevent throttling, we can adjust the priority of our push notifications using the apns-priority
header.
Setting the Priority
By default, if we don’t specify a priority for push notifications, they will be sent with a priority of 10 (immediate delivery), and this will count toward ActivityKit push notification budget. If we exceed this budget, the system may throttle our notifications. To reduce the chance of hitting the limit, consider using a lower priority (priority 5) for updates that don’t require immediate attention.
Handling Frequent Updates
In some use cases, such as tracking a live sports event, we may need to update our Live Activity more frequently than usual. However, updating the Live Activity too often could quickly exhaust the notification budget, causing delays or missed updates.
To overcome this, we can enable frequent updates for our Live Activity. This allows our app to send updates at a higher frequency without hitting the budget limits. To enable this feature, we need to modify our app's Info.plist file:
Open the Info.plist file and add the following entry:
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
This change will allow our app to send frequent ActivityKit push notifications. However, users can turn off this feature in their device settings.
Detecting and Handling Frequent Update Settings
If a user has deactivated frequent updates for the app, we can detect this and display a message asking them to turn it back on. Additionally, we can adjust our update frequency to match the user's preferences.
We can also subscribe to updates regarding the status of frequent updates via the frequentPushEnablementUpdates
stream, which allows us to respond to changes in the notification settings in real-time.
In Summary
Unlike standard notifications, Live Activity push notifications require specific request headers to work properly, making their testing process slightly different.
Here’s how we can test them effectively:
For a simpler and quicker method, Apple provides the Apple Push Notifications Console — a tool designed for testing various types of notifications, including Live Activity ones.
apns-topic
: Your application’s bundle ID.apns-push-type
: Set this to liveactivity
.apns-push-priority
: Use high
to ensure immediate delivery.{
"aps": {
"timestamp": 0,
"event": "start",
"content-state": {
"progress": 0.1,
"currentPositionInQueue": 8
},
"attributes-type": "WaitlistWidgetAttributes",
"attributes": {
"waitlistName": "For Testing",
"bookingId": "GzYfE7P5Wa3lB3xSkUbk"
},
"alert": {
"title": "You've been added to waitlist",
"body": "Wait for while we're preparing your tabel",
"sound": "default"
}
}
}
Once the fields are filled, press Send and watch the Live Activity added on your Lock Screen or Dynamic Island.
curl
CommandAnother hands-on approach is to send a real request to APNs using the curl
command. It’s a bit more technical, but it ensures the notifications behave as expected in a live environment.
To test Live Activity push notifications using the curl
command, we need an Authorization Token (JWT). This token allows us to authenticate our request with the Apple Push Notification Service (APNs).
Here's a step-by-step guide to generating this token:
1. Required Information
Before generating the token, ensure you have the following:
2. Generate the JWT Authentication Token
# Set variables
JWT_ISSUE_TIME=$(date +%s) # Current timestamp in seconds
AUTH_KEY_ID="<Your Auth Key ID>" # Your Auth Key ID
TEAM_ID="<Your Team ID>" # Your Apple Developer Team ID
TOKEN_KEY_FILE_NAME="<Path>" # Path to your .p8 file
# Generate the JWT header
JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | \
openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
# Generate the JWT claims
JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | \
openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
# Combine header and claims
JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
# Sign the header and claims using the private key
JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | \
openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | \
openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
# Combine everything into the final JWT token
AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"
# Output the token
echo "Generated Authorization Token: ${AUTHENTICATION_TOKEN}"
For a detailed guide on setting up and sending these requests, refer to Apple's documentation.
Once the token is generated, use it in the Authorization
header of curl
command. Here’s full command to send a Live Activity push notification:
curl \
--header "apns-topic: com.example.push-type.liveactivity" \
--header "apns-push-type: liveactivity" \
--header "apns-priority: 10" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{
"aps": {
"timestamp": '$(date +%s)',
"event": "start",
"content-state": {
"progress": 0.1,
"currentPositionInQueue": 8
},
"attributes-type": "WaitlistWidgetAttributes",
"attributes": {
"waitlistName": "For Testing",
"bookingId": "GzYfE7P5Wa3lB3xSkUbk"
},
}
}' \
--http2 https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN
Note: To start the live activity use a push-to-start token in the URL, to update and end the activity use push to update token.
Let's see the result;
Start the Live activity
Update/end the live activity
If you're not getting the push-to-start token;
If you're not getting the push-to-update token;
pushTokenUpdates
won't be received until the user grants permission to display Live Activities on the lock screen.If you're getting this error related to push notifications,
Error: Error Domain=com.apple.ActivityKit.ActivityInput Code=0 "(null)" UserInfo={NSUnderlyingError=0x600003b14db0 {Error Domain=SessionCore.PermissionsError Code=4 "(null)"}}
To fix this make sure you've added push capability in application target.
That wraps up our two-part guide on integrating Live Activities and Dynamic Island in iOS. By now, you should have a solid understanding of how to create real-time, interactive updates that enhance user engagement. From the basics of setting up a Live Activity to adding advanced features like animations, you now have the tools to bring dynamic, glanceable content to your app
Thank you for following along! Happy coding!
Get started today
Let's build the next
big thing!
Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.
Get Free ConsultationGet started today
Let's build the next big thing!
Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.
Get Free Consultation