push-notifications

push-notifications

熱門

在 iOS/macOS 應用程式中實作、審查或除錯推播通知 — 包含本地通知、遠端 (APNs) 通知、豐富通知、通知動作、靜默推播以及通知服務/內容擴充。適用於使用 UNUserNotificationCenter、註冊遠端通知、處理通知承載、設定通知類別與動作、建立豐富通知內容或除錯通知傳遞的情境。也適用於在 Swift 應用程式中處理提示、標記、聲音、背景推播或使用者通知權限。

931星標
0分支
更新於 2026/7/26
SKILL.md
唯讀
名稱
push-notifications
描述

在 iOS/macOS 應用程式中實作、審查或除錯推播通知 — 包含本地通知、遠端 (APNs) 通知、豐富通知、通知動作、靜默推播以及通知服務/內容擴充。適用於使用 UNUserNotificationCenter、註冊遠端通知、處理通知承載、設定通知類別與動作、建立豐富通知內容或除錯通知傳遞的情境。也適用於在 Swift 應用程式中處理提示、標記、聲音、背景推播或使用者通知權限。

推播通知

在 iOS/macOS 上使用 UserNotifications 和 APNs 實作、審查及除錯本地與遠端通知。涵蓋權限流程、Token 註冊、承載結構、前景處理、通知動作、分組與豐富通知。目標為 iOS 26+ 搭配 Swift 6.3,除非特別註明,否則向下相容至 iOS 16。

請將相鄰領域分開:Live Activity 的 content-state 承載屬於 activitykit;PushKit/VoIP 通話推播屬於 callkit;App Clip 的臨時通知設定屬於 app-clips;靜默推播後的長時間或排程背景工作屬於 background-processing

目錄

修正審查

審查有缺陷的通知提案時,明確指出違反的合約。APNs Token 審查必須說明 Token 註冊與提示授權無關、每次 didRegister 回呼都要上傳、避免以本地快取為真、絕不假設 Token 長度,並將模擬器註冊失敗視為預期行為,同時註明可使用 .apns 檔案或 simctl push 模擬傳遞。背景推播審查必須說明僅使用 content-availableapns-push-type: backgroundapns-priority: 5、Remote notifications 背景模式、低優先級、限流、不保證、非每隔幾分鐘,以及 didReceiveRemoteNotification 必須回傳正確的 UIBackgroundFetchResult。豐富通知審查必須說明服務擴充需要 mutable-content: 1 加上 alert 承載、靜默推播不會觸發服務擴充、附件為系統驗證並儲存的磁碟檔案、機密使用 Keychain Sharing 而 App Groups 用於共享檔案/UserDefaults、通訊通知需要能力 + NSUserActivityTypes + INInteraction 捐贈 + content.updating(from:),並且每個服務擴充路徑(包括附件下載失敗和 serviceExtensionTimeWillExpire())都必須恰好呼叫一次內容處理器,傳入原始、最佳嘗試或更新後的內容。

權限流程

在排程或顯示使用者可見的提示、聲音或標記之前,先請求通知授權。系統提示僅會顯示一次;後續呼叫會回傳已儲存的決定。APNs Token 註冊是獨立的:即使使用者尚未授予提示授權,當應用程式需要裝置 Token 時,仍應呼叫 registerForRemoteNotifications()

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("授權請求失敗:\(error)")
        return false
    }
}

檢查目前狀態

在假設權限狀態前,務必先檢查目前狀態。使用者隨時可能變更設定。

@MainActor
func checkNotificationStatus() async -> UNAuthorizationStatus {
    let settings = await UNUserNotificationCenter.current().notificationSettings()
    return settings.authorizationStatus
    // .notDetermined, .denied, .authorized, .provisional, .ephemeral
}

暫行通知

暫行通知會安靜地送達通知中心,不會打擾使用者。使用者之後可以選擇保留或關閉。適用於入門流程,在請求完整權限前先展示價值。

// 靜默送達 — 不會對使用者顯示權限提示
try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])

重大警示

重大警示會繞過勿擾模式和靜音開關。需要 Apple 的特殊授權(透過開發者入口網站申請)。僅用於健康、安全或保全情境。

// 需要 com.apple.developer.usernotifications.critical-alerts 授權
try await center.requestAuthorization(
    options: [.alert, .sound, .badge, .criticalAlert]
)

處理拒絕權限

當使用者拒絕通知時,使用 UIApplication.openSettingsURLString 引導他們前往設定。不要重複提示或騷擾使用者。

APNs 註冊

在 SwiftUI 應用程式中使用 UIApplicationDelegateAdaptor 來接收裝置 Token。AppDelegate 回呼是接收 APNs Token 的唯一方式。

@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)")
        // 將 Token 傳送給你的伺服器
        Task { await TokenService.shared.upload(token: token) }
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("APNs 註冊失敗:\(error.localizedDescription)")
        // 模擬器可以模擬推播,但不會向 APNs 註冊。
    }
}

註冊順序

在啟動時設定委派和類別。然後在需要顯示通知的上下文中請求使用者通知授權,並在應用程式需要裝置 Token 時向 APNs 註冊。不要以 .authorized 作為 APNs 註冊的條件;若無提示授權,遠端通知會以靜默方式送達。

@MainActor
func configureNotifications() async {
    let center = UNUserNotificationCenter.current()
    let settings = await center.notificationSettings()

    if settings.authorizationStatus == .notDetermined {
        _ = await requestNotificationPermission()
    }

    // 需要 APNs Token 送達和靜默遠端通知。
    UIApplication.shared.registerForRemoteNotifications()
}

Token 處理

裝置 Token 會變更。每次 didRegisterForRemoteNotificationsWithDeviceToken 觸發時都要重新傳送 Token 給伺服器,而不只是第一次。不要將 Token 本地持久化作為唯一來源,也不要假設 Token 長度固定。

本地通知

直接從裝置排程通知,無需伺服器。適用於提醒、計時器和基於位置的警示。

建立內容

let content = UNMutableNotificationContent()
content.title = "運動提醒"
content.subtitle = "該動一動了"
content.body = "您排定的運動將在 15 分鐘後開始。"
content.sound = .default
content.badge = NSNumber(value: 1)
content.userInfo = ["workoutId": "abc123"]
content.threadIdentifier = "workouts"  // 在通知中心中分組

觸發類型

// 在時間間隔後觸發(重複至少需 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)
// 至少需要「使用期間」定位權限

排程與管理

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()

遠端通知承載

標準 APNs 承載

{
    "aps": {
        "alert": {
            "title": "新訊息",
            "subtitle": "來自 Alice",
            "body": "嘿,你有空吃午餐嗎?"
        },
        "badge": 3,
        "sound": "default",
        "thread-id": "chat-alice",
        "category": "MESSAGE_CATEGORY"
    },
    "messageId": "msg-789",
    "senderId": "user-alice"
}

靜默 / 背景推播

設定 content-available: 1,不包含 alert、sound 或 badge。需要「Background Modes > Remote notifications」以及 APNs 標頭 apns-push-type: backgroundapns-priority: 5。系統會將這些視為低優先級、限流且不保證送達;不要每隔幾分鐘發送,也不要依賴它們提供即時更新。在 didReceiveRemoteNotification 中,執行有限的工作並在背景執行時間內迅速回傳 UIBackgroundFetchResult

{
    "aps": {
        "content-available": 1
    },
    "updateType": "new-data"
}

在 AppDelegate 中處理:

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: 1 加上 alert 字典,讓 Notification Service Extension 能在顯示前修改警示型遠端通知。靜默推播不會觸發服務擴充。使用服務擴充來執行有限的工作,例如下載支援的磁碟附件、解密顯示文字或設定通訊通知;在每個成功、失敗和逾時路徑上都要呼叫內容處理器。對於通訊通知,啟用能力、加入 NSUserActivityTypes、捐贈 INInteraction,然後呼叫 content.updating(from:)

{
    "aps": {
        "alert": { "title": "照片", "body": "Alice 傳送了一張照片" },
        "mutable-content": 1
    },
    "imageUrl": "https://example.com/photo.jpg"
}

本地化通知

使用本地化鍵值,讓通知以使用者的語言顯示:

{
    "aps": {
        "alert": {
            "title-loc-key": "NEW_MESSAGE_TITLE",
            "loc-key": "NEW_MESSAGE_BODY",
            "loc-args": ["Alice"]
        }
    }
}

通知處理

UNUserNotificationCenterDelegate

實作委派來控制前景顯示和處理使用者點擊。儘早設定委派 — 在 application(_:didFinishLaunchingWithOptions:)App.init 中。

@MainActor
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    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:
            // 使用者關閉了通知
            break
        default:
            // 自訂動作按鈕被點擊
            await handleCustomAction(actionIdentifier, userInfo: userInfo)
        }
    }
}

從通知進行深層連結

使用共享的 @Observable 路由器將通知點擊導向正確的畫面。委派寫入待處理的目的地;SwiftUI 視圖觀察並消費它。

@Observable @MainActor
final class DeepLinkRouter {
    static let shared = 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

通知動作與類別

定義可互動的動作,以按鈕形式顯示在通知上。在啟動時註冊類別。

定義類別與動作

func registerNotificationCategories() {
    let replyAction = UNTextInputNotificationAction(
        identifier: "REPLY_ACTION",
        title: "回覆",
        options: [],
        textInputButtonTitle: "傳送",
        textInputPlaceholder: "輸入回覆..."
    )

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

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

    let messageCategory = UNNotificationCategory(
        identifier: "MESSAGE_CATEGORY",
        actions: [replyAction, likeAction, deleteAction],
        intentIdentifiers: [],
        options: [.customDismissAction]  // 關閉時也會觸發 didReceive
    )

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

處理動作回應

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 — 點擊時將應用程式啟動至前景

通知分組

使用 threadIdentifier(或在 APNs 承載中使用 thread-id)將相關通知分組。每個唯一的執行緒會成為通知中心中的一個獨立群組。

content.threadIdentifier = "chat-alice"  // 所有來自 Alice 的訊息會分在同一組
content.summaryArgument = "Alice"
content.summaryArgumentCount = 3         // 「還有 3 則來自 Alice 的通知」

在類別中自訂摘要格式字串:

let category = UNNotificationCategory(
    identifier: "MESSAGE_CATEGORY",
    actions: [replyAction],
    intentIdentifiers: [],
    categorySummaryFormat: "還有 %u 則來自 %@ 的訊息",
    options: []
)

常見錯誤

不要: 當應用程式需要靜默推播或伺服器 Token 綁定時,以提示授權作為 APNs Token 註冊的條件。
要: 請求提示/聲音/標記的授權,並在需要裝置 Token 時向 APNs 註冊。
不要: 使用 String(data: deviceToken, encoding: .utf8) 轉換裝置 Token。
要: 使用十六進位:deviceToken.map { String(format: "%02x", $0) }.joined()
不要: 承諾每隔幾分鐘的靜默重新整理或即時背景送達。
要: 說明背景推播是低優先級、限流、不保證、實際上每小時僅少數幾次,且 didReceiveRemoteNotification 必須執行有限的工作並回傳正確的 UIBackgroundFetchResult
不要: 預期靜默推播會執行 Notification Service Extension,或未在擴充中呼叫內容處理器。
要: 使用 mutable-content: 1 搭配 alert 承載、系統驗證並儲存的支援磁碟附件、INInteraction 捐贈加上 content.updating(from:) 用於通訊通知,並在每個成功、失敗和逾時路徑上傳入原始或最佳嘗試的內容。
不要: 忘記前景處理。若無 willPresent,通知會被靜默抑制。
要: 實作 willPresent 並回傳 .banner.sound.badge
不要: 太晚設定委派,或未使用 AppDelegate adaptor 就從 SwiftUI 視圖註冊。
要:App.init 中設定委派;使用 UIApplicationDelegateAdaptor 處理 APNs。
不要: 僅在 Token「變更」時上傳,或假設 Token 長度固定。要: 每次 didRegister 回呼都上傳,並將 Token 視為不透明資料轉換為十六進位。
不要: 在此處放入 Live Activity、VoIP 或 App Clip 專屬的通知規則。要: 將這些導向 activitykitcallkitapp-clips

審查清單

  • [ ] 在顯示可見的提示/聲音/標記前已請求授權;已處理拒絕情況(設定連結)
  • [ ] APNs 註冊未錯誤地被提示授權狀態阻擋
  • [ ] 裝置 Token 已轉換為十六進位、每次回呼都上傳,且未視為本地快取或固定長度常數
  • [ ] UNUserNotificationCenterDelegate 已在 App.initapplication(_:didFinishLaunching:) 中設定
  • [ ] 已實作前景(willPresent)和點擊(didReceive)處理
  • [ ] 若需要互動式通知,已在啟動時註冊類別/動作
  • [ ] 靜默推播使用 content-available: 1、無 alert/sound/badge、apns-push-type: backgroundapns-priority: 5、Background Modes > Remote notifications、限流注意事項,以及正確的 UIBackgroundFetchResult

參考資料