On this page 10 sections
- Permission is earned, not grabbed on first launch
- The types of push, and which ones you actually need
- Registering, tokens and uploading them safely
- Payloads, categories and actions
- Rich notifications and notification service extensions
- Focus, scheduled delivery and interruption levels
- What the server must get right
- Testing on a device and in production
- Key takeaways
- Frequently asked questions
Apple’s User Notifications framework and the APNs documentation are the source of truth for the APIs. This guide is the practice around them: permission, tokens, payloads, rich notifications, Focus, and the server work a small app cannot skip. It is not a “hello world” that fires a test banner and stops.
Permission is earned, not grabbed on first launch
requestAuthorization shows a system prompt that, once refused, is painful to reverse. The user has to leave the app, find Settings, and change the switch. That is not a funnel; it is a dead end with extra steps.
Ask when three things are true:
- The user has seen the feature the notification would serve (an order, a message, a reminder they just created).
- You have explained, in your own UI, what you will send and how often. This is not a second system prompt; it is a screen or a paragraph with a clear button.
- You have a reasonable first notification to send later, not a “thanks for signing up” toast.
Provisional authorisation (UNAuthorizationOptions.provisional) can deliver notifications quietly to the Notification Centre, without a prompt, on systems that support it. It is useful for low-urgency updates if you are genuinely happy for them to be quiet. It is not a way to sneak around a “no”. If the user then promotes or denies you from a delivered notification, honour that.
Request only the options you need: .alert, .badge, .sound, and .providesAppNotificationSettings if you have an in-app settings screen, which you should. Time-sensitive and critical alerts have extra requirements (a capability, and in the critical case an entitlement Apple grants). Do not request them because they sound important.
If the user declines, keep the app fully usable and offer a path to Settings only from a place where a notification is clearly relevant. Asking again from applicationDidBecomeActive is how you train people to refuse you.
Mention notifications in your privacy policy and in the permission purpose, in line with the same honesty I argue for in privacy manifests and App Store review. You are not required to invent a custom usage string for push the way you are for the camera, but Review and users both react badly to a prompt that appears for no stated reason.
The types of push, and which ones you actually need
Not every wake-up should be a banner.
- Visible alerts are for something the user would have wanted to know soon: a message, a delivery update, a reminder they set. This is the default people mean by “push”.
- Badges are a count, and they go stale. If you badge, you also need a path to clear the badge when the user has seen the items, including on other devices if you can.
- Sounds should be rare. A custom sound is a branding exercise that becomes noise.
- Time-sensitive interruptions can break through scheduled delivery and some Focus settings, and Apple expects them to be reserved for things that genuinely cannot wait. Abuse this and you will earn a Focus break or a disabled permission, not attention.
- Critical alerts are for specific, granted use cases (health, security, and the like). A typical indie app does not have this entitlement and should not plan as if it did.
- Silent, content-available pushes wake the app to fetch, with a budget, and are not a substitute for
BGTaskScheduleror a guaranteed background refresh. They need a server, and they can be delayed or dropped. Use them when a server event should update content without a banner, and still have a visible path if the user needs to know. - Live Activities and widgets are different surfaces. If the job is a live score or a delivery progress view already on the Lock Screen, look at widgets, App Intents and Live Activities rather than stacking more banners.
Most small apps need one visible category of alert, maybe a badge, and nothing else on day one. Each extra type is a policy you have to justify in settings and in the payload.
Registering, tokens and uploading them safely
After you have authorisation (or when you are using a path that still needs a token), call UIApplication.shared.registerForRemoteNotifications() and implement the AppDelegate or scene callbacks for success and failure. The token is Data. Send it to your server as hex or a stable encoding, tied to the signed-in account, and replace it whenever the system gives you a new one. Tokens rotate. A token you stored last year may now address nobody, or worse, you may think it still addresses this user.
import UIKit
import UserNotifications
func startNotifications() async {
let centre = UNUserNotificationCenter.current()
let settings = await centre.notificationSettings()
guard settings.authorizationStatus == .authorized
|| settings.authorizationStatus == .provisional else {
return
}
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}
// In the app delegate:
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
Task { await TokenClient().upload(token) }
}Practical rules:
- Upload over HTTPS, authenticated as the user, and treat the token as a secret that can send a message to a person’s phone. Do not log it in full in production, and do not put it in a crash report.
- Replace, do not only append. One current token per device, and drop tokens APNs tells you are gone.
- Register again on each launch (once you have permission). The callback is how you hear about a new token.
- Sandbox and production tokens are different environments. A development build talks to the sandbox APNs environment; a TestFlight or App Store build talks to production. Sending a sandbox token to production, or the reverse, looks like “push is broken”.
If you do not have a server, you do not have remote push. Local notifications can still cover reminders the device can schedule itself. Do not add a server only to send a weekly marketing notification; that is how permission dies.
Payloads, categories and actions
The APNs payload is JSON. Keep it small, keep aps correct, and put your own keys next to it, not inside aps unless the documentation says so. When you are shaping a fixture, tidy it with the JSON formatter and compare it to Apple’s payload examples, rather than guessing at typo’d keys.
A minimal visible payload looks like this:
{
"aps": {
"alert": {
"title": "Order on the way",
"body": "Your order from North Harbour Books has left the shop."
},
"sound": "default",
"thread-id": "order-A-1024",
"category": "ORDER_STATUS",
"interruption-level": "active",
"relevance-score": 0.6
},
"orderId": "A-1024"
}thread-id groups related notifications. category matches a UNNotificationCategory you registered at launch, with actions such as “Open order” or “Snooze”. Actions should be things the user might want to do from the banner; they should not be the only way to do something important, because not every delivery will be acted on.
mutable-content: 1 is what lets a Notification Service Extension rewrite the payload, usually to attach an image or to decrypt a payload you did not want sitting on a server in plain text. content-available: 1 is the silent wake. They are not interchangeable.
Put deep-link identifiers in the custom keys (orderId above) and route them through the same navigation path the rest of the app uses. A notification that opens the app on the root screen is a wasted permission.
Rich notifications and notification service extensions
A rich notification is usually an image, a short GIF, or a custom notification UI via a Content Extension. The Service Extension runs for a short time when a notification arrives, downloads or decrypts, attaches a UNNotificationAttachment, and must call the content handler before it expires. If it times out, the original payload is delivered instead. That is the correct fallback; design the banner so it still makes sense without the image.
Limits worth respecting, without treating informal numbers from old blog posts as a contract:
- Keep the attachment small and in a format Apple lists as supported.
- Do not do a full app sync in the extension. It is not a background task.
- Handle a failed download quietly. A notification with a title and body is better than no notification.
- If you decrypt in the extension, the keys and the failure path need the same care as the rest of your privacy story.
A Content Extension customises the expanded UI. It is optional. Many good notifications never need one. If you add it, keep actions available and do not hide the meaning of the banner behind a layout that only appears when the user long-presses.
Focus, scheduled delivery and interruption levels
Focus modes, scheduled delivery and interruption levels are how iOS protects attention. Your payload should cooperate.
Interruption levels are, in increasing urgency: passive, active, time-sensitive, and critical. Most traffic should be active or passive. Passive does not light up the screen or play a sound; it is appropriate for a low-priority update. Time-sensitive must be reserved for what the name says. Critical requires the entitlement.
Scheduled delivery can hold notifications until a summary moment the user chose. You cannot buy your way out of that with volume. If something must break through, it has to qualify as time-sensitive (or critical), and it must still be honest.
Respect a denial, a Focus that is silencing you, and a user who has turned off the lock screen, banners or sounds independently in Settings. Query UNNotificationSettings if your in-app settings screen needs to explain why something will not appear, and include providesAppNotificationSettings so the system’s settings can deep-link to you.
Do not measure “success” only as delivery. A notification that is delivered and then causes the user to turn you off is a failure of the product, even if APNs returned 200.
What the server must get right
The app cannot make up for a sloppy provider. A small, correct server does the following.
It signs a JWT with your APNs key (.p8) and the key ID and team ID from your developer account, as Apple documents for token-based provider authentication. That JWT is an ES256 token with a short lifetime; you create a new one periodically, you do not invent a custom signing scheme. When you are debugging a token, a JWT encoder and a JWT / JWKS verifier will help you see the header and claims; they do not replace Apple’s documented claims (iss, iat, and the right algorithm). Never commit the .p8 file, and never paste a production token into a tester.
It sends HTTP/2 requests to the correct APNs host for the environment, with the device token in the path, the topic (usually the bundle ID, with suffixes for VoIP or complications if you use those), and a reasonable apns-priority and apns-push-type. alert and background are different types; mixing them up is a common “silent push arrived as nothing” bug.
It handles responses. A 410 and a gone token should be deleted. A 400 with a reason should be logged and fixed, not retried blindly. A 429 or a 5xx should be retried with backoff. Do not retry a 400 bad-token in a tight loop.
It collapses where it should: apns-collapse-id for “the latest status of this order”, so five courier updates do not stack as five banners.
It does not send marketing blasts through the same path you reserved for order updates without a clear settings toggle. Prefer one topic the user recognises, and let them turn it off in your app as well as in Settings.
Testing on a device and in production
Push does not work fully on the simulator in the way a device does, even though recent Xcode versions have improved tooling around simulated notifications. Test on a device, on both a development build (sandbox) and a TestFlight build (production), with two different tokens.
Useful checks:
- Permission granted, denied, and (if you use it) provisional, including the path to Settings.
- A token is uploaded, rotated, and replaced after a reinstall.
- A visible notification arrives in the foreground and the background, and your
UNUserNotificationCenterDelegatepresents it as you intend. - Tapping it opens the right screen, including a cold start.
- A category action runs, including when the app is not in memory.
- A service extension attachment appears, and a timeout still delivers the text.
- A collapsed identifier replaces, rather than stacks.
- A production TestFlight send uses the production environment and a production token.
Xcode can also send a test payload to a running app. Use that to test the client path; it does not prove your server’s JWT, host or token store. After launch, send one real notification to yourself on the production path before you announce the feature, and watch APNs responses rather than assuming a 200.
Then, after a few weeks, look at whether people who accepted the prompt still have notifications on, if you can do that honestly from settings or support. That is the metric that matters. Shipping is only the start; the same after-launch maintenance habit applies here as to any other feature.
Key takeaways
- Ask for permission in context, after you have explained what you will send. Do not prompt on first launch.
- Keep one current device token per device on a server, and treat sandbox and production as separate environments.
- Use a small, valid
apspayload, with thread and category when they help, and route taps through the same navigation as the rest of the app. - Service extensions should be fast and fail open to the original text. Most apps do not need a content extension on day one.
- Default to active or passive interruption. Reserve time-sensitive for events that truly cannot wait, and honour Focus and scheduled delivery.
- Sign APNs with a
.p8JWT, handle gone tokens, collapse updates, and test on a device in both environments.
If you want a second pair of eyes on a permission flow or a provider, tell me about it.
Frequently asked questions
When should an iOS app ask for notification permission?
After the user has seen the feature a notification would support, and after you have explained in your own UI what you will send. The system prompt should be the confirmation, not the first mention. A first-launch prompt is the usual way to be refused.
What is the difference between an alert and a background push?
An alert is a visible notification, with a title and body, that can show a banner, play a sound or set a badge. A background, content-available push wakes the app to fetch or update state, may be delayed or dropped, and should not be used as a silent substitute for a message the user needs to see. They use different apns-push-type values and different payload keys; do not mix them up.
How do you handle APNs device tokens in Swift?
Implement the registration callback, convert the Data token to a stable encoding such as hex, and upload it over HTTPS, authenticated as the user. Register again on later launches so you receive rotations, replace the stored token for that device, and delete tokens APNs reports as gone. Keep sandbox and production tokens in separate environments.
How do provisional notifications work?
If you request .provisional authorisation, and the system grants it, notifications can be delivered quietly to Notification Centre without an up-front prompt. The user can then keep them, turn them into prominent delivery, or turn them off. Provisional delivery is for genuinely low-urgency updates, not a way to avoid a refusal. Honour the result as you would any other authorisation change.
Why do users turn off iOS app notifications?
Usually because the prompt arrived too early, or the messages were too frequent, too promotional, or not about something they had asked to be told. Fighting Focus, misusing time-sensitive delivery, stale badges and notifications that open the wrong screen all contribute. The fix is fewer, better messages, a clear in-app setting, and a product that still works with notifications off.
Written by Lee Johnson. Published 10 November 2026.