push-notifications

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Push Notifications

推送通知

Implement, review, and debug local and remote notifications on iOS/macOS using
UserNotifications
and APNs. Covers permission flow, token registration, payload structure, foreground handling, notification actions, grouping, and rich notifications. Targets iOS 26+ with Swift 6.2, backward-compatible to iOS 16 unless noted.
使用
UserNotifications
和APNs在iOS/macOS上实现、审核和调试本地与远程通知。内容涵盖权限流程、令牌注册、负载结构、前台处理、通知操作、分组以及富通知。目标平台为iOS 26+,使用Swift 6.2,除非特别说明,否则向下兼容至iOS 16。

Permission Flow

权限流程

Request notification authorization before doing anything else. The system prompt appears only once; subsequent calls return the stored decision.
swift
import UserNotifications

@MainActor
func requestNotificationPermission() async -> Bool {
    let center = UNUserNotificationCenter.current()
    do {
        let granted = try await center.requestAuthorization(
            options: [.alert, .sound, .badge]
        )
        return granted
    } catch {
        print("Authorization request failed: \(error)")
        return false
    }
}
在进行任何操作前先请求通知授权。系统提示仅会显示一次;后续调用将返回已存储的用户决策。
swift
import UserNotifications

@MainActor
func requestNotificationPermission() async -> Bool {
    let center = UNUserNotificationCenter.current()
    do {
        let granted = try await center.requestAuthorization(
            options: [.alert, .sound, .badge]
        )
        return granted
    } catch {
        print("Authorization request failed: \(error)")
        return false
    }
}

Checking Current Status

检查当前状态

Always check status before assuming permissions. The user can change settings at any time.
swift
@MainActor
func checkNotificationStatus() async -> UNAuthorizationStatus {
    let settings = await UNUserNotificationCenter.current().notificationSettings()
    return settings.authorizationStatus
    // .notDetermined, .denied, .authorized, .provisional, .ephemeral
}
永远不要假设权限已开启,操作前务必检查状态。用户可随时更改设置。
swift
@MainActor
func checkNotificationStatus() async -> UNAuthorizationStatus {
    let settings = await UNUserNotificationCenter.current().notificationSettings()
    return settings.authorizationStatus
    // .notDetermined, .denied, .authorized, .provisional, .ephemeral
}

Provisional Notifications

临时通知

Provisional notifications deliver quietly to the notification center without interrupting the user. The user can then choose to keep or turn them off. Use for onboarding flows where you want to demonstrate value before asking for full permission.
swift
// Delivers silently -- no permission prompt shown to the user
try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])
临时通知会静默投递至通知中心,不会打扰用户。之后用户可选择保留或关闭此类通知。适用于引导流程,可在请求完整权限前向用户展示通知价值。
swift
// 静默投递——不会向用户显示权限提示
try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])

Critical Alerts

关键提醒

Critical alerts bypass Do Not Disturb and the mute switch. Requires a special entitlement from Apple (request via developer portal). Use only for health, safety, or security scenarios.
swift
// Requires com.apple.developer.usernotifications.critical-alerts entitlement
try await center.requestAuthorization(
    options: [.alert, .sound, .badge, .criticalAlert]
)
关键提醒可绕过“勿扰模式”和静音开关。需要向Apple申请特殊权限(通过开发者门户提交请求)。仅可用于健康、安全或安保场景。
swift
// 需要com.apple.developer.usernotifications.critical-alerts权限
try await center.requestAuthorization(
    options: [.alert, .sound, .badge, .criticalAlert]
)

Handling Denied Permissions

处理权限被拒的情况

When the user has denied notifications, guide them to Settings. Do not repeatedly prompt or nag.
swift
@MainActor
func openNotificationSettings() {
    guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
    UIApplication.shared.open(url)
}

// Usage: show a button/banner explaining why notifications matter,
// then call openNotificationSettings() on tap.
当用户拒绝通知权限时,引导他们前往设置。不要反复提示或骚扰用户。
swift
@MainActor
func openNotificationSettings() {
    guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
    UIApplication.shared.open(url)
}

// 使用方式:展示按钮/横幅说明通知的重要性,
// 点击时调用openNotificationSettings()。

APNs Registration

APNs注册

Use
UIApplicationDelegateAdaptor
to receive the device token in a SwiftUI app. The AppDelegate callbacks are the only way to receive APNs tokens.
swift
@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
        return true
    }

    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()
        print("APNs token: \(token)")
        // Send token to your server
        Task { await TokenService.shared.upload(token: token) }
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("APNs registration failed: \(error.localizedDescription)")
        // Simulator always fails -- this is expected during development
    }
}
在SwiftUI应用中使用
UIApplicationDelegateAdaptor
接收设备令牌。AppDelegate回调是接收APNs令牌的唯一方式。
swift
@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
        return true
    }

    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()
        print("APNs token: \(token)")
        // 将令牌发送至你的服务器
        Task { await TokenService.shared.upload(token: token) }
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("APNs registration failed: \(error.localizedDescription)")
        // 模拟器中注册总会失败——这属于开发期间的正常情况
    }
}

Registration Order

注册顺序

Request authorization first, then register for remote notifications. Registration triggers the system to contact APNs and return a device token.
swift
@MainActor
func registerForPush() async {
    let granted = await requestNotificationPermission()
    guard granted else { return }
    UIApplication.shared.registerForRemoteNotifications()
}
先请求授权,再注册远程通知。注册操作会触发系统联系APNs并返回设备令牌。
swift
@MainActor
func registerForPush() async {
    let granted = await requestNotificationPermission()
    guard granted else { return }
    UIApplication.shared.registerForRemoteNotifications()
}

Token Handling

令牌处理

Device tokens change. Re-send the token to your server every time
didRegisterForRemoteNotificationsWithDeviceToken
fires, not just the first time. The system calls this method on every app launch that calls
registerForRemoteNotifications()
.
设备令牌会发生变化。每次
didRegisterForRemoteNotificationsWithDeviceToken
触发时,都要重新将令牌发送至服务器,而不只是第一次。每次调用
registerForRemoteNotifications()
的应用启动时,系统都会调用该方法。

Local Notifications

本地通知

Schedule notifications directly from the device without a server. Useful for reminders, timers, and location-based alerts.
直接在设备上调度通知,无需服务器。适用于提醒、计时器和基于位置的警报。

Creating Content

创建通知内容

swift
let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.subtitle = "Time to move"
content.body = "You have a scheduled workout in 15 minutes."
content.sound = .default
content.badge = 1
content.userInfo = ["workoutId": "abc123"]
content.threadIdentifier = "workouts"  // groups in notification center
swift
let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.subtitle = "Time to move"
content.body = "You have a scheduled workout in 15 minutes."
content.sound = .default
content.badge = 1
content.userInfo = ["workoutId": "abc123"]
content.threadIdentifier = "workouts"  // 在通知中心分组

Trigger Types

触发器类型

swift
// Fire after a time interval (minimum 60 seconds for repeating)
let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)

// Fire at a specific date/time
var dateComponents = DateComponents()
dateComponents.hour = 8
dateComponents.minute = 30
let calendarTrigger = UNCalendarNotificationTrigger(
    dateMatching: dateComponents, repeats: true  // daily at 8:30 AM
)

// Fire when entering a geographic region
let region = CLCircularRegion(
    center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01),
    radius: 100,
    identifier: "gym"
)
region.notifyOnEntry = true
region.notifyOnExit = false
let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
// Requires "When In Use" location permission at minimum
swift
// 时间间隔后触发(重复触发的最小间隔为60秒)
let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)

// 在特定日期/时间触发
var dateComponents = DateComponents()
dateComponents.hour = 8
dateComponents.minute = 30
let calendarTrigger = UNCalendarNotificationTrigger(
    dateMatching: dateComponents, repeats: true  // 每天早上8:30触发
)

// 进入地理区域时触发
let region = CLCircularRegion(
    center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01),
    radius: 100,
    identifier: "gym"
)
region.notifyOnEntry = true
region.notifyOnExit = false
let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
// 至少需要“使用期间”位置权限

Scheduling and Managing

调度与管理

swift
let request = UNNotificationRequest(
    identifier: "workout-reminder-abc123",
    content: content,
    trigger: timeTrigger
)

let center = UNUserNotificationCenter.current()
try await center.add(request)

// Remove specific pending notifications
center.removePendingNotificationRequests(withIdentifiers: ["workout-reminder-abc123"])

// Remove all pending
center.removeAllPendingNotificationRequests()

// Remove delivered notifications from notification center
center.removeDeliveredNotifications(withIdentifiers: ["workout-reminder-abc123"])
center.removeAllDeliveredNotifications()

// List all pending requests
let pending = await center.pendingNotificationRequests()
swift
let request = UNNotificationRequest(
    identifier: "workout-reminder-abc123",
    content: content,
    trigger: timeTrigger
)

let center = UNUserNotificationCenter.current()
try await center.add(request)

// 移除特定的待处理通知
center.removePendingNotificationRequests(withIdentifiers: ["workout-reminder-abc123"])

// 移除所有待处理通知
center.removeAllPendingNotificationRequests()

// 从通知中心移除已投递的通知
center.removeDeliveredNotifications(withIdentifiers: ["workout-reminder-abc123"])
center.removeAllDeliveredNotifications()

// 列出所有待处理请求
let pending = await center.pendingNotificationRequests()

Remote Notification Payload

远程通知负载

Standard APNs Payload

标准APNs负载

json
{
    "aps": {
        "alert": {
            "title": "New Message",
            "subtitle": "From Alice",
            "body": "Hey, are you free for lunch?"
        },
        "badge": 3,
        "sound": "default",
        "thread-id": "chat-alice",
        "category": "MESSAGE_CATEGORY"
    },
    "messageId": "msg-789",
    "senderId": "user-alice"
}
json
{
    "aps": {
        "alert": {
            "title": "New Message",
            "subtitle": "From Alice",
            "body": "Hey, are you free for lunch?"
        },
        "badge": 3,
        "sound": "default",
        "thread-id": "chat-alice",
        "category": "MESSAGE_CATEGORY"
    },
    "messageId": "msg-789",
    "senderId": "user-alice"
}

Silent / Background Push

静默/后台推送

Set
content-available: 1
with no alert, sound, or badge. The system wakes the app in the background. Requires the "Background Modes > Remote notifications" capability.
json
{
    "aps": {
        "content-available": 1
    },
    "updateType": "new-data"
}
Handle in AppDelegate:
swift
func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
    guard let updateType = userInfo["updateType"] as? String else {
        return .noData
    }
    do {
        try await DataSyncService.shared.sync(trigger: updateType)
        return .newData
    } catch {
        return .failed
    }
}
设置
content-available: 1
,且不包含提醒、声音或角标。系统会在后台唤醒应用。需要开启“后台模式>远程通知”权限。
json
{
    "aps": {
        "content-available": 1
    },
    "updateType": "new-data"
}
在AppDelegate中处理:
swift
func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
    guard let updateType = userInfo["updateType"] as? String else {
        return .noData
    }
    do {
        try await DataSyncService.shared.sync(trigger: updateType)
        return .newData
    } catch {
        return .failed
    }
}

Mutable Content

可变内容

Set
mutable-content: 1
to allow a Notification Service Extension to modify content before display. Use for downloading images, decrypting content, or adding attachments.
json
{
    "aps": {
        "alert": { "title": "Photo", "body": "Alice sent a photo" },
        "mutable-content": 1
    },
    "imageUrl": "https://example.com/photo.jpg"
}
设置
mutable-content: 1
,允许通知服务扩展在展示前修改内容。可用于下载图片、解密内容或添加附件。
json
{
    "aps": {
        "alert": { "title": "Photo", "body": "Alice sent a photo" },
        "mutable-content": 1
    },
    "imageUrl": "https://example.com/photo.jpg"
}

Localized Notifications

本地化通知

Use localization keys so the notification displays in the user's language:
json
{
    "aps": {
        "alert": {
            "title-loc-key": "NEW_MESSAGE_TITLE",
            "loc-key": "NEW_MESSAGE_BODY",
            "loc-args": ["Alice"]
        }
    }
}
使用本地化键,让通知以用户的语言显示:
json
{
    "aps": {
        "alert": {
            "title-loc-key": "NEW_MESSAGE_TITLE",
            "loc-key": "NEW_MESSAGE_BODY",
            "loc-args": ["Alice"]
        }
    }
}

Notification Handling

通知处理

UNUserNotificationCenterDelegate

UNUserNotificationCenterDelegate

Implement the delegate to control foreground display and handle user taps. Set the delegate as early as possible -- in
application(_:didFinishLaunchingWithOptions:)
or
App.init
.
swift
@MainActor
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
    static let shared = NotificationDelegate()

    // Called when notification arrives while app is in FOREGROUND
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        // Return which presentation elements to show
        // Without this, foreground notifications are silently suppressed
        return [.banner, .sound, .badge]
    }

    // Called when user TAPS the notification
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let userInfo = response.notification.request.content.userInfo
        let actionIdentifier = response.actionIdentifier

        switch actionIdentifier {
        case UNNotificationDefaultActionIdentifier:
            // User tapped the notification body
            await handleNotificationTap(userInfo: userInfo)
        case UNNotificationDismissActionIdentifier:
            // User dismissed the notification
            break
        default:
            // Custom action button tapped
            await handleCustomAction(actionIdentifier, userInfo: userInfo)
        }
    }
}
实现代理以控制前台展示并处理用户点击。尽早设置代理——在
application(_:didFinishLaunchingWithOptions:)
App.init
中。
swift
@MainActor
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
    static let shared = NotificationDelegate()

    // 应用在前台时收到通知时调用
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        // 返回要展示的元素
        // 不实现此方法的话,前台通知会被静默抑制
        return [.banner, .sound, .badge]
    }

    // 用户点击通知时调用
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let userInfo = response.notification.request.content.userInfo
        let actionIdentifier = response.actionIdentifier

        switch actionIdentifier {
        case UNNotificationDefaultActionIdentifier:
            // 用户点击了通知主体
            await handleNotificationTap(userInfo: userInfo)
        case UNNotificationDismissActionIdentifier:
            // 用户 dismissed 通知
            break
        default:
            // 用户点击了自定义操作按钮
            await handleCustomAction(actionIdentifier, userInfo: userInfo)
        }
    }
}

Deep Linking from Notifications

从通知跳转至深层链接

Route notification taps to the correct screen using a shared
@Observable
router. The delegate writes a pending destination; the SwiftUI view observes and consumes it.
swift
@Observable @MainActor
final class DeepLinkRouter {
    var pendingDestination: AppDestination?
}

// In NotificationDelegate:
func handleNotificationTap(userInfo: [AnyHashable: Any]) async {
    guard let id = userInfo["messageId"] as? String else { return }
    DeepLinkRouter.shared.pendingDestination = .chat(id: id)
}

// In SwiftUI -- observe and consume:
.onChange(of: router.pendingDestination) { _, destination in
    if let destination {
        path.append(destination)
        router.pendingDestination = nil
    }
}
See
references/notification-patterns.md
for the full deep-linking handler with tab switching.
使用共享的
@Observable
路由器将通知点击路由至正确的屏幕。代理写入待处理的目标,SwiftUI视图监听并处理该目标。
swift
@Observable @MainActor
final class DeepLinkRouter {
    var pendingDestination: AppDestination?
}

// 在NotificationDelegate中:
func handleNotificationTap(userInfo: [AnyHashable: Any]) async {
    guard let id = userInfo["messageId"] as? String else { return }
    DeepLinkRouter.shared.pendingDestination = .chat(id: id)
}

// 在SwiftUI中——监听并处理:
.onChange(of: router.pendingDestination) { _, destination in
    if let destination {
        path.append(destination)
        router.pendingDestination = nil
    }
}
查看
references/notification-patterns.md
获取完整的深层链接处理逻辑,包括标签页切换。

Notification Actions and Categories

通知操作与类别

Define interactive actions that appear as buttons on the notification. Register categories at launch.
定义显示为通知按钮的交互式操作。在应用启动时注册类别。

Defining Categories and Actions

定义类别与操作

swift
func registerNotificationCategories() {
    let replyAction = UNTextInputNotificationAction(
        identifier: "REPLY_ACTION",
        title: "Reply",
        options: [],
        textInputButtonTitle: "Send",
        textInputPlaceholder: "Type a reply..."
    )

    let likeAction = UNNotificationAction(
        identifier: "LIKE_ACTION",
        title: "Like",
        options: []
    )

    let deleteAction = UNNotificationAction(
        identifier: "DELETE_ACTION",
        title: "Delete",
        options: [.destructive, .authenticationRequired]
    )

    let messageCategory = UNNotificationCategory(
        identifier: "MESSAGE_CATEGORY",
        actions: [replyAction, likeAction, deleteAction],
        intentIdentifiers: [],
        options: [.customDismissAction]  // fires didReceive on dismiss too
    )

    UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}
swift
func registerNotificationCategories() {
    let replyAction = UNTextInputNotificationAction(
        identifier: "REPLY_ACTION",
        title: "Reply",
        options: [],
        textInputButtonTitle: "Send",
        textInputPlaceholder: "Type a reply..."
    )

    let likeAction = UNNotificationAction(
        identifier: "LIKE_ACTION",
        title: "Like",
        options: []
    )

    let deleteAction = UNNotificationAction(
        identifier: "DELETE_ACTION",
        title: "Delete",
        options: [.destructive, .authenticationRequired]
    )

    let messageCategory = UNNotificationCategory(
        identifier: "MESSAGE_CATEGORY",
        actions: [replyAction, likeAction, deleteAction],
        intentIdentifiers: [],
        options: [.customDismissAction]  //  Dismiss时也会触发didReceive
    )

    UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}

Handling Action Responses

处理操作响应

swift
func handleCustomAction(_ identifier: String, userInfo: [AnyHashable: Any]) async {
    switch identifier {
    case "REPLY_ACTION":
        // response is UNTextInputNotificationResponse for text input actions
        break
    case "LIKE_ACTION":
        guard let messageId = userInfo["messageId"] as? String else { return }
        await MessageService.shared.likeMessage(id: messageId)
    case "DELETE_ACTION":
        guard let messageId = userInfo["messageId"] as? String else { return }
        await MessageService.shared.deleteMessage(id: messageId)
    default:
        break
    }
}
Action options:
  • .authenticationRequired
    -- device must be unlocked to perform the action
  • .destructive
    -- displayed in red; use for delete/remove actions
  • .foreground
    -- launches the app to the foreground when tapped
swift
func handleCustomAction(_ identifier: String, userInfo: [AnyHashable: Any]) async {
    switch identifier {
    case "REPLY_ACTION":
        // 对于文本输入操作,response是UNTextInputNotificationResponse
        break
    case "LIKE_ACTION":
        guard let messageId = userInfo["messageId"] as? String else { return }
        await MessageService.shared.likeMessage(id: messageId)
    case "DELETE_ACTION":
        guard let messageId = userInfo["messageId"] as? String else { return }
        await MessageService.shared.deleteMessage(id: messageId)
    default:
        break
    }
}
操作选项:
  • .authenticationRequired
    —— 执行操作前必须解锁设备
  • .destructive
    —— 红色显示;用于删除/移除操作
  • .foreground
    —— 点击时将应用启动至前台

Notification Grouping

通知分组

Group related notifications with
threadIdentifier
(or
thread-id
in the APNs payload). Each unique thread becomes a separate group in Notification Center.
swift
content.threadIdentifier = "chat-alice"  // all messages from Alice group together
content.summaryArgument = "Alice"
content.summaryArgumentCount = 3         // "3 more notifications from Alice"
Customize the summary format string in the category:
swift
let category = UNNotificationCategory(
    identifier: "MESSAGE_CATEGORY",
    actions: [replyAction],
    intentIdentifiers: [],
    categorySummaryFormat: "%u more messages from %@",
    options: []
)
使用
threadIdentifier
(或APNs负载中的
thread-id
)对相关通知进行分组。每个唯一的线程会在通知中心形成一个独立的分组。
swift
content.threadIdentifier = "chat-alice"  // 来自Alice的所有消息会分组在一起
content.summaryArgument = "Alice"
content.summaryArgumentCount = 3         // “来自Alice的3条更多通知”
在类别中自定义摘要格式字符串:
swift
let category = UNNotificationCategory(
    identifier: "MESSAGE_CATEGORY",
    actions: [replyAction],
    intentIdentifiers: [],
    categorySummaryFormat: "%u more messages from %@",
    options: []
)

Common Mistakes

常见错误

DON'T: Register for remote notifications before requesting authorization. DO: Call
requestAuthorization
first, check the result, then call
registerForRemoteNotifications()
.
DON'T: Convert the device token with
String(data: deviceToken, encoding: .utf8)
-- this produces garbage or nil. DO: Convert token bytes to a hex string:
deviceToken.map { String(format: "%02x", $0) }.joined()
.
DON'T: Assume notifications always arrive. APNs is best-effort delivery; the system may throttle or drop notifications. DO: Design features that degrade gracefully without notifications. Use background refresh as a fallback.
DON'T: Put sensitive data directly in the notification payload. It is visible on the lock screen and in notification center. DO: Use
mutable-content: 1
with a Notification Service Extension to fetch sensitive content from your server.
DON'T: Forget to handle the foreground case. Without
willPresent
, notifications are silently suppressed when the app is active. DO: Implement
willPresent
and return the desired presentation options (
.banner
,
.sound
,
.badge
).
DON'T: Set
UNUserNotificationCenter.current().delegate
too late (e.g., in a view's
.onAppear
). DO: Set the delegate in
App.init
,
application(_:didFinishLaunchingWithOptions:)
, or the AppDelegate adaptor.
DON'T: Call
UIApplication.shared.registerForRemoteNotifications()
from a SwiftUI view without an AppDelegate adaptor. There is no SwiftUI-native way to receive the token callback. DO: Use
UIApplicationDelegateAdaptor
for all APNs registration and token handling.
DON'T: Ignore
didFailToRegisterForRemoteNotificationsWithError
. Log it so device failures surface. On simulator, fail silently.
DON'T: Send the device token to your server only once. Tokens change periodically. DO: Re-send on every
didRegisterForRemoteNotificationsWithDeviceToken
call.
不要: 在请求授权前注册远程通知。 要: 先调用
requestAuthorization
,检查结果,再调用
registerForRemoteNotifications()
不要: 使用
String(data: deviceToken, encoding: .utf8)
转换设备令牌——这会产生无效内容或nil。 要: 将令牌字节转换为十六进制字符串:
deviceToken.map { String(format: "%02x", $0) }.joined()
不要: 假设通知总会投递成功。APNs是尽力而为的投递机制;系统可能会限流或丢弃通知。 要: 设计无通知时也能正常降级的功能。使用后台刷新作为备选方案。
不要: 将敏感数据直接放入通知负载中。它会在锁屏和通知中心显示。 要: 使用
mutable-content: 1
搭配通知服务扩展从服务器获取敏感内容。
不要: 忘记处理前台场景。没有
willPresent
的话,应用在活跃状态时通知会被静默抑制。 要: 实现
willPresent
并返回所需的展示选项(
.banner
,
.sound
,
.badge
)。
不要: 太晚设置
UNUserNotificationCenter.current().delegate
(例如在视图的
.onAppear
中)。 要:
App.init
application(_:didFinishLaunchingWithOptions:)
或AppDelegate适配器中设置代理。
不要: 在没有AppDelegate适配器的SwiftUI视图中调用
UIApplication.shared.registerForRemoteNotifications()
。目前没有SwiftUI原生方式接收令牌回调。 要: 所有APNs注册和令牌处理都使用
UIApplicationDelegateAdaptor
不要: 忽略
didFailToRegisterForRemoteNotificationsWithError
。记录该错误,以便发现设备故障。在模拟器中可静默忽略。
不要: 仅将设备令牌发送至服务器一次。令牌会定期变化。 要: 每次
didRegisterForRemoteNotificationsWithDeviceToken
调用时重新发送令牌。

Review Checklist

审核清单

  • Authorization requested before registering for remote notifications
  • Device token converted to hex string (not
    String(data:encoding:)
    )
  • UNUserNotificationCenterDelegate
    set in
    App.init
    or
    application(_:didFinishLaunching:)
  • Foreground notification handling implemented (
    willPresent
    )
  • Notification tap handling implemented with deep linking (
    didReceive
    )
  • Categories and actions registered at launch if interactive notifications needed
  • Badge count managed (reset on app open if appropriate)
  • Silent push background handling configured (Background Modes capability enabled)
  • UIApplicationDelegateAdaptor
    used for APNs token callbacks in SwiftUI apps
  • Sensitive data not included directly in payload (uses service extension)
  • Notification grouping configured with
    threadIdentifier
    where applicable
  • Denied permission case handled gracefully (Settings link)
  • 注册远程通知前已请求授权
  • 设备令牌已转换为十六进制字符串(而非
    String(data:encoding:)
  • UNUserNotificationCenterDelegate
    已在
    App.init
    application(_:didFinishLaunching:)
    中设置
  • 已实现前台通知处理(
    willPresent
  • 已实现通知点击处理并支持深层链接(
    didReceive
  • 若需交互式通知,已在启动时注册类别与操作
  • 已管理角标计数(如适用,应用打开时重置)
  • 已配置静默推送后台处理(已启用后台模式权限)
  • SwiftUI应用中使用
    UIApplicationDelegateAdaptor
    处理APNs令牌回调
  • 敏感数据未直接包含在负载中(使用服务扩展)
  • 已使用
    threadIdentifier
    配置通知分组(如适用)
  • 已优雅处理权限被拒的情况(提供设置链接)

References

参考资料

  • references/notification-patterns.md
    — AppDelegate setup, delegate implementation, deep-link router, silent push, scheduling, token refresh, debugging.
  • references/rich-notifications.md
    — Service Extension (media, decryption), Content Extension (custom UI), attachments, communication notifications.
  • references/notification-patterns.md
    —— AppDelegate设置、代理实现、深层链接路由器、静默推送、调度、令牌刷新、调试。
  • references/rich-notifications.md
    —— 服务扩展(媒体、解密)、内容扩展(自定义UI)、附件、通信通知。