โ† All articles

How to Add Push Notifications to a SwiftUI App

Add push notifications to a SwiftUI app in 2026: request permission, schedule reminders, register for APNs, and handle taps and foreground alerts.

How to Add Push Notifications to a SwiftUI App

Notifications are two different features that share a permission prompt. Local notifications are scheduled on the device and need no server, which makes them the right tool for reminders and timers. Remote notifications, the ones people mean by "push," are sent from your server through Apple Push Notification service (APNs) and need a device token plus a paid Apple Developer account. Most apps want both.

This guide covers the whole surface: asking permission the right way, scheduling a local notification, registering for remote push, handling a tap and a foreground alert, and what your server needs to actually send one. The examples are current SwiftUI and UserNotifications.

On this page

Step 1: Ask permission

Both local and remote notifications need the same authorization. Ask for it with the async API, and ask at a moment the user understands why, not on first launch.

import UserNotifications
 
func requestNotificationPermission() async -> Bool {
    do {
        return try await UNUserNotificationCenter.current()
            .requestAuthorization(options: [.alert, .sound, .badge])
    } catch {
        return false
    }
}

The prompt shows once. If the user declines, you cannot ask again from code; they have to enable notifications in Settings. That is why timing matters. A permission request tied to an action ("remind me at 9am") converts far better than one fired before the user knows what your app does.

Step 2: Local notifications

A local notification is content plus a trigger, wrapped in a request and handed to the notification center. No network, no server, works in the Simulator.

func scheduleReminder() {
    let content = UNMutableNotificationContent()
    content.title = "Time to practice"
    content.body = "Your daily session is ready."
    content.sound = .default
 
    // Fires 5 seconds from now. Use UNCalendarNotificationTrigger for a
    // specific time of day, with repeats: true for a daily reminder.
    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
 
    let request = UNNotificationRequest(
        identifier: UUID().uuidString,
        content: content,
        trigger: trigger
    )
    UNUserNotificationCenter.current().add(request)
}

Give the request a stable identifier if you want to update or cancel it later; a UUID is fine for fire-and-forget. To cancel, call removePendingNotificationRequests(withIdentifiers:). This is the entire local notification story, and for a lot of apps it is all they need.

Step 3: Register for remote push

Remote push has one extra requirement that surprises people in a pure SwiftUI app: the registration callbacks live on UIApplicationDelegate, not on the App struct. Bridge them in with @UIApplicationDelegateAdaptor, and add the Push Notifications capability to your target (Signing and Capabilities in Xcode).

import SwiftUI
import UserNotifications
 
@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
 
final class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
            [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }
 
    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()
        // Send this token to your server. APNs uses it to find this device.
    }
 
    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("APNs registration failed: \(error.localizedDescription)")
    }
}

Registration itself happens after permission is granted, and it must run on the main actor.

let granted = await requestNotificationPermission()
if granted {
    await MainActor.run {
        UIApplication.shared.registerForRemoteNotifications()
    }
}

The device token you receive in didRegisterForRemoteNotificationsWithDeviceToken is the address APNs delivers to. Send it to your server on every successful registration, because it can change after a reinstall or a restore.

Step 4: Handle taps and foreground alerts

Two delegate methods do the work, and both cover local and remote notifications. Conform your AppDelegate to UNUserNotificationCenterDelegate (you already set it as the delegate above) and implement the async versions.

extension AppDelegate: UNUserNotificationCenterDelegate {
    // Show the notification while the app is open. Without this, a
    // notification that arrives in the foreground is silently dropped.
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        [.banner, .list, .sound]
    }
 
    // React when the user taps a notification.
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let userInfo = response.notification.request.content.userInfo
        // Read your custom payload keys and route to the right screen.
    }
}

The willPresent method is the one people forget. Skip it and your carefully sent notification never appears while the user is looking at the app. For tap handling, put a small identifier in the payload (a screen name, an object id) and use it in didReceive to drive navigation through your app state.

What your server needs

To send a remote notification, your server talks to APNs with three things: a token-based auth key (a .p8 from your Apple Developer account), the device token you saved, and a JSON payload. The payload's aps object carries the alert, badge, and sound, and you can add your own keys alongside it.

{
  "aps": {
    "alert": { "title": "New message", "body": "You have a reply." },
    "sound": "default",
    "badge": 1
  },
  "screen": "chat",
  "chatId": "8821"
}

You can write the APNs calls yourself over HTTP/2, but most teams route push through a provider such as Firebase Cloud Messaging so they get one API, delivery reporting, and Android in the same place. Either way, the device sees the same payload, and your didReceive handler reads screen and chatId to open the right view. For the security side of storing tokens and keys, see iOS App Security Best Practices.

Common pitfalls

  • Testing on the Simulator: local notifications work there, and the Simulator can receive simulated pushes with xcrun simctl push or by dragging a .apns file onto it. Real APNs delivery still needs a physical device.
  • Foreground drop: without willPresent, notifications that arrive while the app is open do not show. This is the most common "my push isn't working" report.
  • Stale tokens: the device token changes on reinstall or restore. Re-send it every time registration succeeds, and let your server retire tokens APNs reports as invalid.
  • Asking too early: a permission prompt on first launch gets denied a lot, and a denial is close to permanent. Ask in context.

FAQ

Do I need a server for notifications?

For remote push, yes, because something has to send the payload to APNs. For local notifications, no; they are scheduled entirely on the device.

Do I need a paid Apple Developer account?

For remote push, yes. It requires the Push Notifications capability and an APNs key, both of which need the paid program. Local notifications work without it.

Why do I need an AppDelegate in a SwiftUI app?

The remote-registration callbacks (didRegisterForRemoteNotificationsWithDeviceToken and the failure case) are defined on UIApplicationDelegate. @UIApplicationDelegateAdaptor bridges that class into your SwiftUI App.

What is the difference between local and remote notifications?

Local notifications are scheduled on the device for a future time, ideal for reminders. Remote notifications are pushed from your server through APNs in real time, ideal for messages and events that originate elsewhere.

Can I test push notifications in the Simulator?

You can test simulated pushes with xcrun simctl push or a dragged .apns file. To verify a real end-to-end delivery through APNs, use a device.

How do I open a specific screen when a notification is tapped?

Include an identifier in the payload, read it from response.notification.request.content.userInfo in didReceive, and update your navigation state to route there.

Notifications reward getting the small things right. Ask for permission in context, use local notifications for anything you can schedule on-device, bridge the APNs callbacks with a delegate adaptor, and never forget the willPresent handler. Do that and both halves, the on-device reminders and the server-sent pushes, work the way users expect.


Spaceport generates a production-ready SwiftUI Xcode project with notifications already wired up, alongside RevenueCat subscriptions, Sign in with Apple and Google, onboarding, analytics, a Home Screen widget, and App Store Connect pricing across 25 markets. The permission flow, the delegate adaptor, and the tap handling above ship working in the generated project, so you start from a real notification setup instead of a stub. From an indie iOS dev, for indie iOS devs.

Read more at spaceport.build

Community appsJoin Discord