push-notifications

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Push Notifications — Expert Decisions

推送通知——专家决策指南

Expert decision frameworks for notification choices. Claude knows UNUserNotificationCenter and APNs — this skill provides judgment calls for permission timing, delivery strategies, and architecture trade-offs.

为通知相关选择提供专家级决策框架。Claude熟悉UNUserNotificationCenter与APNs——本技能为权限请求时机、送达策略及架构取舍提供判断依据。

Decision Trees

决策树

Permission Request Timing

权限请求时机

When should you ask for notification permission?
├─ User explicitly wants notifications
│  └─ After user taps "Enable Notifications" button
│     Highest acceptance rate (70-80%)
├─ After demonstrating value
│  └─ After user completes key action
│     "Get notified when your order ships?"
│     Context-specific, 50-60% acceptance
├─ First meaningful moment
│  └─ After onboarding, before home screen
│     Explain why, 30-40% acceptance
└─ On app launch
   └─ AVOID — lowest acceptance (15-20%)
      No context, feels intrusive
The trap: Requesting permission on first launch. Users deny reflexively. Wait for a moment when notifications clearly add value.
何时请求通知权限?
├─ 用户明确想要通知
│  └─ 用户点击「启用通知」按钮后
│     接受率最高(70-80%)
├─ 体现价值之后
│  └─ 用户完成关键操作后
│     「订单发货时通知您?」
│     场景特定,接受率50-60%
├─ 首次有意义时刻
│  └─ 引导流程完成后,进入主屏幕前
│     说明原因,接受率30-40%
└─ 应用启动时
   └─ 避免——接受率最低(15-20%)
      无上下文,显得突兀
误区:应用启动即请求权限。用户会本能拒绝。应等待通知能明确体现价值的时机。

Silent vs Visible Notification

静默通知 vs 可见通知

What's the notification purpose?
├─ Background data sync
│  └─ Silent notification (content-available: 1)
│     No user interruption, wakes app
├─ User needs to know immediately
│  └─ Visible alert
│     Messages, time-sensitive info
├─ Informational, not urgent
│  └─ Badge + silent
│     User sees count, checks when ready
└─ Needs user action
   └─ Visible with actions
      Reply, accept/decline buttons
通知的用途是什么?
├─ 后台数据同步
│  └─ 静默通知(content-available: 1)
│     不打扰用户,唤醒应用
├─ 用户需立即知晓
│  └─ 可见提醒
│     消息、时间敏感信息
├─ 信息类,非紧急
│  └─ 角标 + 静默通知
│     用户看到角标后,可自行查看
└─ 需要用户操作
   └─ 带操作按钮的可见通知
      回复、接受/拒绝按钮

Notification Extension Strategy

通知扩展策略

Do you need to modify notifications?
├─ Download images/media
│  └─ Notification Service Extension
│     mutable-content: 1 in payload
├─ Decrypt end-to-end encrypted content
│  └─ Notification Service Extension
│     Required for E2EE messaging
├─ Custom notification UI
│  └─ Notification Content Extension
│     Long-press/3D Touch custom view
└─ Standard text/badge
   └─ No extension needed
      Less complexity, faster delivery
是否需要修改通知内容?
├─ 下载图片/媒体
│  └─ Notification Service Extension
│     payload中设置mutable-content: 1
├─ 解密端到端加密内容
│  └─ Notification Service Extension
│     E2EE消息必备
├─ 自定义通知UI
│  └─ Notification Content Extension
│     长按/3D Touch自定义视图
└─ 标准文本/角标
   └─ 无需扩展
      复杂度更低,送达速度更快

Token Management

令牌管理

How should you handle device tokens?
├─ Single device per user
│  └─ Replace token on registration
│     Simple, most apps need this
├─ Multiple devices per user
│  └─ Register all tokens
│     Send to all active devices
├─ Token changed (reinstall/restore)
│  └─ Deduplicate on server
│     Same device, new token
└─ User logged out
   └─ Deregister token from user
      Prevents notifications to wrong user

如何处理设备令牌?
├─ 单用户单设备
│  └─ 注册时替换令牌
│     简单,多数应用适用
├─ 单用户多设备
│  └─ 注册所有令牌
│     向所有活跃设备发送通知
├─ 令牌变更(重装/恢复)
│  └─ 服务器端去重
│     同一设备,新令牌
└─ 用户登出
   └─ 从用户账号中注销令牌
      防止通知发送给错误用户

NEVER Do

绝对禁忌

Permission Handling

权限处理

NEVER request permission without context:
swift
// ❌ First thing on app launch — user denies
func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in }
    return true
}

// ✅ After user action that demonstrates value
func userTappedEnableNotifications() {
    showPrePermissionExplanation {
        Task {
            let granted = try? await UNUserNotificationCenter.current()
                .requestAuthorization(options: [.alert, .badge, .sound])
            if granted == true {
                await MainActor.run { registerForRemoteNotifications() }
            }
        }
    }
}
NEVER ignore denied permission:
swift
// ❌ Keeps trying, annoys user
func checkNotifications() {
    Task {
        let settings = await UNUserNotificationCenter.current().notificationSettings()
        if settings.authorizationStatus == .denied {
            // Ask again!  <- User already said no
            requestPermission()
        }
    }
}

// ✅ Respect denial, offer settings path
func checkNotifications() {
    Task {
        let settings = await UNUserNotificationCenter.current().notificationSettings()
        switch settings.authorizationStatus {
        case .denied:
            showSettingsPrompt()  // "Enable in Settings to receive..."
        case .notDetermined:
            showPrePermissionScreen()
        case .authorized, .provisional, .ephemeral:
            ensureRegistered()
        @unknown default:
            break
        }
    }
}
绝对不要无上下文请求权限:
swift
// ❌ 应用启动即请求——用户会拒绝
func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in }
    return true
}

// ✅ 在用户完成体现价值的操作后
func userTappedEnableNotifications() {
    showPrePermissionExplanation {
        Task {
            let granted = try? await UNUserNotificationCenter.current()
                .requestAuthorization(options: [.alert, .badge, .sound])
            if granted == true {
                await MainActor.run { registerForRemoteNotifications() }
            }
        }
    }
}
绝对不要忽略已拒绝的权限:
swift
// ❌ 持续尝试,惹恼用户
func checkNotifications() {
    Task {
        let settings = await UNUserNotificationCenter.current().notificationSettings()
        if settings.authorizationStatus == .denied {
            // 再次请求! <- 用户已经拒绝过
            requestPermission()
        }
    }
}

// ✅ 尊重用户拒绝,提供设置路径
func checkNotifications() {
    Task {
        let settings = await UNUserNotificationCenter.current().notificationSettings()
        switch settings.authorizationStatus {
        case .denied:
            showSettingsPrompt()  // 「前往设置启用通知...」
        case .notDetermined:
            showPrePermissionScreen()
        case .authorized, .provisional, .ephemeral:
            ensureRegistered()
        @unknown default:
            break
        }
    }
}

Token Handling

令牌处理

NEVER cache device tokens long-term in app:
swift
// ❌ Token may change without app knowing
class TokenManager {
    static var cachedToken: String?  // Stale after reinstall!

    func getToken() -> String? {
        return Self.cachedToken  // May be invalid
    }
}

// ✅ Always use fresh token from registration callback
func application(_ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.hexString

    // Send to server immediately — this is the source of truth
    Task {
        await sendTokenToServer(token)
    }
}
NEVER assume token format:
swift
// ❌ Token format is not guaranteed
let tokenString = String(data: deviceToken, encoding: .utf8)  // Returns nil!

// ✅ Convert bytes to hex
extension Data {
    var hexString: String {
        map { String(format: "%02x", $0) }.joined()
    }
}

let tokenString = deviceToken.hexString
绝对不要在应用中长期缓存设备令牌:
swift
// ❌ 令牌可能在应用不知情的情况下变更
class TokenManager {
    static var cachedToken: String?  // 重装后失效!

    func getToken() -> String? {
        return Self.cachedToken  // 可能无效
    }
}

// ✅ 始终使用注册回调中的新鲜令牌
func application(_ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.hexString

    // 立即发送到服务器——这是可信来源
    Task {
        await sendTokenToServer(token)
    }
}
绝对不要假设令牌格式:
swift
// ❌ 令牌格式不固定
let tokenString = String(data: deviceToken, encoding: .utf8)  // 返回nil!

// ✅ 将字节转换为十六进制
extension Data {
    var hexString: String {
        map { String(format: "%02x", $0) }.joined()
    }
}

let tokenString = deviceToken.hexString

Silent Notifications

静默通知

NEVER rely on silent notifications for time-critical delivery:
swift
// ❌ Silent notifications are low priority
// Server sends: {"aps": {"content-available": 1}}
// Expecting: Immediate delivery
// Reality: iOS may delay minutes/hours or drop entirely

// ✅ Use visible notification for time-critical content
// Or use silent for prefetch, visible for alert
{
    "aps": {
        "alert": {"title": "New Message", "body": "..."},
        "content-available": 1  // Also prefetch in background
    }
}
NEVER do heavy work in silent notification handler:
swift
// ❌ System will kill your app
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {

    await downloadLargeFiles()  // Takes too long!
    await processAllData()       // iOS terminates app

    return .newData
}

// ✅ Quick fetch, defer heavy processing
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {

    // 30 seconds max — fetch metadata only
    do {
        let hasNew = try await checkForNewContent()
        if hasNew {
            scheduleBackgroundProcessing()  // BGProcessingTask
        }
        return hasNew ? .newData : .noData
    } catch {
        return .failed
    }
}
绝对不要依赖静默通知实现时间敏感的送达:
swift
// ❌ 静默通知优先级低
// 服务器发送:{"aps": {"content-available": 1}}
// 预期:立即送达
// 实际:iOS可能延迟数分钟/小时,甚至直接丢弃

// ✅ 时间敏感内容使用可见通知
// 或静默通知预取,可见通知提醒
{
    "aps": {
        "alert": {"title": "新消息", "body": "..."},
        "content-available": 1  // 同时在后台预取
    }
}
绝对不要在静默通知处理程序中执行繁重任务:
swift
// ❌ 系统会终止应用
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {

    await downloadLargeFiles()  // 耗时过长!
    await processAllData()       // iOS会终止应用

    return .newData
}

// ✅ 快速获取,延迟繁重处理
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {

    // 最多30秒——仅获取元数据
    do {
        let hasNew = try await checkForNewContent()
        if hasNew {
            scheduleBackgroundProcessing()  // BGProcessingTask
        }
        return hasNew ? .newData : .noData
    } catch {
        return .failed
    }
}

Notification Service Extension

Notification Service Extension

NEVER forget expiration handler:
swift
// ❌ System shows unmodified notification
class NotificationService: UNNotificationServiceExtension {
    override func didReceive(_ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {

        // Start async work...
        downloadImage { image in
            // Never called if timeout!
            contentHandler(modifiedContent)
        }
    }

    // Missing serviceExtensionTimeWillExpire!
}

// ✅ Always implement expiration handler
class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        downloadImage { [weak self] image in
            guard let self, let content = self.bestAttemptContent else { return }
            if let image { content.attachments = [image] }
            contentHandler(content)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        // Called ~30 seconds — deliver what you have
        if let content = bestAttemptContent {
            contentHandler?(content)
        }
    }
}

绝对不要忘记实现过期处理程序:
swift
// ❌ 系统会显示未修改的通知
class NotificationService: UNNotificationServiceExtension {
    override func didReceive(_ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {

        // 启动异步任务...
        downloadImage { image in
            // 超时后不会被调用!
            contentHandler(modifiedContent)
        }
    }

    // 缺少serviceExtensionTimeWillExpire!
}

// ✅ 始终实现过期处理程序
class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        downloadImage { [weak self] image in
            guard let self, let content = self.bestAttemptContent else { return }
            if let image { content.attachments = [image] }
            contentHandler(content)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        // 约30秒后调用——交付已处理好的内容
        if let content = bestAttemptContent {
            contentHandler?(content)
        }
    }
}

Essential Patterns

核心模式

Permission Flow with Pre-Permission

带预授权界面的权限流程

swift
@MainActor
final class NotificationPermissionManager: ObservableObject {
    @Published var status: UNAuthorizationStatus = .notDetermined

    func checkStatus() async {
        let settings = await UNUserNotificationCenter.current().notificationSettings()
        status = settings.authorizationStatus
    }

    func requestPermission() async -> Bool {
        do {
            let granted = try await UNUserNotificationCenter.current()
                .requestAuthorization(options: [.alert, .badge, .sound])

            if granted {
                UIApplication.shared.registerForRemoteNotifications()
            }

            await checkStatus()
            return granted
        } catch {
            return false
        }
    }

    func openSettings() {
        guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
        UIApplication.shared.open(url)
    }
}

// Pre-permission screen
struct NotificationPermissionView: View {
    @StateObject private var manager = NotificationPermissionManager()
    @State private var showSystemPrompt = false

    var body: some View {
        VStack(spacing: 24) {
            Image(systemName: "bell.badge")
                .font(.system(size: 60))

            Text("Stay Updated")
                .font(.title)

            Text("Get notified about new messages, order updates, and important alerts.")
                .multilineTextAlignment(.center)

            Button("Enable Notifications") {
                Task { await manager.requestPermission() }
            }
            .buttonStyle(.borderedProminent)

            Button("Not Now") { dismiss() }
                .foregroundColor(.secondary)
        }
        .padding()
    }
}
swift
@MainActor
final class NotificationPermissionManager: ObservableObject {
    @Published var status: UNAuthorizationStatus = .notDetermined

    func checkStatus() async {
        let settings = await UNUserNotificationCenter.current().notificationSettings()
        status = settings.authorizationStatus
    }

    func requestPermission() async -> Bool {
        do {
            let granted = try await UNUserNotificationCenter.current()
                .requestAuthorization(options: [.alert, .badge, .sound])

            if granted {
                UIApplication.shared.registerForRemoteNotifications()
            }

            await checkStatus()
            return granted
        } catch {
            return false
        }
    }

    func openSettings() {
        guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
        UIApplication.shared.open(url)
    }
}

// 预授权界面
struct NotificationPermissionView: View {
    @StateObject private var manager = NotificationPermissionManager()
    @State private var showSystemPrompt = false

    var body: some View {
        VStack(spacing: 24) {
            Image(systemName: "bell.badge")
                .font(.system(size: 60))

            Text("保持更新")
                .font(.title)

            Text("获取新消息、订单更新及重要提醒通知。")
                .multilineTextAlignment(.center)

            Button("启用通知") {
                Task { await manager.requestPermission() }
            }
            .buttonStyle(.borderedProminent)

            Button("暂不启用") { dismiss() }
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

Notification Action Handler

通知操作处理程序

swift
@MainActor
final class NotificationHandler: NSObject, UNUserNotificationCenterDelegate {
    static let shared = NotificationHandler()

    private let router: DeepLinkRouter

    func userNotificationCenter(_ center: UNUserNotificationCenter,
        willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        // App is in foreground
        let userInfo = notification.request.content.userInfo

        // Check if we should show banner or handle silently
        if shouldShowInForeground(userInfo) {
            return [.banner, .sound, .badge]
        } else {
            handleSilently(userInfo)
            return []
        }
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse) async {
        let userInfo = response.notification.request.content.userInfo

        switch response.actionIdentifier {
        case UNNotificationDefaultActionIdentifier:
            // User tapped notification
            await handleNotificationTap(userInfo)

        case "REPLY_ACTION":
            if let textResponse = response as? UNTextInputNotificationResponse {
                await handleReply(text: textResponse.userText, userInfo: userInfo)
            }

        case "MARK_READ_ACTION":
            await markAsRead(userInfo)

        case UNNotificationDismissActionIdentifier:
            // User dismissed
            break

        default:
            await handleCustomAction(response.actionIdentifier, userInfo: userInfo)
        }
    }

    private func handleNotificationTap(_ userInfo: [AnyHashable: Any]) async {
        guard let deepLink = userInfo["deep_link"] as? String,
              let url = URL(string: deepLink) else { return }

        await router.navigate(to: url)
    }
}
swift
@MainActor
final class NotificationHandler: NSObject, UNUserNotificationCenterDelegate {
    static let shared = NotificationHandler()

    private let router: DeepLinkRouter

    func userNotificationCenter(_ center: UNUserNotificationCenter,
        willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        // 应用在前台
        let userInfo = notification.request.content.userInfo

        // 判断是否显示横幅或静默处理
        if shouldShowInForeground(userInfo) {
            return [.banner, .sound, .badge]
        } else {
            handleSilently(userInfo)
            return []
        }
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse) async {
        let userInfo = response.notification.request.content.userInfo

        switch response.actionIdentifier {
        case UNNotificationDefaultActionIdentifier:
            // 用户点击了通知
            await handleNotificationTap(userInfo)

        case "REPLY_ACTION":
            if let textResponse = response as? UNTextInputNotificationResponse {
                await handleReply(text: textResponse.userText, userInfo: userInfo)
            }

        case "MARK_READ_ACTION":
            await markAsRead(userInfo)

        case UNNotificationDismissActionIdentifier:
            // 用户关闭了通知
            break

        default:
            await handleCustomAction(response.actionIdentifier, userInfo: userInfo)
        }
    }

    private func handleNotificationTap(_ userInfo: [AnyHashable: Any]) async {
        guard let deepLink = userInfo["deep_link"] as? String,
              let url = URL(string: deepLink) else { return }

        await router.navigate(to: url)
    }
}

Rich Notification Service

富通知服务

swift
class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        guard let content = bestAttemptContent else {
            contentHandler(request.content)
            return
        }

        Task {
            // Download and attach media
            if let mediaURL = request.content.userInfo["media_url"] as? String {
                if let attachment = await downloadAttachment(from: mediaURL) {
                    content.attachments = [attachment]
                }
            }

            // Decrypt if needed
            if let encrypted = request.content.userInfo["encrypted_body"] as? String {
                content.body = decrypt(encrypted)
            }

            contentHandler(content)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        if let content = bestAttemptContent {
            contentHandler?(content)
        }
    }

    private func downloadAttachment(from urlString: String) async -> UNNotificationAttachment? {
        guard let url = URL(string: urlString) else { return nil }

        do {
            let (localURL, response) = try await URLSession.shared.download(from: url)

            let fileExtension = (response as? HTTPURLResponse)?
                .mimeType.flatMap { mimeToExtension[$0] } ?? "jpg"

            let destURL = FileManager.default.temporaryDirectory
                .appendingPathComponent(UUID().uuidString)
                .appendingPathExtension(fileExtension)

            try FileManager.default.moveItem(at: localURL, to: destURL)

            return try UNNotificationAttachment(identifier: "media", url: destURL)
        } catch {
            return nil
        }
    }
}

swift
class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        guard let content = bestAttemptContent else {
            contentHandler(request.content)
            return
        }

        Task {
            // 下载并附加媒体
            if let mediaURL = request.content.userInfo["media_url"] as? String {
                if let attachment = await downloadAttachment(from: mediaURL) {
                    content.attachments = [attachment]
                }
            }

            // 如需解密
            if let encrypted = request.content.userInfo["encrypted_body"] as? String {
                content.body = decrypt(encrypted)
            }

            contentHandler(content)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        if let content = bestAttemptContent {
            contentHandler?(content)
        }
    }

    private func downloadAttachment(from urlString: String) async -> UNNotificationAttachment? {
        guard let url = URL(string: urlString) else { return nil }

        do {
            let (localURL, response) = try await URLSession.shared.download(from: url)

            let fileExtension = (response as? HTTPURLResponse)?
                .mimeType.flatMap { mimeToExtension[$0] } ?? "jpg"

            let destURL = FileManager.default.temporaryDirectory
                .appendingPathComponent(UUID().uuidString)
                .appendingPathExtension(fileExtension)

            try FileManager.default.moveItem(at: localURL, to: destURL)

            return try UNNotificationAttachment(identifier: "media", url: destURL)
        } catch {
            return nil
        }
    }
}

Quick Reference

快速参考

Payload Structure

负载结构

FieldPurposeValue
alertVisible notification{title, subtitle, body}
badgeApp icon badgeNumber
soundNotification sound"default" or filename
content-availableSilent/background1
mutable-contentService extension1
categoryAction buttonsCategory identifier
thread-idNotification groupingThread identifier
字段用途
alert可见通知{title, subtitle, body}
badge应用图标角标数字
sound通知音效"default" 或文件名
content-available静默/后台1
mutable-content服务扩展1
category操作按钮分类标识符
thread-id通知分组线程标识符

Permission States

权限状态

StatusMeaningAction
notDeterminedNever askedShow pre-permission
deniedUser declinedShow settings prompt
authorizedFull accessRegister for remote
provisionalQuiet deliveryConsider upgrade prompt
ephemeralApp clip temporaryLimited time
状态含义操作
notDetermined从未请求过显示预授权界面
denied用户已拒绝显示设置引导
authorized完全权限注册远程通知
provisional静默送达考虑引导升级权限
ephemeralApp Clip临时权限有效期有限

Extension Limits

扩展限制

ExtensionTime LimitUse Case
Service Extension~30 secondsDownload media, decrypt
Content ExtensionUser interactionCustom UI
Background fetch~30 secondsData refresh
扩展类型时间限制适用场景
Service Extension~30秒下载媒体、解密
Content Extension用户交互阶段自定义UI
Background fetch~30秒数据刷新

Red Flags

危险信号

SmellProblemFix
Permission on launchLow acceptanceWait for user action
Cached device tokenMay be staleAlways use callback
String(data:encoding:) for tokenReturns nilUse hex encoding
Silent for time-criticalMay be delayedUse visible notification
Heavy work in silent handlerApp terminatedQuick fetch, defer work
No serviceExtensionTimeWillExpireUnmodified content shownAlways implement
Ignoring denied statusFrustrates userOffer settings path
问题迹象潜在问题修复方案
启动即请求权限接受率低等待用户操作后再请求
缓存设备令牌令牌可能失效始终使用注册回调中的令牌
使用String(data:encoding:)处理令牌返回nil使用十六进制编码
时间敏感内容用静默通知可能延迟使用可见通知
静默通知处理程序执行繁重任务应用被终止快速获取,延迟繁重处理
未实现serviceExtensionTimeWillExpire显示未修改的内容始终实现该方法
忽略已拒绝的权限状态惹恼用户提供设置路径