iosNudge

How do push notifications work on iOS, end to end?

MediumAsked oftenApp lifecycleNetworking 2 min read

TL;DR

  1. The app asks the user for permission (UNUserNotificationCenter).
  2. The app registers with APNs (Apple Push Notification service) and gets a device token.
  3. The app sends the token to your server.
  4. Your server sends a JSON payload plus the token to APNs.
  5. 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: notification

Step 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 outside aps.
  • 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) and apns-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 .apns file onto the Simulator, or use xcrun 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 UNNotificationRequest and 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: liveactivity and a special push token from ActivityKit.