Register an iOS app's APNs token with Native Notify from Swift, and handle foreground pushes and taps.
1. Set up the project
Add the Push Notifications capability under Signing & Capabilities — without it iOS never hands out a token. In a SwiftUI app, remove @main from the delegate and attach it with @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate.
2. Ask for permission and get the token
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()
}
}
3. Register the device
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)
}
}
Never create a new deviceId per launch — each one is a separate device. Pass subscriberId for a logged-in user.
4. Save the APNs key
Save the .p8 key with its Key ID, your Team ID and the app's bundle id under App Settings → Push credentials. See Push credentials. A token from another app answers DeviceTokenNotForTopic.
5. Send a push
Check one device first with a test send; a debug build's sandbox token is retried on the sandbox host. For other audiences, see Send notifications. This call reaches every registered device:
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"}}'
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, idempotent delete. See Device registration.