Start a project

Article · iOS · Push · UserNotifications · 13 min read

iOS push notifications that users keep on

A push notification that users keep enabled is not a technical trick. You ask for permission after the app has shown why a notification would help, you register for a device token and keep it current on a server that talks to APNs correctly, and you send a payload the system can deliver without fighting Focus or burying the person in noise. A prompt on first launch, a token you never refresh, and a stream of low-value alerts are the usual ways to end up with permission denied and no second chance that feels honest.

On this page 10 sections
  1. Permission is earned, not grabbed on first launch
  2. The types of push, and which ones you actually need
  3. Registering, tokens and uploading them safely
  4. Payloads, categories and actions
  5. Rich notifications and notification service extensions
  6. Focus, scheduled delivery and interruption levels
  7. What the server must get right
  8. Testing on a device and in production
  9. Key takeaways
  10. 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:

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.

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:

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:

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:

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

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.

All articles · Swift A–Z · Apps

Questions, or an app in mind?

Let's bring your ideas to life. Whether you're envisioning a game-changing Apple Watch app or a transformative iOS experience, I'm here to make it happen.

Start a project