axiom-push-notifications-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Push 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

端点

EnvironmentHostPort
Developmentapi.sandbox.push.apple.com443 or 2197
Productionapi.push.apple.com443 or 2197
环境主机地址端口
开发环境api.sandbox.push.apple.com443 或 2197
生产环境api.push.apple.com443 或 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/json
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 Headers

APNs请求头

HeaderRequiredValuesNotes
apns-push-typeYesalert, background, liveactivity, voip, complication, fileprovider, mdm, locationMust match payload content
apns-topicYesBundle ID (or .push-type.liveactivity suffix)Required for token-based auth
apns-priorityNo10 (immediate), 5 (power-conscious), 1 (low)Default: 10 for alert, 5 for background
apns-expirationNoUNIX timestamp or 00 = deliver once, don't store
apns-collapse-idNoString ≤64 bytesReplaces matching notification on device
apns-idNoUUID (lowercase)Returned by APNs for tracking
authorizationToken authbearer {JWT}Not needed for certificate auth
apns-unique-idResponse onlyUUIDUse with Push Notifications Console delivery log
头部字段必填可选值说明
apns-push-typealert, background, liveactivity, voip, complication, fileprovider, mdm, location必须与payload内容匹配
apns-topic包ID(或带.push-type.liveactivity后缀)基于token的认证必填
apns-priority10(立即推送), 5(功耗优先), 1(低优先级)默认值:alert类型为10,后台推送为5
apns-expirationUNIX时间戳或00 = 仅投递一次,不存储
apns-collapse-id长度≤64字节的字符串替换设备上匹配的通知
apns-id小写UUIDAPNs返回用于追踪请求
authorizationToken认证必填bearer {JWT}证书认证不需要
apns-unique-id仅响应返回UUID配合推送通知控制台投递日志使用

Response Codes

响应码

StatusMeaningCommon Cause
200Success
400Bad requestMalformed JSON, missing required header
403ForbiddenExpired JWT, wrong team/key, topic mismatch
404Not foundInvalid device token path
405Method not allowedNot using POST
410UnregisteredDevice token no longer active (app uninstalled)
413Payload too largeExceeds 4KB (5KB for VoIP)
429Too many requestsRate limited by APNs
500Internal server errorAPNs issue, retry
503Service unavailableAPNs overloaded, retry with backoff

状态码含义常见原因
200成功
400错误请求JSON格式错误、缺失必填头部
403禁止访问JWT过期、团队/密钥错误、topic不匹配
404未找到设备token路径无效
405请求方法不允许未使用POST方法
410已注销设备token不再活跃(应用已卸载)
413payload过大超过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

规则

RuleDetail
AlgorithmES256 (P-256 curve)
Signing keyAPNs auth key (.p8 from developer portal)
Token lifetimeMax 1 hour (403 ExpiredProviderToken if older)
Refresh intervalBetween 20 and 60 minutes
ScopeOne 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

aps
字典键

KeyTypePurposeSince
alertDict/StringAlert contentiOS 10
badgeNumberApp icon badge (0 removes)iOS 10
soundString/DictAudio playbackiOS 10
thread-idStringNotification groupingiOS 10
categoryStringActionable notification typeiOS 10
content-availableNumber (1)Silent background pushiOS 10
mutable-contentNumber (1)Triggers service extensioniOS 10
target-content-idStringWindow/content identifieriOS 13
interruption-levelStringpassive/active/time-sensitive/criticaliOS 15
relevance-scoreNumber 0-1Notification summary sortingiOS 15
filter-criteriaStringFocus filter matchingiOS 15
stale-dateNumberUNIX timestamp (Live Activity)iOS 16.1
content-stateDictLive Activity content updateiOS 16.1
timestampNumberUNIX timestamp (Live Activity)iOS 16.1
eventStringstart/update/end (Live Activity)iOS 16.1
dismissal-dateNumberUNIX timestamp (Live Activity)iOS 16.1
attributes-typeStringLive Activity struct nameiOS 17
attributesDictLive Activity init dataiOS 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/criticaliOS 15
relevance-score0-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字典键

KeyTypePurpose
titleStringShort title
subtitleStringSecondary description
bodyStringFull message
launch-imageStringLaunch screen filename
title-loc-keyStringLocalization key for title
title-loc-args[String]Title format arguments
subtitle-loc-keyStringLocalization key for subtitle
subtitle-loc-args[String]Subtitle format arguments
loc-keyStringLocalization 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

中断级别取值

ValueBehaviorRequires
passiveNo sound/wake. Notification summary only.Nothing
activeDefault. Sound + banner.Nothing
time-sensitiveBreaks scheduled delivery. Banner persists.Time Sensitive capability
criticalOverrides 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

核心方法

MethodPurpose
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+)
supportsContentExtensionsCheck content extension support
方法用途
requestAuthorization(options:)请求通知权限
notificationSettings()检查当前权限状态
add(_:)调度通知请求
getPendingNotificationRequests()列出已调度待发送的通知
removePendingNotificationRequests(withIdentifiers:)取消已调度的通知
getDeliveredNotifications()列出通知中心里已投递的通知
removeDeliveredNotifications(withIdentifiers:)从通知中心移除通知
setNotificationCategories(_:)注册可操作通知类型
setBadgeCount(_:)更新角标(iOS 16+)
supportsContentExtensions检查内容扩展支持情况

UNAuthorizationOptions

UNAuthorizationOptions

OptionPurpose
.alertDisplay alerts
.badgeUpdate badge count
.soundPlay sounds
.carPlayShow in CarPlay
.criticalAlertCritical alerts (requires entitlement)
.provisionalTrial delivery without prompting
.providesAppNotificationSettings"Configure in App" button in Settings
.announcementSiri announcement (deprecated iOS 15+)
选项用途
.alert展示通知横幅
.badge更新角标计数
.sound播放通知音效
.carPlay在CarPlay中展示
.criticalAlert紧急通知(需要权限声明)
.provisional无需弹窗授权的临时试用投递
.providesAppNotificationSettings在设置中展示“在应用内配置”按钮
.announcementSiri播报(iOS 15+已废弃)

UNAuthorizationStatus

UNAuthorizationStatus

ValueMeaning
.notDeterminedNo prompt shown yet
.deniedUser denied or disabled in Settings
.authorizedUser explicitly granted
.provisionalProvisional trial delivery
.ephemeralApp Clip temporary
取值含义
.notDetermined尚未弹出授权提示
.denied用户拒绝或在设置中关闭了通知
.authorized用户明确授予了权限
.provisional临时试用投递权限
.ephemeralApp 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

动作选项

OptionEffect
.authenticationRequiredRequires device unlock
.destructiveRed text display
.foregroundLaunches app to foreground
选项效果
.authenticationRequired需要设备解锁
.destructive展示为红色文字
.foreground将应用唤起到前台

Category Options

类别选项

OptionEffect
.customDismissActionFires delegate on dismiss
.allowInCarPlayShow actions in CarPlay
.hiddenPreviewsShowTitleShow title when previews hidden
.hiddenPreviewsShowSubtitleShow subtitle when previews hidden
.allowAnnouncementSiri can announce (deprecated iOS 15+)
选项效果
.customDismissAction用户关闭通知时触发代理方法
.allowInCarPlay在CarPlay中展示动作
.hiddenPreviewsShowTitle预览隐藏时展示标题
.hiddenPreviewsShowSubtitle预览隐藏时展示副标题
.allowAnnouncementSiri可播报(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

生命周期

MethodWindowPurpose
didReceive(_:withContentHandler:)~30 secondsModify notification content
serviceExtensionTimeWillExpire()Called at deadlineDeliver 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

支持的附件类型

TypeExtensionsMax Size
Image.jpg, .gif, .png10 MB
Audio.aif, .wav, .mp35 MB
Video.mp4, .mpeg50 MB
类型扩展名最大大小
图片.jpg, .gif, .png10 MB
音频.aif, .wav, .mp35 MB
视频.mp4, .mpeg50 MB

Payload Requirement

Payload要求

The notification payload must include
"mutable-content": 1
in the
aps
dictionary for the service extension to fire.

通知payload的
aps
字典中必须包含
"mutable-content": 1
才能触发服务扩展。

Local Notifications API

本地通知API

Trigger Types

触发器类型

TriggerUse CaseRepeating
UNTimeIntervalNotificationTriggerAfter N secondsYes (≥60s)
UNCalendarNotificationTriggerSpecific date/timeYes
UNLocationNotificationTriggerEnter/exit regionYes
触发器使用场景可重复
UNTimeIntervalNotificationTriggerN秒后触发是(间隔≥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

限制

LimitationDetail
Minimum repeat interval60 seconds for UNTimeIntervalNotificationTrigger
Location authorizationLocation trigger requires When In Use or Always authorization
No service extensionsLocal notifications do not trigger UNNotificationServiceExtension
No background wakeLocal notifications cannot use content-available for background processing
App extensionsLocal notifications cannot be scheduled from app extensions (use app group + main app)
Pending limit64 pending notification requests per app

限制详情
最小重复间隔UNTimeIntervalNotificationTrigger最小间隔为60秒
位置权限位置触发器需要“使用时”或“始终”位置权限
无服务扩展本地通知不会触发UNNotificationServiceExtension
无后台唤醒本地通知不能使用content-available进行后台处理
应用扩展限制不能从应用扩展调度本地通知(使用应用组+主应用实现)
待发送数量限制每个应用最多有64个待发送的通知请求

Live Activity Push Headers

Live Activity推送头

Required Headers

必填头

HeaderValue
apns-push-typeliveactivity
apns-topic{bundleID}.push-type.liveactivity
apns-priority5 (routine) or 10 (time-sensitive)
头部字段取值
apns-push-typeliveactivity
apns-topic{bundleID}.push-type.liveactivity
apns-priority5(常规更新)或10(时间敏感更新)

Event Types

事件类型

EventPurposeRequired Fields
startStart Live Activity remotelyattributes-type, attributes, content-state, timestamp
updateUpdate contentcontent-state, timestamp
endEnd Live Activitytimestamp (content-state optional)
事件用途必填字段
start远程启动Live Activityattributes-type, attributes, content-state, timestamp
update更新内容content-state, timestamp
end结束Live Activitytimestamp(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

请求头

HeaderValue
apns-push-typeliveactivity
apns-channel-id{channelID}
authorizationbearer {JWT}
头部字段取值
apns-push-typeliveactivity
apns-channel-id{频道ID}
authorizationbearer {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

频道存储策略

PolicyBehaviorBudget
No StorageDeliver only to connected devicesHigher
Most Recent MessageStore latest for offline devicesLower

策略行为配额
不存储仅投递给当前连接的设备更高
存储最新消息为离线设备存储最新一条消息更低

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_TOKEN
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_TOKEN

Simulator Push

模拟器推送

bash
xcrun simctl push booted com.example.app payload.json
bash
xcrun simctl push booted com.example.app payload.json

Simulator 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