Debugging Notification delivery issues
Can you send Live Activity notifications to simulator?
Yes. The token created by the simulator works. Just don’t use the simulator for testing application lifecycle behavior or background tasks as a may not properly simulate OS restrictions.
Can you drag and drop payloads into the simulator?
That didn’t work for me.
Timing of ‘Token Registration’ vs ‘Payload Sending’
Make sure the tokens are stored in your servers before sending the payload. This must be done in a timely manner.
You should not attempt to request/store token just before you want to send a payload.
Bad: You decide to begin a live activity. Then request start token. Then wait till it’s stored. Then start the live activity. Good: You store a start token as soon as app launches — irrespective of needing to fire a live activity event. Then whenever needed, you just start the live activity without caring if the token is stored or not.
Your token registration on the server must be decoupled from the firing of the events. Otherwise your server will be sending events where tokens may not be stored / is just about to get stored. It’s the same case for update tokens or even regular apns tokens.
Validate that your app has ‘Live Activity’ enabled
Settings » Apps » Your_App » Live Activities » Validate
If you can’t see ‘Live Activities’ then it means something is misconfigured.
How to correctly validate your json?
When you’re using Apple Push Notification Console, make sure you’re not using a timestamp
far in the past. 1-10 seconds is ok, but 50-200 seconds or more may cause the notification to not get sent.
- Attention: The console takes 2-3 seconds to figure out that your json is invalid. If you hit send before it errors out, then it would just send the last valid json. Or if you just pop back to the ‘fields view’ (as opposed to the json view) then it would NOT give you an error that the json was incorrect. You would be hitting send, thinking the json was updated, but it just defaults back to the last valid payload.
tldr make sure the json is correctly formed.
Which fields are necessary for a json payload to be valid?
For a start
event you need the following items:
- Set the value of the
event
field to"start"
. - Include the
attributes-type
andattributes
keys along with their necessary values. This is so that your app would know which type it must use to decode the payload. - Include an
alert
in the JSON payload. The alert portion has multiple purposes:- A payload with an alert, will have a sound and present the live activity in expanded mode.
- Its value are used for:
- Older iPhones that don’t have Live Activity feature.
- Apple Watches. See here for how that works.
Note: The alert isn’t a fallback for devices that know which have disabled Live Activities. Because once Live Activity is disabled, you won’t be getting start,update tokens any more. Whatever tokens you had would become invalid.
This is why it’s critical for your servers to know if a user has Live Activity Enabled or not. If they don’t then you should fallback to regular notifications.
The Apple Docs’s sample payload uses an incorrect payload. Its “attribute” field should match the static portion of the ““ActivityAttributes”"Update: Apple fixed this on June 2025 after I filed a bug report.
And like any other live activity payload you also need:
- The
contentState
- The
timestamp
Also note, I mainly used the Push Notification Console. Didn’t send jsons from command line or actual server. There are more nuances if you do it that way.
How can I be sure my encoding is done correct?
If your encoding is incorrect then the notification won’t get delivered.
Open a playground and just create the encoded json. Don’t try to do it manually by yourself. I was using an enum that had an associated value, the encoded wasn’t what I expected. For more on that see here
You can use the following wwdc code to get the json:
/// - Note: initialize your content accordingly.
let contentState = AdventureAttributes.ContentState(
currentHealthLevel: 0.941,
eventDescription: "Power Panda found a sword!"
)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try! encoder.encode(contentState)
print("\(String(data: json, encoding: .utf8)!)")
💡 Some common mistakes and solutions 💡
Assuming that the Apple Push Notification Console said the notification was delivered then there can be:
Mismatch at Apple’s Live Activity Schema
Example you may be missing timestamp
or contentState
or attributes
, some necessary headers, etc.
Mismatch at your own definition under attributes
and content-state
Main filters to find reasons for failure under Console app are:
- The name of your app’s type that conforms to
ActivityAttributes
. This is because the OS attempts to decode to that type. liveactivities
- set
messageType
to error. But not ever error is flagged as an error. So you might want to remove this
Try some combination of the above to narrow things down.
Some errors I found when I forgot to include a field named trackerId
under my attributes
💡 “Received Event for unknown activity”
💡
Task [22] [com.company.productName::com.company.productName.MyCoolLiveActivity:Attributes type: MyCoolLiveActivityAttributes:BD56CBD6-D99B-4067-A610-961EBB7CB1E2] Encountered missing entry
💡 MyCoolLiveActivityAttributes:C38D2A41-A899-4046-B154-EFEC66C504B8], contentIdentifer: [w:fix-365.00-h:dyn-64.00-160.00-cr:23.5-s:1.0.fam:medium], destinationURL: nil, underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=4865 “No value associated with key CodingKeys(stringValue: “trackerId”, intValue: nil) (“trackerId”).” UserInfo={NSDebugDescription=No value associated with key CodingKeys(stringValue: “trackerId”, intValue: nil) (“trackerId”
- Passing an epoch with the type of String for
dismissal-date
/stale-date
instead of an Integer. For those the notification still arrives. Only that their values have no effect. As if they’re not included.
Note: It’s better to avoid redirecting the structure of ActivityAttributes
or ContentState
to another object. It’s unneeded and potentially confusing. Example:
Is there a limit to how many events I can send?
Yes. There’s a fixed amount budget that your app gets. You’ll get throttled if you don’t have any budget left.
Quoting from WWDC2023 - Update Live Activities with push notifications:
Using correct priority [for the
apns-priority
header] ensures you have budget left when you need it.
The priority you should always consider using first is low priority. Low priority updates are delivered opportunistically, which lowers the impact on the user’s battery life. However, this means the Live Activities might not be updated immediately when the push request is sent. So you should use low priority for updates that are less time-sensitive. Another benefit of using low priority is that there is no limit on how many updates you can send. In order to take advantage of this, you should be using low priority for the majority of your Live Activity updates.
On the other hand, certain updates require the user’s immediate attention, in these cases, choose high priority updates. High priority updates are delivered immediately. That’s why they’re perfect for time-sensitive updates. However, due to their impact on the user’s battery life, the system imposes a budget depending on the device condition. [To be able to inspect the true impact of notifications on your budget, try testing mac os console wirelessly and with a low battery or maybe even with “low power mode”.]without having the device plugged in
If your app exceeds its budget, the system will throttle your push updates, and it will dramatically impact your user experience. If you need the server to send high-priority pushes frequently to keep it up to date then add the
NSSupportsLiveActivitiesFrequentUpdates
flag to your plist with aYES
value. Users can disable frequent updates independently of Live Activities in Settings. So you can must detect the status of the frequent updates feature by accessing theActivityAuthorizationInfo
frequentPushesEnabled
property.
OS suppressing due to budget (or other) reasons
If all that didn’t work and you still need to log at other issues then, see Apple Developer’s Answer - Rico
Connect your phone to the macOS and sift through the logs by:
Filter by Budget and if you see :
priority(0)
,budget(0)
orrunning-not-visible
, the system has decided to not show your activity. Some reasons why this may occur:
- low battery mode
- bad data in the push(es)
- too many pushes in a short period of time
To fix the budgeting issue, you may be able to remedy this by using NSSupportsLiveActivitiesFrequentUpdates
which then gives you more budget.
Validate that your app has ‘Live Activity’ enabled
Settings » Apps » Your_App » Live Activities » Validate
If you can’t see ‘Live Activities’ then it means something is misconfigured.
Getting more information from the macOS Console
.
You can debug much of this using Console.app. The behavior is not entirely consistent across OS versions but nonetheless you can debug to see if your process has lost budget or something else leading to your token not being sent.
A good way to start would be with filtering by the BundleID of the target and / or the target name itself (the process name).
Also be sure to check the system processes for additional information. This will vary based on what and where you are looking but some examples:
- To debug APNS you’ll want to monitor
apnd
along with other relevant processes. - To debug WidgetKit, LiveActivities, Dynamic Island you’ll want to monitor
springboardd
,liveactivitiesd
along with other relevant processes.
A Daemon is a background Process without any user interface. To find a comprehensive list of Apple Daemons see this unofficial list.
Notification delivery in Debug builds
Debug builds may tamper the delivery of notifications.
the debugger loads with a development profile and is not exactly the same, particularly with notification delivery.
from forums
I’ve experienced scenarios were push notifications didn’t deliver and I had to switch from WiFi to Cellular or restart my iPhone or changes networks. Not sure if the root cause was that I was using a debug build or not 🤷.
Code after for loop await may never run!
As someone who’s not super savvy with async await I learned that:
for await ptsToken in Activity<DeviceActivationAttributes>.pushToStartTokenUpdates {
...
}
doSomething()
In this setup, doSomething()
is never called.
A friend explained that this is because pushToStartTokenUpdates
conforms to AsyncSequence
. An AsyncSequence
provides an AsyncIterator
, which exposes a next()
method that asynchronously returns the next element in the sequence as an optional (Element?
). The loop continues until next()
returns nil
, signaling the end of the sequence. Since the for await
loop waits for the sequence to finish, and this particular sequence never ends, execution never reaches doSomething()
.
This Avander Lee article describes how that works with some more detail.
For what it’s worth, even if the sequence emits a finite number of values, it can still take a very long time to complete. For example, if new values are emitted every 30 minutes, it would take 2.5 hours for the loop to finish processing 5 values.
In the case of push-to-start token updates or token updates stream, the sequence emits an indefinite number of values, meaning it never ends. As a result, your for await loop will keep running indefinitely. This also means that token updates can trigger your app to launch or resume in the background at unpredictable times, depending on when those updates occur.
Other notes
How can you extract the key from the p8 file?
It’s just plain text. There’s no decryption required. You can open it with TextEdit
app.
What should I do if my images don’t appear in the leading / trailing views?
- Images have to be less than
4kb
and shouldn’t exceed45x36.67
points. - Images can often not be hard to see, because of their color. Had to do something like:
Image(contentState.imageName)
.renderingMode(.template) // this was critical for resizing
.resizable()
.frame(width: 40, height: 40)
.foregroundStyle(.yellow)
I can’t see logs for the widget. What can I do?
Use OSLog
and sift through the logs.
Generally I have a hard time getting logs from Xcode for App Extensions. But easy to use from macos Console app.
I’m struggling up to come up with a good design. What should I do?
In short Apple suggests that the design should be bold, unique and glanceable. The - WWDC 2023 - Design dynamic Live Activities is a fantastic talk. If you have doubts, then try to take inspiration from Apple Clock, Uber, Apple Maps or Google Maps. You can find a nice variation of design among them.
I can’t find the answer I’m looking for. What should I do?
There’s lot of other stuff mentioned in the forums. You really have to go digging. I’ve also even gone far as to open a DTS ticket. Getting support from an Apple Engineer for $70 is great value for your money. Especially that you can send them code where they can take a look at a later time and get back to you. Only note is, each time it takes 4-7 days for them to get back to you. So make sure you include all the necessary details.
Shout outs
Special shout out to Kevin Lee who reviewed the series, recommended that I upload my blog to iOS Dev Weekly to boost my reach and has always been helping me push forward. I appreciate his kindness, energy and good vibes.
Series Summary
Understanding the architecture is possibly the most complex. To know how to react to callbacks in a way that works for when the app is launched into background vs how to react to callbacks that just resume the app after it was suspended. Also figuring how the server architecture is a challenging task for engineers. Hopefully the series gave you some ideas on how to tackle these problems.