在 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-available、apns-push-type: background、apns-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: background 和 apns-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 專屬的通知規則。要: 將這些導向 activitykit、callkit 和 app-clips。
審查清單
- [ ] 在顯示可見的提示/聲音/標記前已請求授權;已處理拒絕情況(設定連結)
- [ ] APNs 註冊未錯誤地被提示授權狀態阻擋
- [ ] 裝置 Token 已轉換為十六進位、每次回呼都上傳,且未視為本地快取或固定長度常數
- [ ]
UNUserNotificationCenterDelegate已在App.init或application(_:didFinishLaunching:)中設定 - [ ] 已實作前景(
willPresent)和點擊(didReceive)處理 - [ ] 若需要互動式通知,已在啟動時註冊類別/動作
- [ ] 靜默推播使用
content-available: 1、無 alert/sound/badge、apns-push-type: background、apns-priority: 5、Background Modes > Remote notifications、限流注意事項,以及正確的UIBackgroundFetchResult
參考資料
- references/notification-patterns.md — AppDelegate 設定、APNs 回呼、深層連結路由器、靜默推播、除錯
- references/rich-notifications.md — Service Extension、Content Extension、附件、通訊通知
- Apple 文件:APNs 註冊、權限、承載、背景推播




