Permission and registration
iOS shows the “Allow notifications?” dialog once. If the user says no, the app can’t ask again; the only way back is the Settings app. So the moment you ask matters more than anything else:
- Bad: on the first screen, before the user knows why.
- Good: when the user does something that needs it, like tapping “Notify me when my order ships” or turning on reminders. Show a short explanation in your own UI first, then the system dialog.
Separately from permission, the app registers with APNs to get its device token. That should happen on every launch, because the token can change (after restoring from a backup, reinstalling, or for other reasons iOS decides).
UNUserNotificationCenter.requestAuthorization(options:) presents the prompt once per install; later calls return the stored decision. Check notificationSettings() first (.notDetermined, .denied, .authorized, .provisional, .ephemeral) to choose between a pre-prompt, the system prompt, or a Settings deep link (UIApplication.openNotificationSettingsURLString). Provisional authorization (.provisional) skips the prompt and delivers quietly to Notification Center with Keep/Turn Off buttons, a good default for content users haven’t opted into yet. Registration (registerForRemoteNotifications()) is independent of authorization (silent pushes work without it) and should run every launch; tokens are per app per device per environment (sandbox vs production) and must be uploaded as hex.
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = NotificationRouter.shared // before launch finishes
application.registerForRemoteNotifications() // every launch
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
Task { try? await DevicesAPI.shared.register(token: token) } // upsert on the server
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
Logger.push.error("APNs registration failed: \(error)") // e.g. on a simulator without push
}
}
// Called when the user taps "Notify me when it ships"
@MainActor
func enableShippingUpdates() async -> Bool {
let center = UNUserNotificationCenter.current()
switch await center.notificationSettings().authorizationStatus {
case .authorized, .provisional, .ephemeral:
return true
case .notDetermined:
return (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
case .denied:
showSettingsPrompt() // "Notifications are off. Open Settings?"
return false
@unknown default:
return false
}
}- The delegate is set in
didFinishLaunching, before anything else, so taps that launched the app are handled (more on that below). registerForRemoteNotifications()on every launch; iOS calls back with the token, and the app sends it to the server, which updates or inserts it.enableShippingUpdateschecks the current status first: already allowed (done), never asked (show the system dialog now, in context), or denied (offer Settings instead of a dialog that won’t appear).
Predict the output
The user tapped 'Don't Allow' on the first launch. A week later, they turn on 'Order updates' in the app, and the app calls requestAuthorization again. What does the user see?
iOS shows the authorization prompt once per install. Later calls return the stored decision (false) without any UI, so the toggle seems to do nothing. Check notificationSettings() first: if the status is .denied, explain that notifications are off and open the app's notification settings, where the user can turn them on.
- One prompt per install: check
notificationSettings()→ pre-prompt + request (.notDetermined), Settings deep link (.denied). Ask in context. - Provisional authorization for quiet delivery without a prompt.
registerForRemoteNotifications()every launch, independent of authorization; upload hex tokens (environment-specific) as an upsert.- Set the
UNUserNotificationCenterdelegate beforedidFinishLaunchingreturns.
Check this part
How many times can an app show the system notification permission dialog?
Why register for remote notifications on every launch?
What does provisional authorization do?