medium.com 4 min read Original Source

Testing Push Notifications in iOS: Simulator and Real Devices

Hey folks! Testing Push Notifications in iOS applications can sometimes be difficult. Regardless of whether you use Firebase or implement your own backend, you may be stuck for a long time supporting deep links or parsing push models with a complex structure. In this short guide, I’ll cover how to test push notifications locally on the Simulator (with a simple .apns file you can drag-and-drop) fast and easy, and how to test on a real device with proper APNs setup, highlighting the specific moments. Let’s go.

Just a quick reminder. Enable Push in your project and establish minimum operations to register for notifications if your project does not have it yet.

  • Go to Signing & Capabilities in Xcode.
  • Add Push Notifications capability.
  • Add Background Modes → Remote notifications if you want background delivery.
  • Register for Push Notifications

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

func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("📱 Device Token: (token)")
}” data-toggle-ms=”1200″ aria-label=”Copy code” data-nosnippet=”” class=”code-copy-btn absolute right-3 top-3 size-8 p-1.5 flex items-center justify-center bg-black/50 text-white rounded-md transition-colors duration-200 cursor-pointer hover:bg-black/70″>

import UserNotifications
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
        if granted {
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
    }
    return true
}

func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
    print("📱 Device Token: (token)")
}
import UserNotifications
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
        if granted {
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
    }
    return true
}

func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
    print("📱 Device Token: (token)")
}

Now, when you run on a real device, you’ll see your device token printed in the console. This is what you need to send notifications via APNs. Copy this token — it’s required to send pushes from APNs or Firebase.

Testing Notifications in the iOS Simulator

  1. Create an .apns file

or use mine template https://github.com/ndwdm/push-notifications/blob/main/sample.apns

Apple provides a special JSON file format for simulating push notifications. — files with the .apns extension. Let’s create a file called sample.apns:

{
  "aps": {
    "alert": {
      "title": "New Coffee Alert ☕️",
      "body": "Your fresh beans are ready for pickup!"
    },
    "badge": 3,
    "sound": "default"
  },
  "customKey": "customValue"
}
{
  "aps": {
    "alert": {
      "title": "New Coffee Alert ☕️",
      "body": "Your fresh beans are ready for pickup!"
    },
    "badge": 3,
    "sound": "default"
  },
  "customKey": "customValue"
}

Required Fields:

  • aps — The root object that APNs expect.
  • alert — Can be a string or a dictionary with title, subtitle, and body.
  • badge — Integer to update the app icon badge.
  • sound — Name of the sound file or “default”.

Any additional keys (like customKey) are delivered to your app in userInfo (later, I’ll give you code snippets on how to parse them in different places).

This is a simple, minimum parameter push content.
But your logic may have more parameters as data objects inside the payload, such as user ID, message type, navigation links, etc. You can write the logic that copies the body of your push content, or ask the backend department for the structure for the .apns file preparation to work locally with the simulator.

I’ll share my typical file with tiny changes I use for testing most projects.

https://github.com/ndwdm/push-notifications/blob/main/sample.apns

  1. Send to Simulator
    Make sure your app is running in the Simulator but in a background mode, because you won’t see the visual banner at this time, it requires a custom definition.

Change the directory to where your sample.apns is located with the command:

cd

and drag the directory from the Finder into the Terminal window (sometimes I meet developers who don’t know about this simple way), or write the full path to the sample.apns in the command below.

Then, in Terminal, run:

xcrun simctl push booted com.yourdomain.app sample.apns

Where:

  • booted = the currently running simulator
  • com.yourdomain.app = your app’s bundle identifier
  • sample.apns = your JSON file

Alternatively, starting from Xcode 11, you can drag and drop the .apns file directly onto the Simulator window. The notification will appear as if APNs delivered it.

The notification will appear just like a real APNs push.

IMPORTANT: When using drag and drop, your .apns file needs to include a key that indicates your app bundle ID. “Simulator Target Bundle”: “com.yourdomain.app”,

Now you can test different scenarios, simulate:

  • Notifications with only title & body.
  • Silent pushes (with “content-available: 1”).
  • Custom payloads for deep links.

Testing Notifications on a Real Device

Simulator testing is great for layout and quick checks, but real devices behave differently (permissions, tokens, background fetch, etc.):

  • Device registration with APNs.
  • Token generation.
  • Receiving pushes from Apple’s servers.
  • Interaction flows (tap, dismiss, actions, background fetch).

Send a Test Push to Your Device
You can test pushes in several ways:

  • APNs via terminal. Convert your APNs certificate to .p8 and use curl or tools like NWPusher (https://github.com/noodlewerk/NWPusher).
  • Using Firebase Cloud Messaging (FCM). If your app uses Firebase, you can send test notifications directly from the Firebase Console. Firebase Cloud Messaging does not replace APNs. Firebase sends your push payload to Apple’s APNs servers, which then deliver it to your device. That means your app must be correctly set up with APNs (certificates/keys, entitlements, provisioning profile) even if you’re using Firebase.
  • Third-party tools like Pusher (https://pusher.com/) or Postman collections.

Parsing

To parse the payload for custom objects that exist, I prefer to use my own models with internal data type validation, but for demonstrating purposes, show these snippets:

Void) {
let userInfo = response.notification.request.content.userInfo
if let aps = userInfo["aps"] as? [String: Any],
let notification = aps["alert"] as? [String: Any] {

aps["customKey"] // to get the param's value
if let eventDict = userInfo["event"] as? [String: Any] { // to cast the type
}
}
}” data-toggle-ms=”1200″ aria-label=”Copy code” data-nosnippet=”” class=”code-copy-btn absolute right-3 top-3 size-8 p-1.5 flex items-center justify-center bg-black/50 text-white rounded-md transition-colors duration-200 cursor-pointer hover:bg-black/70″>

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        if let aps = userInfo["aps"] as? [String: Any],
           let notification = aps["alert"] as? [String: Any] {

            aps["customKey"] // to get the param's value
            if let eventDict = userInfo["event"] as? [String: Any] { // to cast the type
            }
        }
    }
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        if let aps = userInfo["aps"] as? [String: Any],
           let notification = aps["alert"] as? [String: Any] {

            aps["customKey"] // to get the param's value
            if let eventDict = userInfo["event"] as? [String: Any] { // to cast the type
            }
        }
    }

Or

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        if let userInfo = connectionOptions.notificationResponse?.notification.request.content.userInfo {
        }
    }
func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        if let userInfo = connectionOptions.notificationResponse?.notification.request.content.userInfo {
        }
    }

Localization

Often, we need to show localized push strings. In this way, we use:

"MESSAGE_TITLE" = "New chat message";
"MESSAGE_BODY" = "%1$@: %2$@";
"MESSAGE_TITLE" = "New chat message";
"MESSAGE_BODY" = "%1$@: %2$@";

APNs automatically fetch strings from Localizable.strings inside the app. These strings will show their localized values on the go if they are sent in push body like:

{
    "aps": {
        "alert": {
            "title-loc-key": "MESSAGE_TITLE",
            "loc-key": "MESSAGE_BODY",
            "loc-args": [
                "User",
                "message"
            ],
        },
        "badge": 3,
        "sound": "default",
    },
    "customKey": "customValue",
    "Simulator Target Bundle": "com.yourdomain.app",
}
{
    "aps": {
        "alert": {
            "title-loc-key": "MESSAGE_TITLE",
            "loc-key": "MESSAGE_BODY",
            "loc-args": [
                "User",
                "message"
            ],
        },
        "badge": 3,
        "sound": "default",
    },
    "customKey": "customValue",
    "Simulator Target Bundle": "com.yourdomain.app",
}

Note & Tips:
If you don’t see notifications:

  • Check that your project’s bundle identifier matches in the Apple Developer portal and the .apns file if used for testing.
  • Notifications may work on TestFlight builds only, not in debugging with Xcode. If your backend sends only production pushes, Debug builds won’t receive anything.
  • Verify that push certificates/authentication keys are added to your backend.
  • On real devices, make sure notifications are enabled in Settings → Notifications.

Thanks for reading. I hope it helps in a daily routine. Follow my GitHub

https://github.com/ndwdm

hit the star button, and share your thoughts below this article. See you next.