deviceToken.map { String(format: "%02x", $0) }.joined() in Swift, and make sure you register via UNUserNotificationCenter and UIApplication.shared.registerForRemoteNotifications().Why this happens
When application(_:didRegisterForRemoteNotificationsWithDeviceToken:) fires, Apple Push Notification service (APNs) delivers the device token as a Data (or NSData) buffer of raw bytes, not a human-readable string. Passing deviceToken directly into print() or NSLog() logs the type’s debug description instead of a usable hex token—often appearing in the console as 32 bytes or {length = 32, bytes = 0x...}.
Older tutorials commonly worked around this by calling [deviceToken description] and stripping <, >, and spaces. Starting in iOS 13, Apple changed NSData.description to include metadata ({length = 32, bytes = ...}), which breaks any string-trimming parser and leaves you with malformed characters or truncated output.
In addition, apps built on older UIApplication APIs such as registerForRemoteNotificationTypes: rely on methods deprecated since iOS 8. If registration itself is misconfigured or missing the Push Notifications capability, didRegisterForRemoteNotificationsWithDeviceToken may never be called at all.
How to fix it
1. Convert deviceToken Data to a hex string in Swift
Instead of reading description, iterate over the raw bytes of Data and format each byte as a zero-padded, two-digit lowercase hexadecimal value:
import UIKit
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let tokenString = deviceToken.map { String(format: "%02x", $0) }.joined()
print("APNs Device Token: \(tokenString)")
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register for remote notifications: \(error.localizedDescription)")
}
Using %02x ensures every byte produces two hex characters (for example, 0x0a becomes 0a rather than a), preserving the full 64-character token that APNs expects. Always implement didFailToRegisterForRemoteNotificationsWithError alongside the success delegate method so registration failures surface immediately in the Xcode console.
2. Convert deviceToken NSData to a hex string in Objective-C
If your project still uses an Objective-C AppDelegate, read the underlying bytes buffer directly instead of calling [deviceToken description]:
+ (NSString *)hexadecimalStringFromData:(NSData *)data {
NSUInteger dataLength = data.length;
if (dataLength == 0) {
return nil;
}
const unsigned char *dataBuffer = (const unsigned char *)data.bytes;
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
for (NSUInteger i = 0; i < dataLength; ++i) {
[hexString appendFormat:@"%02x", dataBuffer[i]];
}
return [hexString copy];
}
- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSString *tokenString = [[self class] hexadecimalStringFromData:deviceToken];
NSLog(@"APNs Device Token: %@", tokenString);
}
- (void)application:(UIApplication *)application
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
NSLog(@"Failed to register for remote notifications: %@", error);
}
Because this reads raw bytes from data.bytes, it behaves identically across all iOS versions and is unaffected by NSData.description formatting changes.
3. Register for remote notifications with UserNotifications
If didRegisterForRemoteNotificationsWithDeviceToken is never called, replace deprecated registerForRemoteNotificationTypes: calls with UNUserNotificationCenter and registerForRemoteNotifications():
import UIKit
import UserNotifications
func registerForPushNotifications() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
print("Notification authorization error: \(error.localizedDescription)")
return
}
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
Note that UIApplication.shared.registerForRemoteNotifications() must be called on the main thread, as requestAuthorization executes its completion handler on a background queue. Also keep in mind that silent/background pushes can register even without user alert permission, but standard user-facing notifications require authorization.
4. Handle device tokens in a SwiftUI App lifecycle
Apps using the SwiftUI App protocol do not have an AppDelegate.swift file by default. Attach a UIApplicationDelegate using @UIApplicationDelegateAdaptor so you can receive the APNs callback:
import SwiftUI
import UIKit
import UserNotifications
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("APNs Device Token: \(token)")
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("APNs registration failed: \(error.localizedDescription)")
}
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
5. Verify capabilities and device requirements
If your code is correct but neither delegate callback fires:
- Open your project in Xcode, select your app target, and go to Signing & Capabilities.
- Click + Capability and add Push Notifications if it is not already listed.
- Confirm your Apple Developer provisioning profile supports Push Notifications (automatic signing handles this for paid Apple Developer accounts; free personal teams cannot enable Push Notifications).
- Test on a physical iPhone or iPad with an active internet connection so the device can reach APNs.
How to prevent it
- Never parse
deviceToken.descriptionorString(describing: deviceToken)in production code; always format the rawDatabytes directly. - Call
UIApplication.shared.registerForRemoteNotifications()insideDispatchQueue.main.asyncon every app launch rather than caching the token permanently, because APNs can rotate device tokens. - Implement
didFailToRegisterForRemoteNotificationsWithErrorfrom day one so missing entitlements or offline registration failures are logged clearly. - Use
UNUserNotificationCenterinstead of deprecatedUIRemoteNotificationTypeAPIs.
FAQ
Why does my deviceToken print as “32 bytes” in Swift?
In Swift, Data.description returns a summary string such as 32 bytes rather than the hex contents of the buffer. Map each byte with String(format: "%02x", $0) and join the array to get the 64-character hex string.
Should I use %02x or %02X for the APNs token?
Both lowercase (%02x) and uppercase (%02X) hex characters represent the same binary token, though lowercase %02x is the standard convention in Apple documentation and push provider SDKs. Consistency with your backend or push service is what matters.
Does the APNs device token stay the same forever?
No. APNs can issue a new device token when a user restores a device from a backup, installs the app on a new device, or reinstalls iOS. Always call registerForRemoteNotifications() at launch and send the latest token to your server whenever it changes.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
