Development
How can I share classes between widget and app?
Models must be shared across targets.
- App needs the model to manage its lifecycle (start, update, end, handling dismissed / stale activities).
- Widget needs the model to be able to present it.
Each View should only get added to targets that needs them.
- Your widget must create views based on the models for all Live Activity presentations.
- Your app may need views if it has an in-app view related to the live activity.
Note: You can’t import / link libraries that use shared
or libraries that use certain APIs. For a workaround see here
Do I need to do anything after a payload is delivered?
- The Live Activity will get automatically updated. So nothing needed to do for that.
- The in-app representation of the activity, will need to get updated either by app querying the server or reacting to callbacks from the updates.
Previews are a huge pain!
I constantly had to check the project build logs and the preview error. Also the app target seems to be built even when I though it’s not a dependency.
Check out this list for ways to fix SwiftUI previews
What are different ways to start a live activity?
- Creating a request from the main app while app is in foreground.
- Creating a
start
event from server. - Starting from an App Intent.
Token
Token variances and how to acquire them. Frequency of change
Token | Usage / Scope |
---|---|
regular push notification tokens | Unique per app |
push-to-start token (for Live Activity) | Unique per activity type. Invalidated upon usage |
update token (for Live Activity) | Unique per activity instance |
They are NOT interchangeable. For more on that see here
Why can’t we just use the regular apns token?
- Apple wants to control the frequency of start / updates and the lifecycle of the live activity.
- Apple wants to control it at the APNs level, not at the iOS level. As a result the client won’t have to waste any of its networking, battery to receive a notification only to have to then not present it to the user. Instead the server would just protect the device from such waste.
My guess is that the OS communicates with Apple’s servers, saying things like: “Hey, my battery is low, hold off on sending notifications” and later, “Battery’s good again, you can resume.”
Subscription for push-to-start token updates
- Observe changes on the
static pushToStartTokenUpdates
asynchronous sequence.
Subscription for push-update token updates
Can be done in two ways:
- Activity Started Locally: Retrieve the activity instance upon creation. Subscribe to the activity’s
pushTokenUpdates
// You can find similar example from Apple Example App: https://developer.apple.com/documentation/widgetkit/emoji-rangers-supporting-live-activities-interactivity-and-animations
// Uses https://developer.apple.com/documentation/activitykit/activity/pushtokenupdates-swift.property
func observeActivity(activity: Activity<OrderAttributes>) {
Task {
await withTaskGroup(of: Void.self) { group in
...
group.addTask { @MainActor in
for await pushToken in activity.pushTokenUpdates {
let pushToStartTokenString = pushToken.reduce("") {
$0 + String(format: "%02x", $1)
}
print("pushToStartTokenUpdates token: \(pushToStartTokenString)")
// Note: Each token is associated with a single activity
try await self.sendPushToUpdateToken(order: hotdogOrder, pushTokenString: pushToStartTokenString)
}
}
}
}
}
...
- Activity Started remotely: Listen to updates on the type (not instance).
Not mentioned in example app or wwdc sessions
// uses https://developer.apple.com/documentation/activitykit/activity/activityupdates-swift.type.property
Task {
// Listen for any updates from a Live Activity with
for await activityData in Activity<OrderAttributes>.activityUpdates {
for await tokenData in activityData.pushTokenUpdates {
let pushToStartTokenString = pushToken.reduce("") {
$0 + String(format: "%02x", $1)
}
print("pushToStartTokenUpdates token: \(pushToStartTokenString)")
// Note: Each token is associated with a single activity
try await self.sendPushToUpdateToken(order: hotdogOrder, pushTokenString: pushToStartTokenString)
} ...
}
}
Changes to react to
- Start Event: setup subscriptions
- Token updates: send tokens to server
- Permission changes in regards to Live Activity
- Content updates: update your in-app UI
- Other Life cycle changes: update your in-app UI
- active
- dismissed
- staled
- ended
Where should you place code that reacts to (token) updates?
So all items mentioned in the previous section are items that can occur when app is either in background / suspended or terminated.
- If app is backgrounded / suspended then subscriptions to updates are already placed in memory and will get callbacks immediately.
- If app is terminated then subscriptions are gone into the either. You have to redo all your subscriptions.
Note: Because you’re subscribed to token updates, then the app will get launched into the background. You need to make sure app properly resubscribes to all subscriptions after it’s launched.
Something like
func didFinishLaunching () {
// ALWAYS
handleGeneralAppLaunchFlow()
/// sends push-to-start tokens to server
observerPushToStartTokenUpdates_sendTokenUpdatesToServer()
/// sends update tokens to server
/// Also updates app's internal storage so once app is foregrounded the app's in-app UI can reflect the latest of the activity.
observeActivityUpdates_sendTokenUpdatesToServer_updateLocalStorageOfActivityToBeAbleToUpdateInAppUI()
// OTHER
setupOtherSubscriptions()
// ONLY IF FOREGROUNDED
if UIApplication.shared.applicationState == .active {
setupUtilitiesNeededForForeground()
}
}
func setupOtherSubscriptions() {
// some examples
setupBluetoothDiscovery()
setupLocationTracking()
setupPushNotificationDelegate()
other()
}
The key note is: Once your user needs to subscribe, then from there on, you want your subscription to carry on indefinitely. To carry subscriptions you have to re-do it upon any app launch (into foreground or into background). If you some functions / utilities that shouldn’t get launched until app is foregrounded then you can gate them as shown above.
Or as Quinn has put it:
The ‘obvious’ one is that your works needs to have some sort of checkpoint mechanism. That is, you need to periodically write your current progress to persistent storage so that, when you’re relaunched after a termination, you can continue your work [setup things as normal. Without this checkpoint, if the app gets launched into the background to code-paths that are different from the user-originated flows, then you’re not maintaining the app’s previous subscriptions. Your launch into background becomes broken due to lack of proper subscription to token / activity changes] from there.
The ‘obvious’ one is that your works needs to have some sort of checkpoint mechanism. That is, you need to periodically write your current progress to persistent storage so that, when you’re relaunched after a termination, you can continue you work from there.
How do I update the entire experience? What architecture should I have in place if for when notifications aren’t delivered or if app has bad internet and is not getting updated?
The Live Activity (notification) itself gets updated automatically. But for the in-app experience, you can get a hold of the activity, and upon changes, update your in-app UI. If your info is stale (due to network issues or just not receiving updates / notifications), then:
- Reflect a staled UI. You can use the
isStale
property to drive UI logic. Your app would get callbacks so you can update your in-app UI. - Then when a stale state is detected: query the latest with a regular HTTP Request.
Apple says:
The system will use this date [
stateDate
set in your payload] to decide when to render your stale view. from WWDC 2023 - Update Live Activities with push notifications - 16:44
Also from docs:
While setting the
staleDate
is optional, it’s helpful when you want to ensure your Live Activity doesn’t display outdated content. At the specified date, theactivityState
changes toActivityState.stale
andisStale
changes to true. AccessisStale
to monitor the activity state and respond to outdated Live Activities that haven’t received updates.For example, while a person has network connectivity, a sports app could update the Live Activity with the latest game information and advance the stale date. If a person enters an area without network connectivity, the app can’t update the Live Activity with new information and an advanced stale date. Eventually, the Live Activity becomes stale and displays text to indicate that the displayed information is outdated. On the next app launch or when [if] it performs background tasks, the app can also respond to the
ActivityState.stale
state.
To get the callbacks, you must be subscribed to activityStateUpdates
Any extra note about stale data?
Yes. If you missed a notification and used the stale date to query your server manually, then well, missing a notification may happen again. Your overall solution should be:
- Set a stale date in the payload
- Subscribe to changes
- If you got a callback that your live activity has gone stale: Manually query server
- Update the current Live Activity based on new information. Must set a new stale date.
- Do this again if you got another callback for a stale Live Activity
Basically your architecture has to be resilient against notifications not being sent indefinitely
By properly using the stale date, your architecture becomes “Notifications first, HTTP request second”. It gives you the best of both worlds. Makes you nicely server driven which means less operations on client, less polling and immediate updates, along with a regular HTTP request as a fallback.
You may also choose to to not use notifications at all to update the in-app experience and just rely on polling. What’s best for you depends on your needs. It’s just that the user may disable notifications or your tech stack has gone or not reliable or you’ve gone through all your Live Activity budget.
How to update a live activity while app is in background?
Approach | Pros | Cons |
---|---|---|
beginBackgroundTask(expirationHandler:) | Immediate | Only good for 30 seconds |
BGProcessingTask | Will be given enough time | Time of execution is unknown and not immediate. It could be in next 30 minutes or end of day when user is charging their phone. |
Sending Push Notifications | Processing is all done by server. Doesn’t require any app background processing | Requires server integration and token management. |
Note: Using
BGProcessingTask
doesn’t align well with the naming of ‘Live Activity’. Users and Developers perceive that as something that is updated in real time.
Live Activities can be used for two kinds of activities:
- Immediate: Uber ride
- Deferred: Syncing photos with the cloud for Google Photos
BackgroundTasks would be a terrible choice for an Uber app. Less terrible for a photo syncing app. Though your messaging has to properly match the deferred nature of the activity. Example:
- User starts a sync operation in foreground. The sync task is a 10 minute long task.
- User backgrounds the app right away.
- Live activity shows as “syncing paused” / “syncing paused - for immediate syncing open the app otherwise we’ll complete the sync by EOD”
- It should just act as a nudge to the user. But also if somehow the app did get some background operation time, then it will get processed.
Also see Background Task heuristics - how to increase chances of getting background time and BackgroundTasks testing from the forums.
All that said, I still can’t fully understand or recommend a proper use case for Background Tasks for Live Activity.
What would the architecture look for storing the push-to-start token? What if I had multiple live activities that needed to start at the same time?
This is a very good and complex question.
I haven’t developed this, but my understanding is that the server should maintain a single push-to-start token per type.
Type here means, the concrete type that conforms to the ActivityAttributes
protocol. Example a sports app can have:
GameScoreAttributes
: Brazil vs France game startedPlayerAttributes
: Ronaldo signs new partnership with Adidas worth 40M a year.TeamAttributes
: Arsenal has signed Messi
App should have three push-to-start
tokens at any given time
So for example, if user was only subscribed for a single match between two teams, then as soon as the app / OS realizes that the token is used for the GameScoreAttributes
related activity, then it will issue a new one.
- push-to-start token should be stored per type i.e. singular (per type), and is then refreshed as soon as its used (or other reasons for refresh)
- push update token is per activity
So if you have 5 live activities starting together, Like 5 soccer matches all starting at 12pm then:
- The server already has a single push-to-start token for the first game. It starts it.
- The OS realizes its push-to-start was used / invalidated. It then issues a new one.
- App is then backgrounded (either launched or just un-suspended).
- App must send the new push-to-start token to server. Note this only works if you properly setup subscriptions upon launching into background.
- Repeat steps 2-5 as many times as needed. Here it would be 3 more times. Making it a total of 5 push-to-start tokens.
There’s obviously a budget for the tokens. idk what the budget is. But it’s just always seems a good idea to also have NSSupportsLiveActivitiesFrequentUpdates
added to your plist. In a slack conversation a colleague said the plist only affects budget for updating live activity, not starting live activity from server.
If the user was also subscribed to a PlayerAttributes
, then the app would have a secondary push-to-start
token awaiting to begin as well.
How do you get the pushToken that is specific to the new activity that was started from a push-to-start
token?
I’m still actively developing this. But it should be either of the two choices. Not sure.
func subscribe() {
Task {
for await activity in Activity<AdventureAttributes>.activityUpdates {
for await token in activity.pushTokenUpdates {
🅰: sendToServer(token.hexadecimalString, for: activity.id) // this requires that apns to give you the activity ID upon getting the token on client or using the token with apns. Once you have that id, then you can associate things with it. I don't think either do that. But I could be totally wrong. Not sure.
🅱: sendToServer(token.hexadecimalString, for: activity.attributes.hero.name) // this requires your own API to have a field for the key. In the EmojiRanger example, `hero.name` is the key. Obviously it could be something more suitable like `hero.id` and then everything is based off that. This is very doable and doesn't require APNS to help you with identity.
}
}
}
}
Improvements
Tips for reusing views
Make your Leading / Trailing views customizable. Example:
struct LeadingView: View {
var isCompact: Bool
let imageName: String
var body: some View {
Image(isCompact ? "compactImage" : "expandedImage") // You can obviously adjust the frame as well. This is just some simplification.
}
}
Animations
See here for tips. For the most part, I’m leaving animations out of scope. With exceptions, there is a maximum 2 second duration for animations. Exceptions are if: Animation is done indefinitely for you when you use the following APIs / Frameworks:
- Animate countdowns by using
Text
countdown API
- Animate Audio bars by using
CallKit
(phone / voip calls) +MediaPlayer
(playing audio / video) which use the “Now Playing” feature. For more on that see here
Note: Apps that have extended background operation (Location Tracking, Audio Playing) can obviously update the app more frequently. But that doesn’t mean the animation duration will be different.
Ticking between activities
Apple suggested that there’s a way that you can tick (switch) between your live activities, but I wasn’t able to figure out. So I asked about it in the forums.
Hi, the word tick is misleading here. The only way to actually do something like this is via APNs, even then, you will get throttled trying to change the activity less than every 15 seconds.
Actions
- You can add actions with App Intent
- You can deeplinks using
widgetURL
orLink
. For more on that see Linking to specific app scenes from your widget or Live Activity
Open Questions
How to end a live activity?
I followed the docs to the best of my knowledge and attempted to use an event: "end"
in my payload along with a dismissal-date
. I was expecting the live activity to remain on the screen until its dismissal date. However the live activity was immediately ending. I’m not sure what the problem was. I ended up sending another event: "update"
with the last desired value, followed by another event: "end"
when I wanted the notification to get dismissed.