推送通知 API 参考
APNs HTTP/2 传输、UserNotifications 框架以及实时活动和广播推送等推送驱动功能的全面 API 参考。
快速参考
// AppDelegate — minimal remote notification setup
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
sendTokenToServer(token)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Registration failed: \(error)")
}
// Show notifications when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
// Handle notification tap / action response
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let userInfo = response.notification.request.content.userInfo
// Route to appropriate screen based on userInfo
}
}
APNs 传输参考
端点
| 环境 | 主机 | 端口 |
|---|
| 开发 | api.sandbox.push.apple.com | 443 或 2197 |
| 生产 | api.push.apple.com | 443 或 2197 |
请求格式
POST /3/device/{device_token}
Host: api.push.apple.com
Authorization: bearer {jwt_token}
apns-topic: {bundle_id}
apns-push-type: alert
Content-Type: application/json
APNs 标头
| 标头 | 必需 | 值 | 说明 |
|---|
| apns-push-type | 是 | alert、background、liveactivity、voip、complication、fileprovider、mdm、location | 必须与负载内容匹配 |
| apns-topic | 是 | Bundle ID(或 .push-type.liveactivity 后缀) | 基于令牌的身份验证所必需 |
| apns-priority | 否 | 10(立即)、5(省电)、1(低) | 默认值:alert 为 10,background 为 5 |
| apns-expiration | 否 | UNIX 时间戳或 0 | 0 = 投递一次,不存储 |
| apns-collapse-id | 否 | 字符串,≤64 字节 | 替换设备上匹配的通知 |
| apns-id | 否 | UUID(小写) | APNs 返回的跟踪标识符 |
| authorization | 令牌身份验证 | bearer {JWT} | 证书身份验证不需要 |
| apns-unique-id | 仅响应 | UUID | 与 Push Notifications Console 的投递日志配合使用 |
响应代码
| 状态 | 含义 | 常见原因 |
|---|
| 200 | 成功 | |
| 400 | 请求错误 | JSON 格式错误、缺少必需标头 |
| 403 | 禁止 | JWT 已过期、团队或密钥错误、主题不匹配 |
| 404 | 未找到 | 设备令牌路径无效 |
| 405 | 不允许使用该方法 | 未使用 POST |
| 410 | 已注销 | 设备令牌不再有效(应用已卸载) |
| 413 | 负载过大 | 超过 4KB(VoIP 为 5KB) |
| 429 | 请求过多 | APNs 限流 |
| 500 | 内部服务器错误 | APNs 问题,请重试 |
| 503 | 服务不可用 | APNs 过载,请退避后重试 |
JWT 身份验证参考
JWT 标头
{ "alg": "ES256", "kid": "{10-char Key ID}" }
JWT 声明
{ "iss": "{10-char Team ID}", "iat": {unix_timestamp} }
规则
| 规则 | 详情 |
|---|
| 算法 | ES256(P-256 曲线) |
| 签名密钥 | APNs 身份验证密钥(来自开发者门户的 .p8 文件) |
| 令牌有效期 | 最长 1 小时(超过后返回 403 ExpiredProviderToken) |
| 刷新间隔 | 20 到 60 分钟之间 |
| 作用范围 | 一个密钥适用于团队中的所有应用和两个环境 |
Authorization 标头格式
authorization: bearer eyAia2lkIjog...
负载参考
aps 字典键
| 键 | 类型 | 用途 | 起始版本 |
|---|
| alert | Dict/String | 提醒内容 | iOS 10 |
| badge | Number | 应用图标标记(0 表示移除) | iOS 10 |
| sound | String/Dict | 音频播放 | iOS 10 |
| thread-id | String | 通知分组 | iOS 10 |
| category | String | 可操作通知类型 | iOS 10 |
| content-available | Number (1) | 静默后台推送 | iOS 10 |
| mutable-content | Number (1) | 触发服务扩展 | iOS 10 |
| target-content-id | String | 窗口/内容标识符 | iOS 13 |
| interruption-level | String | passive/active/time-sensitive/critical | iOS 15 |
| relevance-score | Number 0-1 | 通知摘要排序 | iOS 15 |
| filter-criteria | String | 专注模式过滤条件匹配 | iOS 15 |
| stale-date | Number | UNIX 时间戳(实时活动) | iOS 16.1 |
| content-state | Dict | 实时活动内容更新 | iOS 16.1 |
| timestamp | Number | UNIX 时间戳(实时活动) | iOS 16.1 |
| event | String | start/update/end(实时活动) | iOS 16.1 |
| dismissal-date | Number | UNIX 时间戳(实时活动) | iOS 16.1 |
| attributes-type | String | 实时活动结构体名称 | iOS 17 |
| attributes | Dict | 实时活动初始化数据 | iOS 17 |
提醒字典键
| 键 | 类型 | 用途 |
|---|
| title | 字符串 | 简短标题 |
| subtitle | 字符串 | 次要描述 |
| body | 字符串 | 完整消息 |
| launch-image | 字符串 | 启动屏幕文件名 |
| title-loc-key | 字符串 | 标题的本地化键 |
| title-loc-args | [String] | 标题格式参数 |
| subtitle-loc-key | 字符串 | 副标题的本地化键 |
| subtitle-loc-args | [String] | 副标题格式参数 |
| loc-key | 字符串 | 正文的本地化键 |
| loc-args | [String] | 正文格式参数 |
声音字典(关键提醒)
{ "critical": 1, "name": "alarm.aiff", "volume": 0.8 }
中断级别值
| 值 | 行为 | 要求 |
|---|
| passive | 无声音或唤醒。仅显示通知摘要。 | 无 |
| active | 默认。声音 + 横幅。 | 无 |
| time-sensitive | 突破计划送达时间。横幅会持续显示。 | 时间敏感通知功能 |
| critical | 绕过勿扰模式和响铃开关。 | Apple 批准 + 权限 |
示例负载
基本提醒
{
"aps": {
"alert": {
"title": "New Message",
"subtitle": "From Alice",
"body": "Hey, are you free for lunch?"
},
"badge": 3,
"sound": "default"
}
}
使用 loc-key/loc-args 本地化
{
"aps": {
"alert": {
"title-loc-key": "MESSAGE_TITLE",
"title-loc-args": ["Alice"],
"loc-key": "MESSAGE_BODY",
"loc-args": ["Alice", "lunch"]
},
"sound": "default"
}
}
静默后台推送
{
"aps": {
"content-available": 1
},
"custom-key": "sync-update"
}
丰富通知(Service Extension)
{
"aps": {
"alert": {
"title": "Photo shared",
"body": "Alice shared a photo with you"
},
"mutable-content": 1,
"sound": "default"
},
"image-url": "https://example.com/photo.jpg"
}
关键提醒
{
"aps": {
"alert": {
"title": "Server Down",
"body": "Production database is unreachable"
},
"sound": { "critical": 1, "name": "default", "volume": 1.0 },
"interruption-level": "critical"
}
}
带类别的时间敏感通知
{
"aps": {
"alert": {
"title": "Package Delivered",
"body": "Your order has been delivered to the front door"
},
"interruption-level": "time-sensitive",
"category": "DELIVERY",
"sound": "default"
},
"order-id": "12345"
}
UNUserNotificationCenter API 参考
关键方法
| 方法 | 用途 |
|---|
| requestAuthorization(options:) | 请求权限 |
| notificationSettings() | 检查当前状态 |
| add(_:) | 安排通知请求 |
| getPendingNotificationRequests() | 列出已安排的通知 |
| removePendingNotificationRequests(withIdentifiers:) | 取消已安排的通知 |
| getDeliveredNotifications() | 列出通知中心中的通知 |
| removeDeliveredNotifications(withIdentifiers:) | 从通知中心移除通知 |
| setNotificationCategories(_:) | 注册可操作类型 |
| setBadgeCount(_:) | 更新角标(iOS 16+) |
| supportsContentExtensions | 检查是否支持内容扩展 |
UNAuthorizationOptions
| 选项 | 用途 |
|---|
| .alert | 显示提醒 |
| .badge | 更新角标计数 |
| .sound | 播放声音 |
| .carPlay | 在 CarPlay 中显示 |
| .criticalAlert | 关键提醒(需要权限) |
| .provisional | 无需提示即可试行送达 |
| .providesAppNotificationSettings | “在设置中配置”按钮 |
| .announcement | Siri 播报(iOS 15+ 已弃用) |
UNAuthorizationStatus
| 值 | 含义 |
|---|
| .notDetermined | 尚未显示提示 |
| .denied | 用户拒绝或已在设置中关闭 |
| .authorized | 用户明确授权 |
| .provisional | 试行送达 |
| .ephemeral | App Clip 临时状态 |
请求授权
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
if granted {
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}
检查设置
let settings = await center.notificationSettings()
switch settings.authorizationStatus {
case .authorized: break
case .denied:
// Direct user to Settings
case .provisional:
// Upgrade to full authorization
case .notDetermined:
// Request authorization
case .ephemeral:
// App Clip — temporary
@unknown default: break
}
代理方法
// Foreground presentation — called when notification arrives while app is active
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async
-> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
// Action response — called when user taps notification or action button
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let actionIdentifier = response.actionIdentifier
let userInfo = response.notification.request.content.userInfo
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// User tapped notification body
break
case UNNotificationDismissActionIdentifier:
// User dismissed (requires .customDismissAction on category)
break
default:
// Custom action
break
}
}
// Settings — called when user taps "Configure in App" from notification settings
func userNotificationCenter(_ center: UNUserNotificationCenter,
openSettingsFor notification: UNNotification?) {
// Navigate to in-app notification settings
}
UNNotificationCategory 和 UNNotificationAction API
类别注册
let likeAction = UNNotificationAction(
identifier: "LIKE",
title: "Like",
options: []
)
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type a message..."
)
let deleteAction = UNNotificationAction(
identifier: "DELETE",
title: "Delete",
options: [.destructive, .authenticationRequired]
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE",
actions: [likeAction, replyAction, deleteAction],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "New message",
categorySummaryFormat: "%u more messages",
options: [.customDismissAction]
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
操作选项
| 选项 | 效果 |
|---|
| .authenticationRequired | 需要解锁设备 |
| .destructive | 以红色文字显示 |
| .foreground | 将应用启动到前台 |
类别选项
| 选项 | 效果 |
|---|
| .customDismissAction | 关闭时触发代理方法 |
| .allowInCarPlay | 在 CarPlay 中显示操作 |
| .hiddenPreviewsShowTitle | 隐藏预览时显示标题 |
| .hiddenPreviewsShowSubtitle | 隐藏预览时显示副标题 |
| .allowAnnouncement | Siri 可播报(iOS 15+ 已弃用) |
UNNotificationActionIcon(iOS 15+)
let icon = UNNotificationActionIcon(systemImageName: "hand.thumbsup")
let action = UNNotificationAction(
identifier: "LIKE",
title: "Like",
options: [],
icon: icon
)
UNNotificationServiceExtension API
在显示前修改通知内容。在单独的扩展进程中运行。
生命周期
| 方法 | 时间窗口 | 用途 |
|---|
| didReceive(_:withContentHandler:) | ~30 秒 | 修改通知内容 |
| 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)
guard let content = bestAttemptContent,
let imageURLString = content.userInfo["image-url"] as? String,
let imageURL = URL(string: imageURLString) else {
contentHandler(request.content)
return
}
// Download and attach image
let task = URLSession.shared.downloadTask(with: imageURL) { url, _, error in
defer { contentHandler(content) }
guard let url = url, error == nil else { return }
let attachment = try? UNNotificationAttachment(
identifier: "image",
url: url,
options: [UNNotificationAttachmentOptionsTypeHintKey: "public.jpeg"]
)
if let attachment = attachment {
content.attachments = [attachment]
}
}
task.resume()
}
override func serviceExtensionTimeWillExpire() {
if let content = bestAttemptContent {
contentHandler?(content)
}
}
}
支持的附件类型
| 类型 | 扩展名 | 最大大小 |
|---|
| 图像 | .jpg, .gif, .png | 10 MB |
| 音频 | .aif, .wav, .mp3 | 5 MB |
| 视频 | .mp4, .mpeg | 50 MB |
负载要求
通知负载必须在 aps 字典中包含 "mutable-content": 1,服务扩展才能触发。
本地通知 API
触发类型
| 触发器 | 使用场景 | 重复 |
|---|
| UNTimeIntervalNotificationTrigger | N 秒后 | 是(≥60s) |
| UNCalendarNotificationTrigger | 特定日期/时间 | 是 |
| UNLocationNotificationTrigger | 进入/离开区域 | 是 |
时间间隔触发器
let content = UNMutableNotificationContent()
content.title = "Reminder"
content.body = "Time to take a break"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)
let request = UNNotificationRequest(
identifier: "break-reminder",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
日历触发器
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(
identifier: "daily-9am",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
位置触发器
import CoreLocation
let center = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)
let region = CLCircularRegion(center: center, radius: 100, identifier: "apple-park")
region.notifyOnEntry = true
region.notifyOnExit = false
let trigger = UNLocationNotificationTrigger(region: region, repeats: false)
let request = UNNotificationRequest(
identifier: "arrived-at-office",
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
限制
| 限制 | 详情 |
|---|
| 最小重复间隔 | UNTimeIntervalNotificationTrigger 为 60 秒 |
| 位置授权 | 位置触发器需要“使用 App 期间”或“始终”授权 |
| 不支持服务扩展 | 本地通知不会触发 UNNotificationServiceExtension |
| 不会在后台唤醒 | 本地通知不能使用 content-available 进行后台处理 |
| App 扩展 | 不能从 App 扩展中安排本地通知(请使用 App Group + 主 App) |
| 待处理数量上限 | 每个 App 最多可有 64 个待处理通知请求 |
Live Activity 推送标头
必需标头
| 标头 | 值 |
|---|
| apns-push-type | liveactivity |
| apns-topic | {bundleID}.push-type.liveactivity |
| apns-priority | 5(常规)或 10(时间敏感) |
事件类型
| 事件 | 用途 | 必需字段 |
|---|
| start | 远程启动 Live Activity | attributes-type, attributes, content-state, timestamp |
| update | 更新内容 | content-state, timestamp |
| end | 结束 Live Activity | timestamp(content-state 可选) |
更新负载
{
"aps": {
"timestamp": 1709913600,
"event": "update",
"content-state": {
"homeScore": 2,
"awayScore": 1,
"inning": "Top 7"
}
}
}
启动负载(Push-to-Start Token)
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
},
"alert": {
"title": "Game Starting",
"body": "Giants vs Dodgers is about to begin"
}
}
}
启动负载(基于 Channel)
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
}
}
}
结束负载
{
"aps": {
"timestamp": 1709913600,
"event": "end",
"dismissal-date": 1709917200,
"content-state": {
"homeScore": 5,
"awayScore": 3,
"inning": "Final"
}
}
}
Push-to-Start Token
// Observe push-to-start tokens (iOS 17.2+)
for await token in Activity<GameAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
sendPushToStartTokenToServer(tokenString)
}
Activity 推送令牌
// Observe activity-specific push tokens
for await tokenData in activity.pushTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
sendActivityTokenToServer(token, activityId: activity.id)
}
Content-state 编码规则:系统始终使用默认的 JSONDecoder — 不要在 ActivityAttributes.ContentState 中使用自定义编码策略。
广播推送 API(iOS 18+)
面向多个接收方的服务端推送,用于 Live Activities,无需跟踪各设备的令牌。
端点
POST /4/broadcasts/apps/{TOPIC}
标头
| 标头 | 值 |
|---|
| apns-push-type | liveactivity |
| apns-channel-id | {channelID} |
| authorization | bearer {JWT} |
通过 Channel 订阅
try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .channel(channelId)
)
Channel 存储策略
| 策略 | 行为 | 预算 |
|---|
| 不存储 | 仅向已连接的设备发送 | 更高 |
| 最新消息 | 为离线设备存储最新消息 | 更低 |
命令行测试
JWT 生成
JWT_ISSUE_TIME=$(date +%s)
JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"
发送提醒推送
curl -v \
--header "apns-topic: $TOPIC" \
--header "apns-push-type: alert" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{"aps":{"alert":"test"}}' \
--http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}
发送 Live Activity 推送
curl \
--header "apns-topic: com.example.app.push-type.liveactivity" \
--header "apns-push-type: liveactivity" \
--header "apns-priority: 10" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{
"aps": {
"timestamp": '$(date +%s)',
"event": "update",
"content-state": { "score": "2-1" }
}
}' \
--http2 https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN
模拟器推送
xcrun simctl push booted com.example.app payload.json
模拟器负载文件
{
"Simulator Target Bundle": "com.example.app",
"aps": {
"alert": { "title": "Test", "body": "Hello" },
"sound": "default"
}
}
资源
WWDC:2021-10091, 2023-10025, 2023-10185, 2024-10069
文档:/usernotifications, /usernotifications/sending-notification-requests-to-apns, /usernotifications/generating-a-remote-notification, /activitykit
技能:axiom-push-notifications, axiom-push-notifications-diag, axiom-extensions-widgets