Skip to content
iOS Push Notifications: Complete APNs & Swift Setup Guide

19 Minutes

iOS Push Notifications: Complete APNs & Swift Setup Guide

Fix Bugs Faster! Log Collection Made Easy

Get started

Whether you’re building your first iOS app or improving user engagement, understanding push notifications is essential. They’re how apps stay connected with users, delivering updates, reminders and alerts even when the app itself isn’t running.

In this guide, you’ll learn how to implement iOS push notifications from setup to debugging, using Swift and Apple Push Notification Service (APNs) in a real app.

We’ll cover APNs keys, permissions, payloads, and the tiny details that make notifications reliable across every iPhone and iPad.

By the end, you’ll know how to send, test, and debug iOS push notifications effectively, avoiding common APNs errors while improving both delivery performance and user retention.

What are iOS push notifications?

iOS push notifications are messages sent via access point names (APNs) from a server to an iPhone or iPad to inform the user about updates, alerts or actions, even when the app is closed.

In real apps, these notifications provide booking updates, delivery tracking, feature announcements, reminders, promotions, and re-engagement nudges. They play a vital role at various stages of the customer journey, and they rely on a few core components that route the message from our backend to the user’s device.

The main components are:

  • APNs: Apple’s delivery system that handles all notification routing for iOS devices.
  • Device token: A unique identifier that APNs assign to each device, so our server knows exactly where to send a notification.
  • App server: The backend service that prepares the JSON payload, signs the request and sends it to APNs for delivery.

These three pieces form the foundation of every iOS push notification system.

How iOS push notifications work

iOS push notifications register our app with APNs, sending a signed payload from our server and letting APNs deliver it to the device.

Once our app is set up and the user grants permission, each notification passes through a few steps before appearing on screen.

  1. Our app asks the user for permission to send them notifications.
  2. iOS registers the app with APNs. This returns a unique device token.
  3. The device token is sent to our backend server and stored for future sends. Device tokens can change, so we must update them regularly.
  4. When we trigger a notification, our server creates a JSON payload and signs it using our APN key.
  5. The server sends the payload to APNs, including the device token.
  6. APNs verifies the request and securely routes the notification to the correct device.
  7. iOS displays it as a banner, alert, lock-screen message or in-app notification, depending on user settings.

Local vs remote notifications

iOS supports two main types of notifications: local and remote. Both look identical to users, but they differ in how they’re created and delivered. Understanding the distinction helps us choose the right method for our app.

Local notificationsRemote notifications
Triggered on the user’s device without using a network.Sent from our backend server through the Apple Push Notification Service (APNs).
Used for scheduled reminders, in-app alerts or background tasks.Ideal for real-time updates, messages, or alerts from external data sources.
Content defined and managed within the app.Payload and delivery handled by our server and APNs.

In short: use local notifications for device-driven events, and remote notifications for dynamic, server-controlled updates.

Why push notifications will still matter in 2026

Push notifications were first rolled out in 2009 and they continue to drive high engagement, retention and repeat usage today. Even with stricter privacy rules, they remain one of the most effective ways to reach iOS users.

Some key performance signals to illustrate this point:

  • Open rates reach up to 20%, far above email.
  • iOS reaction rates stay around 4–5%.
  • CTRs often hit 25–30%.
  • Apps using push see nearly 3× higher 90-day retention.
  • Retention boosts: +120% (occasional), +440% (weekly), +820% (daily).
  • Opt-in rates remain steady at ~44%.
  • Nearly half of users say a push influenced a purchase.
  • 60% say pushes make them open an app more often.

Brands are still finding innovative ways to weave push notifications into their marketing mix. Bluesky, a rival to X, made headlines over the summer by adopting the activity notifications feature, which lets readers request notifications from specific accounts. The glowing coverage this roll-out has received demonstrates the enduring value and potential of push notifications.

How to set up push notifications in iOS

Setting up push notifications in iOS requires configuring our app to work with APNs. This includes generating the correct credentials, enabling the right capabilities in Xcode and confirming that everything works with a real-device test.

1. Create an APNs Auth Key (.p8)

To set up iOS push notifications, the first thing we need is an APNs Auth Key. This key allows our server to authenticate with APNs and send notifications to our app.

To generate the APNs key:

  1. Log in to the Apple Developer portal.
  2. Navigate to Certificates, Identifiers & Profiles → Keys.
  3. Create a new key and enable APNs for it.
  4. Download the .p8 file — note that we can only download it once.

We now have four values that our backend will need:

  • APNs .p8 file — the key that we just downloaded.
  • Key ID — visible next to our key in Certificates → Keys.
  • Team ID — found in the Membership Details of our developer account.
  • Bundle ID — from our Xcode project (Targets → General).

Keep these secure. Our server will need them to sign every request sent to APNs.

2. Enable push capabilities in Xcode

Once we’ve created our APN key, we can enable push support in our Xcode project. This is really useful, as it tells iOS that our app is allowed to receive remote notifications, and ensures that our provisioning profile includes the correct entitlements.

How to enable push notifications in Xcode:

  1. Open the project in Xcode.
  2. Select the app target → open Signing & Capabilities.
  3. Click + Capability and add Push Notifications.
  4. If we rely on background updates or silent pushes, we can also add Background Modes and enable Remote notifications.

Once this is enabled, Xcode updates our entitlements automatically. Our app is now set up to receive device tokens and handle incoming notifications sent through APNs.

3. Register and test notifications (with curl example)

With our APN key and Xcode capabilities configured, we now need to register our app for remote notifications and verify that everything works end to end.

First we request permission from the user through UNUserNotificationCenter, then call UIApplication.shared.registerForRemoteNotifications() to trigger APNs registration.

When iOS returns a device token in didRegisterForRemoteNotificationsWithDeviceToken, we send it to our backend so it can target this specific device during testing.

Note that APNs does not work on the iOS Simulator. You should always test on a physical device.

To confirm the full pipeline works, we can send a sample push notification using curl:

curl -v \\
--header "apns-topic: <OUR_BUNDLE_ID>" \\
--header "authorization: bearer <OUR_JWT>" \\
--data '{"aps":{"alert":"Test push!"}}' \\
<https://api.push.apple.com/3/device/><DEVICE_TOKEN>

If everything is set up correctly, we should see the test notification appear instantly on our device.

How to implement push notifications in Swift

Here’s where we move from theory to practice.

Once our app is configured to work with APNs we can focus on how it actually behaves in Swift. Here we decide when we ask for permission, how we handle the device token and what happens when a notification arrives in the foreground or background.

To recap, implementing push notifications in Swift means:

  • Requesting notification permission from the user.
  • Registering for remote notifications on app startup.
  • Handling the device token and possible errors.
  • Responding to notifications via UNUserNotificationCenter delegates.
  • Supporting advanced use cases like silent pushes and background updates.

In the following sections, we’ll walk through each part step by step, so our app can handle iOS push notifications reliably in real-world scenarios.

1. Register for remote notifications

To implement push notifications in Swift, we first register our app with the iOS notification system and APNs.

This step triggers APN registration and provides the device token our backend needs to send notifications. Note, though, that it requires permission from the user.

Typical flow in Swift:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
    if granted {
        DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}
  1. Set UNUserNotificationCenter.current().delegate = self early in app launch.
  2. Call requestAuthorization(options:) to ask for alert, sound, and badge permissions.
  3. If granted, call UIApplication.shared.registerForRemoteNotifications() on the main thread.
  4. Implement the device-token callback to receive and upload the token:
func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    print("APNs token:", token)
    MyAPI.uploadDeviceToken(token)   // Send to backend
}
  1. Handle registration failures for easier debugging:
func application(_ application: UIApplication,
                 didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("Failed to register:", error.localizedDescription)
}

This registration flow connects our Swift app to APNs and ensures we get a valid device token for testing and production pushes.

2. Handle foreground and background events

Notifications can arrive in both foreground or background states. With registration in place, we can now decide how our app will react when a notification arrives in either of these states.

Quick note: by default, iOS only shows banners and alerts when the app is in the background, so we often want more control on the Swift side.

We typically use UNUserNotificationCenterDelegate:

  • Foreground: Implement userNotificationCenter(_:willPresent:withCompletionHandler:) to decide whether to show an alert, play a sound, or update UI while the app is open.
  • Background / tapped: Implement userNotificationCenter(_:didReceive:withCompletionHandler:) to react when the user taps a notification—for example to navigate to a specific screen, open a chat, or load extra data.

By handling these callbacks, we can keep iOS behavior consistent across the different app states.

3. Support silent notifications and data refresh

Silent pushes are a powerful way to keep our app’s content fresh without interrupting the user. They let us refresh data or trigger background work without showing an alert to the user. This is useful for syncing content, updating badges, or preparing data before the user opens the app.

To support silent pushes in Swift, we:

  • Enable Background Modes → Remote notifications in Xcode (already done in setup).
  • Send pushes with {"aps": {"content-available": 1}} and no alert payload.
  • Implement application(_:didReceiveRemoteNotification:fetchCompletionHandler:) to handle the incoming payload in the background.
  • Perform lightweight work (e.g. fetch new data) and call the completion handler with .newData, .noData, or .failed.

APNs payload examples

Before continuing, let’s take a quick look at real APN payloads.

Every push notification is just a JSON object inside the aps dictionary, with optional custom fields for app-specific logic. Keeping payloads clean and lightweight improves delivery speed and avoids throttling, where we restrict the rate at which a function, request or process can run.

Here’s what the code strings look like:

Standard alert

{
  "aps": {
    "alert": {
      "title": "Hello!",
      "body": "Your order is on the way 🚚"
    },
    "badge": 1,
    "sound": "default"
  }
}

Silent/background push

{
  "aps": {
    "content-available": 1
  },
  "updateType": "sync",
  "timestamp": "2025-01-01T12:00:00Z"
}

Rich media (image/video)

{
  "aps": {
    "alert": "Big sale today!",
    "mutable-content": 1
  },
  "media-url": "<https://example.com/image.jpg>"
}

These templates cover the payload types used most often during development, testing and debugging.

Quick APNs sending tips (curl & endpoint basics)

A few quick checks make curl-based APN testing much easier. These small but essential steps help validate token correctness, endpoint selection and header formatting, the failure points that we will typically run into.

Essential curl tips

  • Use the right APN environment: Sandbox: https://api.development.push.apple.com/3/device/<TOKEN> Production: https://api.push.apple.com/3/device/<TOKEN>
  • Always include: apns-topic: <YOUR_BUNDLE_ID>
  • Keep test payloads small to rule out formatting issues.
  • Refresh JWTs frequently. APNs rejects expired or malformed headers.

These simple rules remove most of the guesswork when validating the full path from backend → APNs → device.

4. Add a minimal UNUserNotificationCenter implementation

UNUserNotificationCenter is the central object your app uses to schedule, deliver, manage, and respond to notifications. It lets us tie everything together and gives us a single place to manage permissions, callbacks and basic behavior – all with a minimal Swift implementation.

Here’s the code to show you:

import UserNotifications
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let center = UNUserNotificationCenter.current()
        center.delegate = self

        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
            if granted {
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        }

        return true
    }

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler:
        @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        completionHandler([.banner, .sound])
    }
}

From here, we can extend the logic to handle background taps, silent pushes, and app-specific flows.

Managing iOS push notification permissions & user experience

It goes without saying that managing notifications is crucial to ensuring a smooth push experience. This is particularly true with iOS, which gives users full control over alerts.

It’s our job to request permission at the right time and explain why notifications matter. A poorly timed prompt can lead to permanent denial, while a thoughtful opt-in flow can dramatically improve our approval rates.

If we want to maintain good permission management, we need to remember 3 things:

  • Ask at the right moment.
  • Provide clear value.
  • Respect the user’s preferences.

In this section, we’ll walk through how to request authorization correctly, design a helpful pre-permission screen, and ensure that our push notifications feel useful and not intrusive.

When and how to request permission

When to Request PermissionHow to Request Permission
After the user performs an action that logically benefits from notifications (saving a favorite, enabling reminders, joining a community, etc.)Provide a short in-app explanation first (“We can notify you about…”) before triggering the system prompt.
When the user clearly understands the value of opting inTrigger the iOS permission alert only when there is context, not on first launch.
When the user is engaged, not rushing or exploring blindlyProvide a settings shortcut later if they decline. Don’t repeat prompts aggressively.
When we can track the user’s readiness or previous interactionsMonitor when the prompt was shown so we avoid asking at the wrong moment or too frequently.

Requesting permissions with proper timing and clear intent leads to significantly higher approval rates and a better overall user experience.

Designing a user-friendly opt-in flow

A user-friendly opt-in flow explains why notifications matter, and sets the right expectations before iOS displays its system prompt.

We can guide users with a simple pre-permission screen that highlights the value they’ll get—updates, reminders, alerts, or anything that improves their experience.

A good opt-in flow usually includes:

  • A short, friendly explanation of what we’ll send.
  • Clear benefits (“We’ll notify you when…”).
  • A single “Enable notifications” button that triggers the system alert.
  • A fallback option (“Maybe later”) that respects the user’s choice.
  • No walls or forced prompts.

When we combine clarity with respectful timing, users are far more likely to opt in—and stay opted in—because they understand the value of the notifications we send.

Localization & timing best practices

Localization Best PracticesTiming Best Practices
Localize notification text, action buttons, and categories so the entire flow feels natural.Send based on the user’s local time zone, not server time.
Match the tone to local expectations (some regions prefer direct language, others prefer more formal or friendly wording).Avoid disruptive hours: try to avoid sending notifications before 8 AM and after 10 PM.
Adapt content to local language and idioms to ensure clarity and relevance.Use high-engagement windows like 9–11 AM and 5–7 PM.
Reflect local holidays, events, and seasonal behavior in your messaging.Weekend and weekday engagement patterns vary—adjust schedules accordingly.
Support right-to-left languages and region-specific formatting.Avoid midday “rush hours” (e.g., 12–2 PM) for non-urgent pushes.

Example: For the Spanish market, a shopping reminder sent at 10:30 AM local time, localized to Spanish with a friendly tone (“Tus artículos siguen disponibles 😄” or “Your items are still available 😄”) **will outperform a generic English notification sent at 6AM.

Debugging & troubleshooting iOS push notifications

Even when our APNs setup looks correct, it’s common for iOS push notifications to silently fail.

Most issues come from a small set of misconfigurations: wrong environment, invalid tokens, missing capabilities or authorization problems.

In this section, we’ll walk through the most frequent delivery issues, what to check first, and how to use logs and external tools to see what’s really happening between our server, APNs and the device.

The goal isn’t to debug every edge case, but to give us a fast checklist so we can move from “nothing shows up” to a working push in minutes instead of hours.

Fixing common delivery issues

SymptomWhat we should check
Notification never arrives on deviceConfirm the device is on a real device (not simulator) and has network access. Make sure the app is installed and not uninstalled after token generation.
Works on some devices but not othersCompare device tokens; old tokens may be invalid. Ensure we’re using the latest token from didRegisterForRemoteNotificationsWithDeviceToken.
No device token is generatedCheck that Push Notifications and Remote notifications (Background Modes) are enabled in Xcode, and registerForRemoteNotifications() is called after permission is granted.
APNs returns 400 / 403 / 410 errorsVerify our APNs Auth Key, Key ID, Team ID, and Bundle ID. Make sure we’re using the correct APNs endpoint (sandbox vs production) and topic.
Notification arrives but no alert is shownCheck the payload: does it include a valid aps.alert? On foreground, confirm willPresent delegate calls the completion handler with presentation options.
Notification shows but tap does nothingEnsure didReceive (or scene-based callbacks) handles navigation logic and that we aren’t swallowing the event or missing routing code.

Checking APNs keys, tokens & logs

When notifications don’t arrive, the first thing we validate is authentication and addressing: our APNs key, device tokens and logs.

A single mismatch in these values is enough to break delivery.

We start by confirming we’re using the correct .p8 APNs key, Key ID, Team ID, and Bundle ID, and that our server is sending the right apns-topic header.

Then we double-check that the device token we’re using is the latest one returned by iOS. Remember that tokens can change across installs, restores or environments.

On the logging side, we log every outgoing push request with:

  • the device token (masked in production logs)
  • the environment (sandbox vs production)
  • the APNs response status and body

We also check Xcode console logs on the device and our backend logs in parallel. This combination usually reveals whether the issue is in our app, our server, or APNs.

Debugging iOS push notifications with Bugfender

Bugfender makes troubleshooting iOS push notifications much easier by showing real-time logs from physical devices, no Xcode connection required. This helps us confirm whether the app is registering correctly, receiving callbacks, or failing silently.

With Bugfender, we can:

  • Confirm didRegisterForRemoteNotificationsWithDeviceToken is called and capture the token.
  • Detect errors from didFailToRegisterForRemoteNotificationsWithError.
  • Log incoming payloads in didReceiveRemoteNotification or notification center delegates.
  • Verify silent push callbacks and background delivery.
  • Compare logs across multiple devices to spot inconsistent behavior.

If APNs says the push was delivered but nothing appears on the device, Bugfender’s logs help pinpoint whether the problem is in permissions, app logic, or background handling.

iOS push notifications FAQ

What’s new for iOS push notifications in iOS 18?

iOS 18 introduces improved notification relevance ranking, smarter grouping, and expanded control over focus modes. Developers also get more consistent background delivery for silent pushes, plus refined permissions UX that makes timing and context even more important.

Can I send iOS push notifications without a server?

Yes, but only in limited cases. You can send test pushes using tools like curl, but real production pushes typically require a backend. Alternatives include Firebase Cloud Messaging, serverless functions, or third-party push providers that remove the need to run a full server.

What is the payload size limit for APNs?

APNs allows payloads up to 4 KB for remote notifications. For media attachments, APNs only delivers the URL and the app downloads the file. Keeping payloads small improves reliability and delivery speed.

Why are my push notifications not showing up on iOS?

Common causes include:

  • Missing permission (user tapped “Don’t Allow”)
  • Using the wrong APNs environment (sandbox vs production)
  • Invalid or outdated device token
  • Missing aps.alert in the payload
  • Foreground notifications not displayed due to delegate settings
  • Silent notifications arriving but not triggering UI updates

Checking tokens, APNs responses, and device logs (via Bugfender) usually identifies the issue.

Do iOS push notification device tokens change?

Yes. Tokens can change after reinstalling the app, restoring a device, switching environments, or when Apple rotates internal identifiers. Always send the latest token to your backend on every launch.

Can iOS deliver push notifications to the app when it’s killed?

Yes, remote notifications still arrive on the system and appear to the user. However, silent pushes won’t run background code if the user force-killed the app. Only visible alerts are guaranteed.

How often can I send iOS push notifications?

There’s no official APNs limit, but apps sending too many pushes may get lower engagement or higher opt-out rates. Data suggests 1–3 pushes per week is safe for most use cases unless the app is transactional (messages, delivery updates, reminders).

Do push notifications work on iOS simulators?

No. APNs does not deliver remote notifications to simulators. Testing must be done on a physical device.

Why do iOS push notifications get delayed?

Delays usually happen due to poor network conditions, large payloads, server-side retries, or the device being in low power mode. Silent pushes are especially affected and may be throttled by the system.

Conclusion: building reliable iOS push notifications

iOS push notifications aren’t just a “nice to have”, they’re one of the most effective ways to keep users engaged, informed, and returning to the app. Once we understand how APNs, device tokens, and payloads fit together, the setup becomes repeatable: configure APNs, enable capabilities in Xcode, register for remote notifications and handle events cleanly in UNUserNotificationCenter.

The real performance boost comes from smart permission timing, user-friendly messaging and proper debugging. When pushes fail silently, visibility into real devices is essential — which is exactly where Bugfender helps uncover what’s actually happening in production.

Get these fundamentals right once, and we can ship iOS push notifications in Swift that are reliable, respectful, and consistently great for retention.

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Aleix Ventayol

Aleix Ventayol is CEO and co-founder of Bugfender, with 20 years' experience building apps and solutions for clients like AVG, Qustodio, Primavera Sound and Levi's. As a former CTO and full-stack developer, Aleix is passionate about building tools that solve the real problems of app development and help teams build better software.

Join thousands of developers
and start fixing bugs faster than ever.