Update Dynamic Island and Live Activity with Push Notification

ohdarling
6 min readOct 29, 2022
Dynamic Island

With the launch of Live Activity in iOS 16 and the official support of Dynamic Island in iOS 16.1, we have been able to submit to the App Store for release. Recently, I encountered some problems in testing the background update of Live Activity, so I will record them here.

What is Dynamic Island and Live Activity

Live Activity and Dynamic Island actually exist as two forms of the same widget, and they are configured using ActivityConfiguration in ActivityKit. Dynamic Island content is displayed normally on iPhone 14 Pro and iPhone 14 Pro Max, which have Dynamic Island support, while it is displayed as Live Activity on all other models.

struct GroceryDeliveryApp: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: GroceryDeliveryAppAttributes.self) { context in
return VStack(alignment: .leading) {
HStack {
VStack(alignment: .center) {
Text(context.state.courierName + " is on the way!").font(.headline)
Text(Date(timeIntervalSince1970: context.state.deliveryTime).formatted(Date.ISO8601FormatStyle()))
}
}
}.padding(15)
} dynamicIsland: { context in

DynamicIsland {
// UI for expanded state
} compactLeading: {
// UI for leading in compact state
} compactTrailing: {
// UI for trailing in compact state
} minimal: {
// UI for minimal state
}
.keylineTint(.cyan)
}
}
}

How to update Dynamic Island and Live Activity

Unlike the normal home widgets, Live Activity does not have a timeline provider to provide a regular update mechanism, it can only rely on the App to update it actively, or rely on Push notifications to update it.

If you want to use an App to actively update Live Activity, it requires the App to have any kind of background running mode to keep the App running continuously in the background, so that the content displayed on the Live Activity can be updated according to the running status, but this may not be applicable to most of the Apps, except for navigation and audio playback Apps, it is difficult to apply for the permission to run continuously in the background.

Then the only way left is to use Push Notification to update.

Authentication method for APNs

The traditional way to connect to APNs is to use certificate-based authentication, but in connections using certificate-based authentication, even with the additional information needed to update the Live Activity, you will still encounter the TopicDisallowed error, as found in a post in the Apple Developer Forum (https://developer.apple.com /forums/thread/712499) mentions that such notifications require the use of Token-Based authentication to connect to APNs.

Token-Based Connection to APNs

You can find how to establish a Token-Based connection to APNs in Apple’s developer documentation: Establishing a Token-Based Connection to APNs.

Note that the Key required for Token-Based authentication is not the same as the Key generated by the original certificate authentication, the Key required for Token-Based needs to be generated again in the Apple developer backend, and the same Key can be used for both production and Sandbox environments, which needs to be secured.

How to get the Key for authentication is also mentioned in Apple’s documentation, which can be created by simply filling in a name.

Get the Push Token

It should be noted that the Push Token used for Live Activity push is not the Device Token obtained when registering through UIApplication at the time of App launch, you need to obtain the Push Token for Live Activity push notification through the object obtained when creating Live Activity separately.

var activities = Activity<GroceryDeliveryAppAttributes>.activitiesactivities.forEach { act in
if let data = act.pushToken {
let mytoken = data.map { String(format: "%02x", $0) }.joined()
print("act push token", mytoken)
}
}

The pushToken property of the Activity object allows you to get the Push Token that you need to use for the Live Activity push.

Note: The pushToken of the Activity object is not generated instantly when the object is created, it needs to wait for some time before it has a value, so you need to wait or monitor pushTokenUpdates to get a valid value.

Generate Authentication Token for APNs

To test sending push notifications, I initially found a Mac app to test, but it was not adapted for Token-Based authentication, and Token-Based authentication uses JWT to generate Token, so for testing purposes, I found an article on Medium that provides a script to quickly generate the requested Payload and send it to APNs.

The following script is from iOS 16 Live Activities: Updating Remotely Using Push Notification.

export TEAM_ID=YOUR_TEAM_ID
export TOKEN_KEY_FILE_NAME=YOUR_AUTHKEY_FILE.p8
export AUTH_KEY_ID=YOUR_AUTHKEY_ID
export DEVICE_TOKEN=YOUR_PUSH_TOKEN
export APNS_HOST_NAME=api.sandbox.push.apple.com
export JWT_ISSUE_TIME=$(date +%s)
export JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
export JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"

Generate Payload and Send Push

After obtaining the AuthKey and generating the APNs Token, you can start to actually send push notifications to the Live Activity.

curl -v \
--header "apns-topic:YOUR_BUNDLE_ID.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data \
'{"aps": {
"timestamp":1666667682,
"event": "end",
"content-state": {
"courierName": "Iron Man",
"deliveryTime": 1666661582
},
"alert": {
"title": "Track Update",
"body": "Tony Stark is now handling the delivery!"
}
}}' \
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN

The event here can be update or end, and the specific format of the Payload can be found in the Apple developer documentation Updating and ending your Live Activity with remote push notifications.

In addition, the content-state in the Payload is consistent with the ContentState of the ActivityAttributes in the Live Activity, so it is recommended to use simple atomic types, such as String or Int, for the attributes here to make it easier to generate Payloads. This makes it easier to generate Payloads.

After doing this, you can officially send push notifications to the Live Activity, which is done after running the above curl command.

Update the UI of Live Activity

Unlike traditional home widgets, there is no method in the Live Activity to let us feel that we have received a push notification and to update the content of the current Live Activity.

In fact, this is all automatic. When a notification is received, the system will automatically map the content-state in the Payload to the ContentState of the ActivityAttributes, and regenerate the body of the widget through ActivityConfiguration, thus updating the Live Activity’s interface will be updated.

How to maintain Live Activity update history

During the testing process, I came across a requirement that I may need to keep the history data from push notifications so that I can maintain a list to show the status of multiple orders, but the server can’t maintain such a list directly and can only push the status for individual orders, so I can do something on the Live Activity side.

We can save the ContentState during the Activity generation process and de-duplicate it with a unique key to get a history list.

var orders = [String]()
if let oldOrders = UserDefaults.standard.array(forKey: "current_orders") as? [String] {
orders = oldOrders.filter { $0 != context.state.courierName }
}
orders.append(context.state.courierName)
UserDefaults.standard.set(orders, forKey: "current_orders")

Here, UserDefaults are used to keep a history of received ContentState and are de-duplicated by a unique attribute, so that the list can be displayed during subsequent interface generation.

Conclusion

In general, developing and pushing updates to Live Activity and Dynamic Island is not very complicated, but since we generally use third-party push services to send push notifications during development, and at this point in time, none of the third-party services are yet adapted to Live Activity, so if we need to update Live Activity in the background but the app itself does not have permission to run in the background, we need to set up the relevant push services and connect with them, which requires some extra development work and cost.

I hope this article will be helpful to you.

Follow me to get more iOS development articles :)

如果需要中文版本: https://xujiwei.com/blog/2022/10/update-dynamic-island-and-live-activity-with-push-notification/

References

--

--