How do push notifications work on iOS, end to end?
MediumAsked oftenApp lifecycleNetworking 2 min read
TL;DR
- The app asks the user for permission (
UNUserNotificationCenter). - The app registers with APNs (Apple Push Notification service) and gets a device token.
- The app sends the token to your server.
- Your server sends a JSON payload plus the token to APNs.
- APNs delivers it to the device. iOS shows it, or wakes your app.
App -> APNs: register APNs -> App: device token
App -> Your server: token Your server -> APNs: payload + token
APNs -> Device: notificationStep 1 and 2: permission and registration
import UserNotifications
func setUpPush() async {
let center = UNUserNotificationCenter.current()
let granted = (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
guard granted else { return }
await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
}// AppDelegate
func application(_ app: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
Task { try? await api.registerPushToken(token) }
}
func application(_ app: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Push registration failed:", error)
}Tokens can change. Send the token to the server on every launch. Tip: ask for permission at a moment that makes sense to the user, not on first launch. You only get one system prompt.
Provisional authorization
try await center.requestAuthorization(options: [.alert, .provisional])No prompt. Notifications go quietly to Notification Center. The user can keep or turn them off later.
Step 4: the payload
{
"aps": {
"alert": { "title": "New message", "body": "Ana: See you at 6?" },
"sound": "default",
"badge": 3,
"thread-id": "chat-42"
},
"chatID": "42"
}- Max size: 4 KB for normal pushes.
- Custom keys (like
chatID) go outsideaps. - The server authenticates with APNs using a token key (.p8) (recommended, never expires) or a certificate (expires every year).
- The server sets headers like
apns-push-type(alert,background,liveactivity) andapns-priority.
Handling notifications in the app
final class PushDelegate: NSObject, UNUserNotificationCenterDelegate {
// App is in the foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async
-> UNNotificationPresentationOptions {
[.banner, .sound]
}
// User tapped the notification
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let info = response.notification.request.content.userInfo
if let chatID = info["chatID"] as? String { await router.openChat(chatID) }
}
}Set UNUserNotificationCenter.current().delegate early, in didFinishLaunching.
Without willPresent, notifications are not shown while the app is open.
Silent (background) pushes
{ "aps": { "content-available": 1 }, "syncID": "abc" }No alert. Wakes the app in the background to fetch data. Use apns-push-type: background and priority 5. The system throttles them and may skip some.
Rich notifications
- Notification Service Extension: set
"mutable-content": 1. You can change the content before it is shown, for example download an image or decrypt the text. You get about 30 seconds. - Notification Content Extension: a custom UI when the user long-presses.
- Categories and actions: buttons like "Reply" or "Mark as read".
let reply = UNTextInputNotificationAction(identifier: "REPLY", title: "Reply")
let category = UNNotificationCategory(identifier: "MESSAGE", actions: [reply], intentIdentifiers: [])
UNUserNotificationCenter.current().setNotificationCategories([category])Testing
- Drag a
.apnsfile onto the Simulator, or usexcrun simctl push <device> <bundle-id> payload.json. - Use the Push Notifications Console in the Apple developer website to send test pushes.
Common follow-up questions
- Local vs remote notifications? Local ones are scheduled by the app with
UNNotificationRequestand a trigger. Remote ones come from a server through APNs. - Is delivery guaranteed? No. APNs stores only the last notification per app for an offline device.
- Sandbox vs production APNs? Debug builds use the sandbox environment. TestFlight and App Store builds use production.
- How are Live Activities updated? With pushes using
apns-push-type: liveactivityand a special push token from ActivityKit.