axiom-push-notifications-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePush Notifications API Reference
推送通知API参考
Comprehensive API reference for APNs HTTP/2 transport, UserNotifications framework, and push-driven features including Live Activities and broadcast push.
本文是APNs HTTP/2传输、UserNotifications框架,以及Live Activity、广播推送等推送驱动功能的综合API参考。
Quick Reference
快速参考
swift
// 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
}
}swift
// AppDelegate — 最小化远程通知配置
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)")
}
// 应用在前台时展示通知
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
// 根据userInfo路由到对应页面
}
}APNs Transport Reference
APNs传输参考
Endpoints
端点
| Environment | Host | Port |
|---|---|---|
| Development | api.sandbox.push.apple.com | 443 or 2197 |
| Production | api.push.apple.com | 443 or 2197 |
| 环境 | 主机地址 | 端口 |
|---|---|---|
| 开发环境 | api.sandbox.push.apple.com | 443 或 2197 |
| 生产环境 | api.push.apple.com | 443 或 2197 |
Request Format
请求格式
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/jsonPOST /3/device/{device_token}
Host: api.push.apple.com
Authorization: bearer {jwt_token}
apns-topic: {bundle_id}
apns-push-type: alert
Content-Type: application/jsonAPNs Headers
APNs请求头
| Header | Required | Values | Notes |
|---|---|---|---|
| apns-push-type | Yes | alert, background, liveactivity, voip, complication, fileprovider, mdm, location | Must match payload content |
| apns-topic | Yes | Bundle ID (or .push-type.liveactivity suffix) | Required for token-based auth |
| apns-priority | No | 10 (immediate), 5 (power-conscious), 1 (low) | Default: 10 for alert, 5 for background |
| apns-expiration | No | UNIX timestamp or 0 | 0 = deliver once, don't store |
| apns-collapse-id | No | String ≤64 bytes | Replaces matching notification on device |
| apns-id | No | UUID (lowercase) | Returned by APNs for tracking |
| authorization | Token auth | bearer {JWT} | Not needed for certificate auth |
| apns-unique-id | Response only | UUID | Use with Push Notifications Console delivery log |
| 头部字段 | 必填 | 可选值 | 说明 |
|---|---|---|---|
| apns-push-type | 是 | alert, background, liveactivity, voip, complication, fileprovider, mdm, location | 必须与payload内容匹配 |
| apns-topic | 是 | 包ID(或带.push-type.liveactivity后缀) | 基于token的认证必填 |
| apns-priority | 否 | 10(立即推送), 5(功耗优先), 1(低优先级) | 默认值:alert类型为10,后台推送为5 |
| apns-expiration | 否 | UNIX时间戳或0 | 0 = 仅投递一次,不存储 |
| apns-collapse-id | 否 | 长度≤64字节的字符串 | 替换设备上匹配的通知 |
| apns-id | 否 | 小写UUID | APNs返回用于追踪请求 |
| authorization | Token认证必填 | bearer {JWT} | 证书认证不需要 |
| apns-unique-id | 仅响应返回 | UUID | 配合推送通知控制台投递日志使用 |
Response Codes
响应码
| Status | Meaning | Common Cause |
|---|---|---|
| 200 | Success | |
| 400 | Bad request | Malformed JSON, missing required header |
| 403 | Forbidden | Expired JWT, wrong team/key, topic mismatch |
| 404 | Not found | Invalid device token path |
| 405 | Method not allowed | Not using POST |
| 410 | Unregistered | Device token no longer active (app uninstalled) |
| 413 | Payload too large | Exceeds 4KB (5KB for VoIP) |
| 429 | Too many requests | Rate limited by APNs |
| 500 | Internal server error | APNs issue, retry |
| 503 | Service unavailable | APNs overloaded, retry with backoff |
| 状态码 | 含义 | 常见原因 |
|---|---|---|
| 200 | 成功 | |
| 400 | 错误请求 | JSON格式错误、缺失必填头部 |
| 403 | 禁止访问 | JWT过期、团队/密钥错误、topic不匹配 |
| 404 | 未找到 | 设备token路径无效 |
| 405 | 请求方法不允许 | 未使用POST方法 |
| 410 | 已注销 | 设备token不再活跃(应用已卸载) |
| 413 | payload过大 | 超过4KB(VoIP类型为5KB) |
| 429 | 请求过多 | 被APNs限流 |
| 500 | 内部服务错误 | APNs自身问题,可重试 |
| 503 | 服务不可用 | APNs过载,退避后重试 |
JWT Authentication Reference
JWT认证参考
JWT Header
JWT头部
json
{ "alg": "ES256", "kid": "{10-char Key ID}" }json
{ "alg": "ES256", "kid": "{10位密钥ID}" }JWT Claims
JWT声明
json
{ "iss": "{10-char Team ID}", "iat": {unix_timestamp} }json
{ "iss": "{10位团队ID}", "iat": {unix_timestamp} }Rules
规则
| Rule | Detail |
|---|---|
| Algorithm | ES256 (P-256 curve) |
| Signing key | APNs auth key (.p8 from developer portal) |
| Token lifetime | Max 1 hour (403 ExpiredProviderToken if older) |
| Refresh interval | Between 20 and 60 minutes |
| Scope | One key works for all apps in team, both environments |
| 规则 | 详情 |
|---|---|
| 算法 | ES256(P-256曲线) |
| 签名密钥 | APNs认证密钥(开发者平台获取的.p8文件) |
| Token有效期 | 最长1小时(超过会返回403 ExpiredProviderToken) |
| 刷新间隔 | 20到60分钟之间 |
| 适用范围 | 一个密钥可用于团队下所有应用、所有环境 |
Authorization Header Format
授权头格式
authorization: bearer eyAia2lkIjog...authorization: bearer eyAia2lkIjog...Payload Reference
Payload参考
aps
Dictionary Keys
apsaps
字典键
aps| Key | Type | Purpose | Since |
|---|---|---|---|
| alert | Dict/String | Alert content | iOS 10 |
| badge | Number | App icon badge (0 removes) | iOS 10 |
| sound | String/Dict | Audio playback | iOS 10 |
| thread-id | String | Notification grouping | iOS 10 |
| category | String | Actionable notification type | iOS 10 |
| content-available | Number (1) | Silent background push | iOS 10 |
| mutable-content | Number (1) | Triggers service extension | iOS 10 |
| target-content-id | String | Window/content identifier | iOS 13 |
| interruption-level | String | passive/active/time-sensitive/critical | iOS 15 |
| relevance-score | Number 0-1 | Notification summary sorting | iOS 15 |
| filter-criteria | String | Focus filter matching | iOS 15 |
| stale-date | Number | UNIX timestamp (Live Activity) | iOS 16.1 |
| content-state | Dict | Live Activity content update | iOS 16.1 |
| timestamp | Number | UNIX timestamp (Live Activity) | iOS 16.1 |
| event | String | start/update/end (Live Activity) | iOS 16.1 |
| dismissal-date | Number | UNIX timestamp (Live Activity) | iOS 16.1 |
| attributes-type | String | Live Activity struct name | iOS 17 |
| attributes | Dict | Live Activity init data | iOS 17 |
| 键 | 类型 | 用途 | 支持版本 |
|---|---|---|---|
| alert | 字典/字符串 | 通知内容 | iOS 10 |
| badge | 数字 | 应用图标角标(0表示移除) | iOS 10 |
| sound | 字符串/字典 | 音频播放 | iOS 10 |
| thread-id | 字符串 | 通知分组 | iOS 10 |
| category | 字符串 | 可操作通知类型 | iOS 10 |
| content-available | 数字(1) | 静默后台推送 | iOS 10 |
| mutable-content | 数字(1) | 触发服务扩展 | iOS 10 |
| target-content-id | 字符串 | 窗口/内容标识符 | iOS 13 |
| interruption-level | 字符串 | passive/active/time-sensitive/critical | iOS 15 |
| relevance-score | 0-1的数字 | 通知摘要排序 | iOS 15 |
| filter-criteria | 字符串 | 专注模式过滤匹配 | iOS 15 |
| stale-date | 数字 | UNIX时间戳(Live Activity用) | iOS 16.1 |
| content-state | 字典 | Live Activity内容更新 | iOS 16.1 |
| timestamp | 数字 | UNIX时间戳(Live Activity用) | iOS 16.1 |
| event | 字符串 | start/update/end(Live Activity用) | iOS 16.1 |
| dismissal-date | 数字 | UNIX时间戳(Live Activity用) | iOS 16.1 |
| attributes-type | 字符串 | Live Activity结构体名称 | iOS 17 |
| attributes | 字典 | Live Activity初始化数据 | iOS 17 |
Alert Dictionary Keys
Alert字典键
| Key | Type | Purpose |
|---|---|---|
| title | String | Short title |
| subtitle | String | Secondary description |
| body | String | Full message |
| launch-image | String | Launch screen filename |
| title-loc-key | String | Localization key for title |
| title-loc-args | [String] | Title format arguments |
| subtitle-loc-key | String | Localization key for subtitle |
| subtitle-loc-args | [String] | Subtitle format arguments |
| loc-key | String | Localization key for body |
| loc-args | [String] | Body format arguments |
| 键 | 类型 | 用途 |
|---|---|---|
| title | 字符串 | 短标题 |
| subtitle | 字符串 | 二级描述 |
| body | 字符串 | 完整消息 |
| launch-image | 字符串 | 启动页文件名 |
| title-loc-key | 字符串 | 标题本地化key |
| title-loc-args | [字符串] | 标题格式化参数 |
| subtitle-loc-key | 字符串 | 副标题本地化key |
| subtitle-loc-args | [字符串] | 副标题格式化参数 |
| loc-key | 字符串 | 正文本地化key |
| loc-args | [字符串] | 正文格式化参数 |
Sound Dictionary (Critical Alerts)
Sound字典(紧急通知用)
json
{ "critical": 1, "name": "alarm.aiff", "volume": 0.8 }json
{ "critical": 1, "name": "alarm.aiff", "volume": 0.8 }Interruption Level Values
中断级别取值
| Value | Behavior | Requires |
|---|---|---|
| passive | No sound/wake. Notification summary only. | Nothing |
| active | Default. Sound + banner. | Nothing |
| time-sensitive | Breaks scheduled delivery. Banner persists. | Time Sensitive capability |
| critical | Overrides DND and ringer switch. | Apple approval + entitlement |
| 取值 | 行为 | 要求 |
|---|---|---|
| passive | 无铃声/唤醒,仅出现在通知摘要 | 无 |
| active | 默认值,响铃+展示横幅 | 无 |
| time-sensitive | 绕过定时投递,横幅持续展示 | 时间敏感权限 |
| critical | 覆盖勿扰模式和铃声开关 | 苹果审核通过+权限声明 |
Example Payloads
示例Payload
Basic Alert
基础通知
json
{
"aps": {
"alert": {
"title": "New Message",
"subtitle": "From Alice",
"body": "Hey, are you free for lunch?"
},
"badge": 3,
"sound": "default"
}
}json
{
"aps": {
"alert": {
"title": "新消息",
"subtitle": "来自Alice",
"body": "嘿,你有空吃午饭吗?"
},
"badge": 3,
"sound": "default"
}
}Localized with loc-key/loc-args
带loc-key/loc-args的本地化通知
json
{
"aps": {
"alert": {
"title-loc-key": "MESSAGE_TITLE",
"title-loc-args": ["Alice"],
"loc-key": "MESSAGE_BODY",
"loc-args": ["Alice", "lunch"]
},
"sound": "default"
}
}json
{
"aps": {
"alert": {
"title-loc-key": "MESSAGE_TITLE",
"title-loc-args": ["Alice"],
"loc-key": "MESSAGE_BODY",
"loc-args": ["Alice", "lunch"]
},
"sound": "default"
}
}Silent Background Push
静默后台推送
json
{
"aps": {
"content-available": 1
},
"custom-key": "sync-update"
}json
{
"aps": {
"content-available": 1
},
"custom-key": "sync-update"
}Rich Notification (Service Extension)
富媒体通知(服务扩展)
json
{
"aps": {
"alert": {
"title": "Photo shared",
"body": "Alice shared a photo with you"
},
"mutable-content": 1,
"sound": "default"
},
"image-url": "https://example.com/photo.jpg"
}json
{
"aps": {
"alert": {
"title": "照片分享",
"body": "Alice给你分享了一张照片"
},
"mutable-content": 1,
"sound": "default"
},
"image-url": "https://example.com/photo.jpg"
}Critical Alert
紧急通知
json
{
"aps": {
"alert": {
"title": "Server Down",
"body": "Production database is unreachable"
},
"sound": { "critical": 1, "name": "default", "volume": 1.0 },
"interruption-level": "critical"
}
}json
{
"aps": {
"alert": {
"title": "服务器宕机",
"body": "生产环境数据库不可访问"
},
"sound": { "critical": 1, "name": "default", "volume": 1.0 },
"interruption-level": "critical"
}
}Time-Sensitive with Category
带类别的时间敏感通知
json
{
"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"
}json
{
"aps": {
"alert": {
"title": "包裹已送达",
"body": "你的订单已经送到前门了"
},
"interruption-level": "time-sensitive",
"category": "DELIVERY",
"sound": "default"
},
"order-id": "12345"
}UNUserNotificationCenter API Reference
UNUserNotificationCenter API参考
Key Methods
核心方法
| Method | Purpose |
|---|---|
| requestAuthorization(options:) | Request permission |
| notificationSettings() | Check current status |
| add(_:) | Schedule notification request |
| getPendingNotificationRequests() | List scheduled |
| removePendingNotificationRequests(withIdentifiers:) | Cancel scheduled |
| getDeliveredNotifications() | List in notification center |
| removeDeliveredNotifications(withIdentifiers:) | Remove from center |
| setNotificationCategories(_:) | Register actionable types |
| setBadgeCount(_:) | Update badge (iOS 16+) |
| supportsContentExtensions | Check content extension support |
| 方法 | 用途 |
|---|---|
| requestAuthorization(options:) | 请求通知权限 |
| notificationSettings() | 检查当前权限状态 |
| add(_:) | 调度通知请求 |
| getPendingNotificationRequests() | 列出已调度待发送的通知 |
| removePendingNotificationRequests(withIdentifiers:) | 取消已调度的通知 |
| getDeliveredNotifications() | 列出通知中心里已投递的通知 |
| removeDeliveredNotifications(withIdentifiers:) | 从通知中心移除通知 |
| setNotificationCategories(_:) | 注册可操作通知类型 |
| setBadgeCount(_:) | 更新角标(iOS 16+) |
| supportsContentExtensions | 检查内容扩展支持情况 |
UNAuthorizationOptions
UNAuthorizationOptions
| Option | Purpose |
|---|---|
| .alert | Display alerts |
| .badge | Update badge count |
| .sound | Play sounds |
| .carPlay | Show in CarPlay |
| .criticalAlert | Critical alerts (requires entitlement) |
| .provisional | Trial delivery without prompting |
| .providesAppNotificationSettings | "Configure in App" button in Settings |
| .announcement | Siri announcement (deprecated iOS 15+) |
| 选项 | 用途 |
|---|---|
| .alert | 展示通知横幅 |
| .badge | 更新角标计数 |
| .sound | 播放通知音效 |
| .carPlay | 在CarPlay中展示 |
| .criticalAlert | 紧急通知(需要权限声明) |
| .provisional | 无需弹窗授权的临时试用投递 |
| .providesAppNotificationSettings | 在设置中展示“在应用内配置”按钮 |
| .announcement | Siri播报(iOS 15+已废弃) |
UNAuthorizationStatus
UNAuthorizationStatus
| Value | Meaning |
|---|---|
| .notDetermined | No prompt shown yet |
| .denied | User denied or disabled in Settings |
| .authorized | User explicitly granted |
| .provisional | Provisional trial delivery |
| .ephemeral | App Clip temporary |
| 取值 | 含义 |
|---|---|
| .notDetermined | 尚未弹出授权提示 |
| .denied | 用户拒绝或在设置中关闭了通知 |
| .authorized | 用户明确授予了权限 |
| .provisional | 临时试用投递权限 |
| .ephemeral | App Clip临时权限 |
| @unknown default | 未知状态 |
Request Authorization
请求授权
swift
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
if granted {
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}swift
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
if granted {
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}Check Settings
检查设置
swift
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
}swift
let settings = await center.notificationSettings()
switch settings.authorizationStatus {
case .authorized: break
case .denied:
// 引导用户到系统设置
case .provisional:
// 升级为完整授权
case .notDetermined:
// 请求授权
case .ephemeral:
// App Clip — 临时权限
@unknown default: break
}Delegate Methods
代理方法
swift
// 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
}swift
// 前台展示 — 应用活跃时收到通知会调用
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async
-> UNNotificationPresentationOptions {
return [.banner, .sound, .badge]
}
// 动作响应 — 用户点击通知或动作按钮时调用
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let actionIdentifier = response.actionIdentifier
let userInfo = response.notification.request.content.userInfo
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// 用户点击了通知正文
break
case UNNotificationDismissActionIdentifier:
// 用户关闭了通知(需要category配置.customDismissAction)
break
default:
// 自定义动作
break
}
}
// 设置 — 用户从通知设置点击“在应用内配置”时调用
func userNotificationCenter(_ center: UNUserNotificationCenter,
openSettingsFor notification: UNNotification?) {
// 导航到应用内的通知设置页
}UNNotificationCategory and UNNotificationAction API
UNNotificationCategory和UNNotificationAction API
Category Registration
类别注册
swift
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])swift
let likeAction = UNNotificationAction(
identifier: "LIKE",
title: "点赞",
options: []
)
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY",
title: "回复",
options: [],
textInputButtonTitle: "发送",
textInputPlaceholder: "输入消息..."
)
let deleteAction = UNNotificationAction(
identifier: "DELETE",
title: "删除",
options: [.destructive, .authenticationRequired]
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE",
actions: [likeAction, replyAction, deleteAction],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "新消息",
categorySummaryFormat: "还有%u条消息",
options: [.customDismissAction]
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])Action Options
动作选项
| Option | Effect |
|---|---|
| .authenticationRequired | Requires device unlock |
| .destructive | Red text display |
| .foreground | Launches app to foreground |
| 选项 | 效果 |
|---|---|
| .authenticationRequired | 需要设备解锁 |
| .destructive | 展示为红色文字 |
| .foreground | 将应用唤起到前台 |
Category Options
类别选项
| Option | Effect |
|---|---|
| .customDismissAction | Fires delegate on dismiss |
| .allowInCarPlay | Show actions in CarPlay |
| .hiddenPreviewsShowTitle | Show title when previews hidden |
| .hiddenPreviewsShowSubtitle | Show subtitle when previews hidden |
| .allowAnnouncement | Siri can announce (deprecated iOS 15+) |
| 选项 | 效果 |
|---|---|
| .customDismissAction | 用户关闭通知时触发代理方法 |
| .allowInCarPlay | 在CarPlay中展示动作 |
| .hiddenPreviewsShowTitle | 预览隐藏时展示标题 |
| .hiddenPreviewsShowSubtitle | 预览隐藏时展示副标题 |
| .allowAnnouncement | Siri可播报(iOS 15+已废弃) |
UNNotificationActionIcon (iOS 15+)
UNNotificationActionIcon(iOS 15+)
swift
let icon = UNNotificationActionIcon(systemImageName: "hand.thumbsup")
let action = UNNotificationAction(
identifier: "LIKE",
title: "Like",
options: [],
icon: icon
)swift
let icon = UNNotificationActionIcon(systemImageName: "hand.thumbsup")
let action = UNNotificationAction(
identifier: "LIKE",
title: "点赞",
options: [],
icon: icon
)UNNotificationServiceExtension API
UNNotificationServiceExtension API
Modifies notification content before display. Runs in a separate extension process.
在通知展示前修改通知内容,运行在独立的扩展进程中。
Lifecycle
生命周期
| Method | Window | Purpose |
|---|---|---|
| didReceive(_:withContentHandler:) | ~30 seconds | Modify notification content |
| serviceExtensionTimeWillExpire() | Called at deadline | Deliver best attempt immediately |
| 方法 | 时间窗口 | 用途 |
|---|---|---|
| didReceive(_:withContentHandler:) | ~30秒 | 修改通知内容 |
| serviceExtensionTimeWillExpire() | 到达截止时间时调用 | 立即投递当前最优版本的内容 |
Implementation
实现示例
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,
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)
}
}
}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,
let imageURLString = content.userInfo["image-url"] as? String,
let imageURL = URL(string: imageURLString) else {
contentHandler(request.content)
return
}
// 下载并附加图片
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)
}
}
}Supported Attachment Types
支持的附件类型
| Type | Extensions | Max Size |
|---|---|---|
| Image | .jpg, .gif, .png | 10 MB |
| Audio | .aif, .wav, .mp3 | 5 MB |
| Video | .mp4, .mpeg | 50 MB |
| 类型 | 扩展名 | 最大大小 |
|---|---|---|
| 图片 | .jpg, .gif, .png | 10 MB |
| 音频 | .aif, .wav, .mp3 | 5 MB |
| 视频 | .mp4, .mpeg | 50 MB |
Payload Requirement
Payload要求
The notification payload must include in the dictionary for the service extension to fire.
"mutable-content": 1aps通知payload的字典中必须包含才能触发服务扩展。
aps"mutable-content": 1Local Notifications API
本地通知API
Trigger Types
触发器类型
| Trigger | Use Case | Repeating |
|---|---|---|
| UNTimeIntervalNotificationTrigger | After N seconds | Yes (≥60s) |
| UNCalendarNotificationTrigger | Specific date/time | Yes |
| UNLocationNotificationTrigger | Enter/exit region | Yes |
| 触发器 | 使用场景 | 可重复 |
|---|---|---|
| UNTimeIntervalNotificationTrigger | N秒后触发 | 是(间隔≥60秒) |
| UNCalendarNotificationTrigger | 指定日期/时间触发 | 是 |
| UNLocationNotificationTrigger | 进入/退出地理区域时触发 | 是 |
Time Interval Trigger
时间间隔触发器
swift
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)swift
let content = UNMutableNotificationContent()
content.title = "提醒"
content.body = "该休息一下了"
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)Calendar Trigger
日历触发器
swift
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)swift
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)Location Trigger
位置触发器
swift
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)swift
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)Limitations
限制
| Limitation | Detail |
|---|---|
| Minimum repeat interval | 60 seconds for UNTimeIntervalNotificationTrigger |
| Location authorization | Location trigger requires When In Use or Always authorization |
| No service extensions | Local notifications do not trigger UNNotificationServiceExtension |
| No background wake | Local notifications cannot use content-available for background processing |
| App extensions | Local notifications cannot be scheduled from app extensions (use app group + main app) |
| Pending limit | 64 pending notification requests per app |
| 限制 | 详情 |
|---|---|
| 最小重复间隔 | UNTimeIntervalNotificationTrigger最小间隔为60秒 |
| 位置权限 | 位置触发器需要“使用时”或“始终”位置权限 |
| 无服务扩展 | 本地通知不会触发UNNotificationServiceExtension |
| 无后台唤醒 | 本地通知不能使用content-available进行后台处理 |
| 应用扩展限制 | 不能从应用扩展调度本地通知(使用应用组+主应用实现) |
| 待发送数量限制 | 每个应用最多有64个待发送的通知请求 |
Live Activity Push Headers
Live Activity推送头
Required Headers
必填头
| Header | Value |
|---|---|
| apns-push-type | liveactivity |
| apns-topic | {bundleID}.push-type.liveactivity |
| apns-priority | 5 (routine) or 10 (time-sensitive) |
| 头部字段 | 取值 |
|---|---|
| apns-push-type | liveactivity |
| apns-topic | {bundleID}.push-type.liveactivity |
| apns-priority | 5(常规更新)或10(时间敏感更新) |
Event Types
事件类型
| Event | Purpose | Required Fields |
|---|---|---|
| start | Start Live Activity remotely | attributes-type, attributes, content-state, timestamp |
| update | Update content | content-state, timestamp |
| end | End Live Activity | timestamp (content-state optional) |
| 事件 | 用途 | 必填字段 |
|---|---|---|
| start | 远程启动Live Activity | attributes-type, attributes, content-state, timestamp |
| update | 更新内容 | content-state, timestamp |
| end | 结束Live Activity | timestamp(content-state可选) |
Update Payload
更新Payload
json
{
"aps": {
"timestamp": 1709913600,
"event": "update",
"content-state": {
"homeScore": 2,
"awayScore": 1,
"inning": "Top 7"
}
}
}json
{
"aps": {
"timestamp": 1709913600,
"event": "update",
"content-state": {
"homeScore": 2,
"awayScore": 1,
"inning": "Top 7"
}
}
}Start Payload (Push-to-Start Token)
启动Payload(推送启动Token)
json
{
"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"
}
}
}json
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
},
"alert": {
"title": "比赛开始",
"body": "Giants对阵Dodgers的比赛即将开始"
}
}
}Start Payload (Channel-Based)
启动Payload(基于频道)
json
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
}
}
}json
{
"aps": {
"timestamp": 1709913600,
"event": "start",
"content-state": {
"homeScore": 0,
"awayScore": 0,
"inning": "Top 1"
},
"attributes-type": "GameAttributes",
"attributes": {
"homeTeam": "Giants",
"awayTeam": "Dodgers"
}
}
}End Payload
结束Payload
json
{
"aps": {
"timestamp": 1709913600,
"event": "end",
"dismissal-date": 1709917200,
"content-state": {
"homeScore": 5,
"awayScore": 3,
"inning": "Final"
}
}
}json
{
"aps": {
"timestamp": 1709913600,
"event": "end",
"dismissal-date": 1709917200,
"content-state": {
"homeScore": 5,
"awayScore": 3,
"inning": "Final"
}
}
}Push-to-Start Token
推送启动Token
swift
// 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)
}swift
// 监听推送启动Token更新(iOS 17.2+)
for await token in Activity<GameAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
sendPushToStartTokenToServer(tokenString)
}Activity Push Token
Activity推送Token
swift
// 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 encoding rule: the system always uses default JSONDecoder — do not use custom encoding strategies in your ActivityAttributes.ContentState.
swift
// 监听单个Activity对应的推送Token更新
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中使用自定义编码策略。
Broadcast Push API (iOS 18+)
广播推送API(iOS 18+)
Server-to-many push for Live Activities without tracking individual device tokens.
无需追踪单个设备Token即可向大量设备推送Live Activity的服务端能力。
Endpoint
端点
POST /4/broadcasts/apps/{TOPIC}POST /4/broadcasts/apps/{TOPIC}Headers
请求头
| Header | Value |
|---|---|
| apns-push-type | liveactivity |
| apns-channel-id | {channelID} |
| authorization | bearer {JWT} |
| 头部字段 | 取值 |
|---|---|
| apns-push-type | liveactivity |
| apns-channel-id | {频道ID} |
| authorization | bearer {JWT} |
Subscribe via Channel
通过频道订阅
swift
try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .channel(channelId)
)swift
try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .channel(channelId)
)Channel Storage Policies
频道存储策略
| Policy | Behavior | Budget |
|---|---|---|
| No Storage | Deliver only to connected devices | Higher |
| Most Recent Message | Store latest for offline devices | Lower |
| 策略 | 行为 | 配额 |
|---|---|---|
| 不存储 | 仅投递给当前连接的设备 | 更高 |
| 存储最新消息 | 为离线设备存储最新一条消息 | 更低 |
Command-Line Testing
命令行测试
JWT Generation
JWT生成
bash
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}"bash
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}"Send Alert Push
发送通知推送
bash
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}bash
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}Send Live Activity Push
发送Live Activity推送
bash
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_TOKENbash
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_TOKENSimulator Push
模拟器推送
bash
xcrun simctl push booted com.example.app payload.jsonbash
xcrun simctl push booted com.example.app payload.jsonSimulator Payload File
模拟器Payload文件
json
{
"Simulator Target Bundle": "com.example.app",
"aps": {
"alert": { "title": "Test", "body": "Hello" },
"sound": "default"
}
}json
{
"Simulator Target Bundle": "com.example.app",
"aps": {
"alert": { "title": "测试", "body": "你好" },
"sound": "default"
}
}Resources
资源
WWDC: 2021-10091, 2023-10025, 2023-10185, 2024-10069
Docs: /usernotifications, /usernotifications/sending-notification-requests-to-apns, /usernotifications/generating-a-remote-notification, /activitykit
Skills: axiom-push-notifications, axiom-push-notifications-diag, axiom-extensions-widgets
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