Swift Recipe (native iOS)
Register a native iOS app's APNs device token with Native Notify's push API from Swift — delegate, hex token, URLSession registration, and the Apple credentials it needs.
Native iOS apps talk to APNs directly; the device token that arrives in the app delegate is exactly what the universal service registers.
1. Ask for permission and register with APNs
import UIKit
import UserNotifications
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
guard granted else { return }
DispatchQueue.main.async { application.registerForRemoteNotifications() }
}
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let apnsToken = deviceToken.map { String(format: "%02x", $0) }.joined()
Task { await UniversalPush.register(appId: 123, appToken: "yourAppToken", apnsToken: apnsToken) }
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("APNs registration failed:", error) // usually the Push Notifications capability is missing
}
// Show the banner while the app is in the foreground too.
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .list, .sound])
}
// A tap: your pushData keys sit at the top level of userInfo (and the whole object under "body").
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if let url = userInfo["url"] as? String {
print("Open", url) // route to it in your app
}
completionHandler()
}
}
- Push Notifications capability: in Xcode, Signing & Capabilities → + Capability → Push Notifications. Without it iOS never hands out a token (
didFailToRegisterForRemoteNotificationsWithErrorreports a missingaps-environmententitlement). - SwiftUI app? Remove
@mainfrom the delegate and attach it to yourAppstruct instead:@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate. - APNs can re-deliver
didRegisterForRemoteNotificationsWithDeviceTokenat any launch (and rotates the token on restore) — registering every time is correct: it is an idempotent upsert for the samedeviceId. - Taps: your
pushDatakeys arrive at the top level ofuserInfo(the whole object is also underuserInfo["body"]), together withnn_notification_id.
2. Register with Native Notify
import Foundation
enum UniversalPush {
static func register(appId: Int, appToken: String, apnsToken: String, subscriberId: String? = nil) async {
guard let url = URL(string: "https://app.nativenotify.com/api/universal/device/register") else { return }
var body: [String: Any] = [
"appId": appId,
"appToken": appToken,
"deviceId": DeviceIdentity.stableKey(),
"platform": "ios",
"tokens": ["apnsToken": apnsToken],
]
if let subscriberId { body["subscriberId"] = subscriberId }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
do {
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
if status != 201 {
print("Native Notify register failed (\(status)):", String(decoding: data, as: UTF8.self))
}
} catch {
print("Native Notify register error:", error)
}
}
}
/// A per-install key kept in the Keychain. `ThisDeviceOnly` keeps a backup
/// restore from copying the same key onto a second phone.
enum DeviceIdentity {
private static let account = "nn-universal-device-id"
static func stableKey() -> String {
if let saved = read() { return saved }
let fresh = UUID().uuidString
let attributes: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: Data(fresh.utf8),
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
SecItemAdd(attributes as CFDictionary, nil)
return fresh
}
private static func read() -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
}
DeviceIdentity keeps one UUID per device in the Keychain: it survives relaunches and reinstalls, and …ThisDeviceOnly stops a backup restore from copying it onto a second phone. Never generate a new id per launch — every distinct deviceId is a separate device.
Pass subscriberId when a user is logged in; omit it for an anonymous device.
3. Credentials the app needs
Save the app's Apple .p8 key with its Key ID, Team ID and the app's Bundle ID as the app's credentials in Native Notify — Push Credentials. The Bundle ID must be this app's: a token that belongs to another app answers DeviceTokenNotForTopic.
4. Send and verify
curl -X POST https://app.nativenotify.com/api/universal/notifications/send \
-H "Content-Type: application/json" \
-d '{"appId":123,"appToken":"yourAppToken","title":"Hello iOS","message":"Delivered over APNs.","audience":{"type":"all"}}'
"all" reaches every registered device, with or without a subscriberId; to reach one logged-in user's devices, send {"type":"subscribers","subscriberIds":["user_8241"]} instead — see Send Notifications.
Confirm one device first with test-send and its deviceId — for iOS it returns APNs' answer immediately: accepted, or failed with a reasonCode such as Unregistered (the app was uninstalled or the token revoked) or BadDeviceToken (not a valid token for this app — a debug build's sandbox token is retried on the sandbox host automatically).
Deregister on logout
import Foundation
extension UniversalPush {
static func deregister(appId: Int, appToken: String) async {
guard let url = URL(string: "https://app.nativenotify.com/api/universal/device/deregister") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: [
"appId": appId, "appToken": appToken, "deviceId": DeviceIdentity.stableKey(),
] as [String: Any])
_ = try? await URLSession.shared.data(for: request)
}
}
Deregister is a hard delete (idempotent — safe to call more than once). See Device Registration.