Integrating Apple Push Notification Service (APNs)
Push notifications in iOS look like a simple task right until notifications arrive on the simulator but not on a real device. Or arrive in dev environment and disappear in production. The reason is almost always one: misconfigured certificates or environment mismatch (sandbox vs production). APNs is a strict system, and any configuration mismatch leads to silent loss of notifications without client-side error.
Setup: p8 key vs p12 certificate
Apple supports two authentication methods for APNs:
APNs Auth Key (.p8) — JWT token. One key for entire account, doesn't expire (unless revoked), works for both sandbox and production without switching. Recommended approach since 2016.
APNs Certificate (.p12) — SSL certificate. Expires in a year, separate for sandbox and production, bound to specific Bundle ID. Outdated method, but still found in legacy projects.
For new project always choose .p8 through Apple Developer Console → Certificates, Identifiers & Profiles → Keys.
Configuration on iOS app side
// AppDelegate.swift
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { granted, error in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
// Send token to server
NotificationService.shared.registerToken(tokenString)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("APNs registration failed: \(error)")
}
didFailToRegisterForRemoteNotificationsWithError — many forget to implement this method. Without it, silent registration failure goes unlogged.
Capabilities and Entitlements
In Xcode: Target → Signing & Capabilities → + Capability → Push Notifications. This adds aps-environment to .entitlements. Value — development for debug, production for release. Mismatch of this value when sending notification — most common cause of BadDeviceToken error from APNs.
Also add Background Modes → Remote notifications, if background processing needed.
Payload types
Standard alert:
{
"aps": {
"alert": {
"title": "New message",
"body": "Ivan wrote to you"
},
"badge": 3,
"sound": "default"
},
"userId": "u123",
"messageId": "m456"
}
Silent push (background update without UI):
{
"aps": {
"content-available": 1
},
"syncType": "messages"
}
Silent push requires Background Modes → Remote notifications in Capabilities. On iOS 13+ Apple limits silent push to ~3 per hour — can't use as polling replacement.
Notification Service Extension
For modifying notifications before display (decryption, image download) — NotificationServiceExtension. Separate target in Xcode, handles notifications with mutable-content: 1 in payload:
class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
guard let bestAttempt = request.content.mutableCopy() as? UNMutableNotificationContent,
let attachmentURL = request.content.userInfo["imageUrl"] as? String,
let url = URL(string: attachmentURL) else {
contentHandler(request.content)
return
}
// Download image and attach
downloadAttachment(from: url) { attachment in
if let attachment { bestAttempt.attachments = [attachment] }
contentHandler(bestAttempt)
}
}
}
Extension timeout — 30 seconds. If not completed — APNs shows original notification.
Token and its lifecycle
Device token changes: on app reinstall, on backup restore, sometimes on iOS update. Server must update token on every didRegisterForRemoteNotificationsWithDeviceToken. APNs returns 410 Gone on sending to outdated token — server must delete it.
Debugging
-
Simulator: APNs works only on physical devices (before Xcode 11.4 didn't work at all, from 11.4+ — via
.apnsfile in simulator) -
Console.app: filter by
dasdandapsdprocesses — APNs daemon logs there - Instruments → Push Notifications: delivery tracking
What's included
- APNs Auth Key (.p8) setup in Apple Developer Console
- Capabilities and Entitlements in Xcode
- Registration, token retrieval, token lifecycle handling
- Foreground / background / terminated state handling
- Notification Service Extension for rich notifications
- Silent push for background sync
- Backend integration for token storage and sending
Timeline
Basic APNs integration with alert notifications: 1 day. With rich notifications, silent push, Extension, and full token lifecycle: 2–3 days.







