```html SwiftUI: How to Implement Push Notifications (iOS 17+, 2026)

How to Implement Push Notifications in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: UNUserNotificationCenter Updated: May 11, 2026
TL;DR

Request authorization with UNUserNotificationCenter.current().requestAuthorization(options:), then call registerForRemoteNotifications() to get an APNs device token. Implement UNUserNotificationCenterDelegate via an AppDelegate to handle foreground and tap events.

import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        Task {
            let granted = try? await UNUserNotificationCenter.current()
                .requestAuthorization(options: [.alert, .sound, .badge])
            if granted == true {
                await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
            }
        }
        return true
    }

    // Foreground delivery
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                 willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        return [.banner, .sound, .badge]
    }
}

Full implementation

The approach below wires a full AppDelegate into the SwiftUI lifecycle using @UIApplicationDelegateAdaptor. The delegate handles all three notification lifecycle events — authorization, foreground display, and tap response — and surfaces state into the SwiftUI view hierarchy via an @Observable store so views can react to incoming notification payloads without singletons or notifications. For a production app you will forward the APNs device token to your server inside didRegisterForRemoteNotificationsWithDeviceToken.

import SwiftUI
import UserNotifications

// MARK: - Observable notification store

@Observable
final class NotificationStore {
    var lastPayload: [AnyHashable: Any]?
    var deviceToken: String?
    var authorizationStatus: UNAuthorizationStatus = .notDetermined
}

// MARK: - AppDelegate

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    let store = NotificationStore()

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        Task { await requestAndRegister() }
        return true
    }

    private func requestAndRegister() async {
        let center = UNUserNotificationCenter.current()
        do {
            let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
            let settings = await center.notificationSettings()
            await MainActor.run {
                store.authorizationStatus = settings.authorizationStatus
            }
            if granted {
                await MainActor.run {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        } catch {
            print("Notification authorization error:", error)
        }
    }

    // APNs token received — forward to your server here
    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
        store.deviceToken = token
        print("APNs token:", token)
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("Failed to register:", error)
    }

    // Show banner + play sound even when app is in foreground
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        return [.banner, .sound, .badge]
    }

    // User tapped a notification or action button
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let payload = response.notification.request.content.userInfo
        await MainActor.run {
            store.lastPayload = payload
        }
    }
}

// MARK: - App entry point

@main
struct PushDemoApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(appDelegate.store)
        }
    }
}

// MARK: - Content view

struct ContentView: View {
    @Environment(NotificationStore.self) private var store

    var body: some View {
        NavigationStack {
            List {
                Section("Status") {
                    LabeledContent("Authorization", value: store.authorizationStatus.label)
                    LabeledContent("Device token", value: store.deviceToken.map { String($0.prefix(16)) + "…" } ?? "—")
                }

                if let payload = store.lastPayload {
                    Section("Last notification payload") {
                        ForEach(Array(payload.keys), id: \.self) { key in
                            LabeledContent(
                                "\(key)",
                                value: "\(payload[key] ?? "nil")"
                            )
                        }
                    }
                }
            }
            .navigationTitle("Push Notifications")
        }
    }
}

// MARK: - Helpers

extension UNAuthorizationStatus {
    var label: String {
        switch self {
        case .authorized:    return "Authorized"
        case .denied:        return "Denied"
        case .provisional:   return "Provisional"
        case .ephemeral:     return "Ephemeral"
        case .notDetermined: return "Not determined"
        @unknown default:    return "Unknown"
        }
    }
}

#Preview {
    let store = NotificationStore()
    store.authorizationStatus = .authorized
    store.deviceToken = "abc123def456abc123def456abc123de"
    store.lastPayload = ["aps": ["alert": "Hello!", "badge": 1], "screen": "home"]
    return ContentView()
        .environment(store)
}

How it works

  1. @UIApplicationDelegateAdaptor — bridges the UIKit AppDelegate into the SwiftUI App lifecycle. Because SwiftUI's App struct owns the appDelegate instance, you can safely access appDelegate.store and inject it into the environment.
  2. requestAndRegister() — calls requestAuthorization(options:) with the async/await API introduced in iOS 15 and promoted as the preferred path in iOS 17. Registration only happens if the user grants permission, preventing a pointless network round-trip.
  3. didRegisterForRemoteNotificationsWithDeviceToken — converts the raw Data token into a hex string. In production you POST this string to your backend so APNs can route messages to this device.
  4. willPresent — returning [.banner, .sound, .badge] opts in to showing banners while the app is open. Without this delegate method iOS silently suppresses all foreground notifications.
  5. @Observable NotificationStore — using Swift's @Observable macro (iOS 17+) instead of ObservableObject means only views that actually read a property re-render, avoiding unnecessary redraws across the whole tree.

Variants

Local (scheduled) notifications

No server required — schedule a notification to fire after a delay using UNTimeIntervalNotificationTrigger. Useful for reminders and timers.

func scheduleLocalNotification(title: String, body: String, inSeconds delay: TimeInterval) async throws {
    let content = UNMutableNotificationContent()
    content.title = title
    content.body = body
    content.sound = .default
    content.badge = 1

    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay, repeats: false)
    let request = UNNotificationRequest(
        identifier: UUID().uuidString,
        content: content,
        trigger: trigger
    )
    try await UNUserNotificationCenter.current().add(request)
}

// Usage inside a SwiftUI view:
Button("Remind me in 5 seconds") {
    Task {
        try? await scheduleLocalNotification(
            title: "Hey there 👋",
            body: "This is a local notification.",
            inSeconds: 5
        )
    }
}

Provisional authorization (no permission prompt)

Pass [.alert, .sound, .badge, .provisional] to requestAuthorization(options:). iOS will grant authorization silently and deliver notifications quietly to Notification Center (no lock-screen banner) without ever prompting the user. The user can then upgrade to full authorization from Settings or via a system prompt triggered by interacting with one of your quiet notifications. This is ideal for apps where you want to demonstrate value before asking for full permission.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement push notifications in SwiftUI for iOS 17+.
Use UNUserNotificationCenter, @UIApplicationDelegateAdaptor, and @Observable.
Request authorization with [.alert, .sound, .badge].
Handle foreground delivery (willPresent) and tap response (didReceive).
Expose device token and last payload via an @Observable store in the environment.
Make it accessible (VoiceOver labels for status rows).
Add a #Preview with realistic sample data.

In Soarias's Build phase, paste this prompt into a new file context so Claude Code scaffolds the AppDelegate, store, and entry point together — then use the Edit tool to slot in your real server endpoint for the device token upload.

Related

FAQ

Does this work on iOS 16?

Most of the code works on iOS 16 with minor adjustments. The @Observable macro requires iOS 17+ — on iOS 16 you would replace it with ObservableObject + @Published. The setBadgeCount(_:) call also requires iOS 16+, so it is safe on both. The async/await requestAuthorization overload is available from iOS 15, so there is no need for a callback-based fallback. For iOS 16 support, simply swap @Observable for ObservableObject.

How do I test push notifications on the Simulator?

Since Xcode 14 you can drag a .apns JSON file onto a running simulator or use xcrun simctl push <device-id> <bundle-id> payload.apns from Terminal. A minimal payload file looks like: {"aps":{"alert":{"title":"Test","body":"Hello"},"badge":1}}. This exercises willPresent and didReceive but does not produce a real APNs device token — token-dependent flows (e.g., posting to your server) still require a physical device.

What is the UIKit equivalent?

In a UIKit app you implement the same UNUserNotificationCenterDelegate methods, typically on your AppDelegate or a dedicated coordinator object. The API surface is identical — requestAuthorization, registerForRemoteNotifications, willPresent, didReceive — the only difference is that UIKit apps use AppDelegate natively without needing @UIApplicationDelegateAdaptor. SwiftUI's adaptor is simply the bridge that makes the same pattern available in a declarative app lifecycle.

Last reviewed: 2026-05-11 by the Soarias team.

```