黑苹果macOS UserNotifications推送通知开发完全指南:从本地通知到APNs远程推送的完整实现与富媒体交互
发布时间:2026年6月12日 | 分类:黑苹果 | 关键词:UserNotifications,推送通知,APNs,UNNotification
前言:macOS通知系统的现代化演进
通知系统是macOS用户体验的重要组成部分。从最早的NSUserNotification到iOS 10/macOS 10.14引入的UserNotifications框架,Apple完成了一次彻底的API现代化。UserNotifications(简称UN)框架统一了iOS和macOS的通知处理方式,提供了本地通知、远程推送通知、通知内容扩展、通知操作等丰富的功能。
对于黑苹果用户来说,正确配置推送通知(特别是APNs远程推送)可能面临额外的挑战:SMBIOS机型选择影响APNs证书生成、网络环境差异导致推送延迟、以及TCC权限数据库可能的异常。本文将全面覆盖UserNotifications框架的所有关键知识点,从基础API到生产级远程推送实现,帮助你构建功能完善的通知系统。
UserNotifications框架架构
UserNotifications框架采用了一套清晰的责任分离架构:
核心组件
| 组件 | 职责 |
| UNUserNotificationCenter | 通知管理的中央调度器,负责请求权限、调度和取消通知 |
| UNNotificationRequest | 表示一个通知请求,包含标识符、内容和触发器 |
| UNMutableNotificationContent | 通知的内容(标题、副标题、正文、附件、用户信息) |
| UNNotificationTrigger | 触发条件(时间、日历、地理位置) |
| UNNotificationCategory | 通知类别,关联可用的操作按钮 |
| UNNotificationAction | 用户可执行的操作(按钮或文本输入) |
| UNNotificationAttachment | 富媒体附件(图片、音频、视频) |
远程推送特殊组件
| 组件 | 职责 |
| PKPushRegistry | VoIP推送注册(macOS上较少使用) |
| NSApplicationDelegate | 接收远程推送的deviceToken回调 |
| APNs Server | Apple服务器端,负责转发推送消息 |
第一步:请求通知权限
在发送任何通知之前,必须先获得用户的授权。UserNotifications提供了精细的权限选项。
权限请求实现
import UserNotifications
class NotificationManager {
static let shared = NotificationManager()
private let center = UNUserNotificationCenter.current()
func requestAuthorization() async -> Bool {
do {
// 请求所需的权限类型
let granted = try await center.requestAuthorization(
options: [.alert, .sound, .badge, .provisional]
)
if granted {
print("通知权限已授予")
// 注册远程推送(在获得权限后)
await MainActor.run {
NSApplication.shared.registerForRemoteNotifications()
}
} else {
print("用户拒绝了通知权限")
}
return granted
} catch {
print("权限请求失败: \(error)")
return false
}
}
func checkNotificationSettings() async {
let settings = await center.notificationSettings()
switch settings.authorizationStatus {
case .authorized:
print("完全授权")
case .provisional:
print("临时授权(静默传递到通知中心)")
case .denied:
print("已拒绝")
case .notDetermined:
print("尚未请求")
@unknown default:
break
}
print("提醒样式: \(settings.alertSetting)")
print("声音: \(settings.soundSetting)")
print("角标: \(settings.badgeSetting)")
}
}
macOS特有的权限选项
macOS引入了.provisional(临时授权)选项,这在桌面应用中非常有价值:
- 通知将静默显示在通知中心中,不会弹出横幅
- 不打断用户工作流,降低了用户拒绝授权的概率
- 可以后续通过用户交互升级为完全授权
- 适合"先试用再决定"的产品策略
第二步:发送本地通知
本地通知是最常用的通知类型,不需要服务器支持,完全在设备端触发。
基础本地通知
func scheduleBasicNotification() async {
// 创建通知内容
let content = UNMutableNotificationContent()
content.title = "备份完成"
content.subtitle = "Time Machine已成功备份"
content.body = "上次备份时间:\(Date().formatted())。共备份156GB数据。"
content.sound = .default
content.badge = 1
// 创建触发器:10秒后触发
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 10,
repeats: false
)
// 创建请求
let request = UNNotificationRequest(
identifier: "backup-complete-\(UUID().uuidString)",
content: content,
trigger: trigger
)
// 添加到通知中心
do {
try await UNUserNotificationCenter.current().add(request)
print("通知已调度")
} catch {
print("调度失败: \(error)")
}
}
日历触发器通知
// 每天早上9点提醒
func scheduleDailyReminder() {
let content = UNMutableNotificationContent()
content.title = "每日健康提醒"
content.body = "该起来活动一下了!建议离开电脑5分钟。"
content.sound = .default
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let trigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents,
repeats: true
)
let request = UNNotificationRequest(
identifier: "daily-reminder",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request)
}
带富媒体附件的通知
func scheduleNotificationWithAttachment() async {
// 先准备附件文件
let tempDir = FileManager.default.temporaryDirectory
let imageURL = tempDir.appendingPathComponent("screenshot.jpg")
// 假设从某处获取了截屏图片
let imageData = // ... 获取图片数据 ...
try? imageData?.write(to: imageURL)
do {
let attachment = try UNNotificationAttachment(
identifier: "screenshot",
url: imageURL,
options: [
UNNotificationAttachmentOptionsThumbnailHiddenKey: false,
UNNotificationAttachmentOptionsThumbnailClippingRectKey:
CGRect(x: 0, y: 0, width: 0.5, height: 1.0).dictionaryRepresentation
]
)
let content = UNMutableNotificationContent()
content.title = "截屏已保存"
content.body = "当前屏幕截图已保存到桌面"
content.attachments = [attachment]
content.sound = .default
let request = UNNotificationRequest(
identifier: "screenshot",
content: content,
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
)
try await UNUserNotificationCenter.current().add(request)
} catch {
print("附件创建失败: \(error)")
}
}
附件限制:macOS对通知附件有严格的大小限制。单个附件不超过10MB,总附件大小不超过30MB。超过限制的通知将被系统静默丢弃。
第三步:通知操作与交互
通知不仅可以展示信息,还可以让用户通过操作按钮直接与应用交互,无需打开应用窗口。
定义通知操作
func registerNotificationCategories() {
// 定义"标记为已读"操作
let markReadAction = UNNotificationAction(
identifier: "MARK_READ",
title: "标记为已读",
options: [.authenticationRequired]
)
// 定义"回复"操作(带文本输入)
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY",
title: "回复",
options: [.authenticationRequired],
textInputButtonTitle: "发送",
textInputPlaceholder: "输入回复内容..."
)
// 定义"删除"操作(破坏性操作)
let deleteAction = UNNotificationAction(
identifier: "DELETE",
title: "删除",
options: [.destructive, .authenticationRequired]
)
// 创建消息类别
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE",
actions: [replyAction, markReadAction, deleteAction],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "一条新消息",
categorySummaryFormat: "%u条来自%@的新消息",
options: [.customDismissAction]
)
// 注册类别
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}
操作选项详解:
- .authenticationRequired:需要用户解锁设备后才能执行(桌面端为输入密码)
- .destructive:以红色样式展示,提示这是破坏性操作
- .foreground:点击后在执行操作前将应用带到前台
处理通知操作的响应
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
// 当用户在通知上点击操作时触发
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
switch response.actionIdentifier {
case "MARK_READ":
handleMarkRead(userInfo: userInfo)
case "REPLY":
if let textResponse = response as? UNTextInputNotificationResponse {
let replyText = textResponse.userText
handleReply(text: replyText, userInfo: userInfo)
}
case "DELETE":
handleDelete(userInfo: userInfo)
case UNNotificationDefaultActionIdentifier:
// 用户点击了通知本体(打开了应用)
handleNotificationTap(userInfo: userInfo)
case UNNotificationDismissActionIdentifier:
// 用户关闭了通知
handleDismiss(userInfo: userInfo)
default:
break
}
completionHandler()
}
// 当应用在前台时收到通知(macOS默认不展示)
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// macOS前台也展示通知
completionHandler([.banner, .sound, .badge, .list])
}
}
第四步:APNs远程推送实现
远程推送是实现服务端主动触达用户的核心能力。对于黑苹果环境,APNs的配置有特殊注意点。
APNs证书配置流程
- 在Apple Developer后台创建App ID,启用Push Notifications能力
- 生成APNs密钥(推荐使用.p8格式的Token-Based认证)或证书(.p12格式)
- 在Xcode项目中启用Push Notifications Capability
- 注册远程推送并获取Device Token
- 将Device Token发送到自己的服务器
- 服务器通过APNs服务端发送推送消息
Device Token注册
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
// 注册远程推送
NSApplication.shared.registerForRemoteNotifications()
}
func application(_ application: NSApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// 将Data转换为十六进制字符串
let tokenString = deviceToken.map {
String(format: "%02.2hhx", $0)
}.joined()
print("APNs Device Token: \(tokenString)")
// 发送Token到自己的服务器
sendDeviceTokenToServer(tokenString)
}
func application(_ application: NSApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("APNs注册失败: \(error.localizedDescription)")
// 黑苹果常见原因:网络问题导致无法连接Apple服务器
// 尝试检查网络代理设置和DNS配置
}
}
黑苹果APNs注册常见问题:
| 问题 | 原因 | 解决方案 |
| 注册APNs失败 | 网络代理/防火墙阻止了到api.push.apple.com的连接 | 配置代理规则放行*.push.apple.com:443 |
| Token为空 | SMBIOS机型与证书不匹配 | 确保SMBIOS与App ID bundle一致 |
| 推送延迟 | 使用了VPN或代理 | 将APNs流量排除在代理外 |
| 推送收不到 | TCC数据库异常 | 用tccutil reset通知权限后重试 |
服务端推送实现(Python示例)
import jwt
import time
import httpx
class APNsClient:
def __init__(self, key_id, team_id, key_path, bundle_id, use_sandbox=False):
self.key_id = key_id
self.team_id = team_id
self.bundle_id = bundle_id
self.use_sandbox = use_sandbox
with open(key_path, 'r') as f:
self.private_key = f.read()
if use_sandbox:
self.base_url = "https://api.sandbox.push.apple.com"
else:
self.base_url = "https://api.push.apple.com"
def _generate_token(self):
# 生成JWT Token用于APNs认证
now = int(time.time())
payload = {
'iss': self.team_id,
'iat': now
}
headers = {
'alg': 'ES256',
'kid': self.key_id
}
return jwt.encode(payload, self.private_key, algorithm='ES256', headers=headers)
async def send_push(self, device_token, title, body, badge=None, sound="default"):
# 发送推送通知到指定设备
token = self._generate_token()
payload = {
"aps": {
"alert": {
"title": title,
"body": body
},
"sound": sound,
"badge": badge,
"mutable-content": 1
}
}
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/3/device/{device_token}",
json=payload,
headers={
"authorization": f"bearer {token}",
"apns-topic": self.bundle_id,
"apns-priority": "10",
"apns-push-type": "alert"
}
)
return response.status_code, response.text
第五步:静默推送(Silent Push)
静默推送不会向用户展示通知,而是唤醒应用在后台执行任务。这在数据同步、内容预加载等场景中非常有用。
静默推送发送
# 服务端发送静默推送
payload = {
"aps": {
"content-available": 1 # 关键字段:表明这是静默推送
},
"sync-type": "full",
"timestamp": int(time.time())
}
# apns-push-type 必须设为 "background"
# apns-priority 设为 5(低优先级,系统可延迟处理)
macOS端接收静默推送
// 需要在App Capabilities中启用 "Background Modes" -> "Remote Notifications"
func application(_ application: NSApplication,
didReceiveRemoteNotification userInfo: [String: Any]) {
let syncType = userInfo["sync-type"] as? String ?? "unknown"
print("收到静默推送,同步类型: \(syncType)")
// 在后台执行数据同步
let backgroundTask = ProcessInfo.processInfo.beginActivity(
options: .background,
reason: "远程数据同步"
)
Task {
await performDataSync(type: syncType)
ProcessInfo.processInfo.endActivity(backgroundTask)
}
}
静默推送的限制:macOS对静默推送有频率限制(每个应用每小时约3-5条),超过频率的推送将被系统延迟或丢弃。不建议用于实时消息传递。
第六步:通知管理API
查看已调度的待推送通知
func listPendingNotifications() async {
let pendingRequests = await UNUserNotificationCenter.current()
.pendingNotificationRequests()
print("待推送通知: \(pendingRequests.count) 条")
for request in pendingRequests {
print(" ID: \(request.identifier)")
print(" 标题: \(request.content.title)")
if let trigger = request.trigger as? UNCalendarNotificationTrigger {
print(" 触发时间: \(String(describing: trigger.nextTriggerDate()))")
}
}
}
取消通知
func cancelNotification(identifier: String) {
UNUserNotificationCenter.current()
.removePendingNotificationRequests(withIdentifiers: [identifier])
}
func cancelAllNotifications() {
UNUserNotificationCenter.current()
.removeAllPendingNotificationRequests()
}
更新已存在的通知
func updateNotification(identifier: String, newContent: UNMutableNotificationContent) {
// 使用相同的identifier会覆盖旧的通知
let request = UNNotificationRequest(
identifier: identifier, // 相同的ID
content: newContent,
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
)
UNUserNotificationCenter.current().add(request)
}
第七步:通知服务扩展(Notification Service Extension)
通知服务扩展允许在推送到达用户设备后、展示前,对通知内容进行修改。这在以下场景中特别有用:
- 解密加密的推送内容
- 下载通知附件(图片等)
- 替换通知内容为本地化字符串
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 bestAttemptContent = bestAttemptContent else {
contentHandler(request.content)
return
}
// 解密内容
if let encryptedBody = bestAttemptContent.userInfo["encrypted-body"] as? String {
bestAttemptContent.body = decrypt(encryptedBody)
}
// 下载通知图片附件
if let imageURLString = bestAttemptContent.userInfo["image-url"] as? String,
let imageURL = URL(string: imageURLString) {
let task = URLSession.shared.downloadTask(with: imageURL) { url, _, error in
if let url = url {
do {
let attachment = try UNNotificationAttachment(
identifier: "remote-image",
url: url,
options: nil
)
bestAttemptContent.attachments = [attachment]
} catch {
print("附件创建失败")
}
}
contentHandler(bestAttemptContent)
}
task.resume()
return
}
contentHandler(bestAttemptContent)
}
// 在超时前未完成时调用(约30秒)
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
黑苹果推送系统专项调试
在黑苹果环境中调试推送问题,可以使用以下方法:
验证APNs连接
# 测试与APNs服务器的连通性
nc -zv api.push.apple.com 443
nc -zv api.sandbox.push.apple.com 443
# 查看系统推送相关日志
log stream --predicate 'subsystem contains "apsd"' --level debug
log show --predicate 'process == "apsd"' --last 1h
重置推送状态
# 重置推送守护进程
sudo launchctl kickstart -k system/com.apple.apsd
# 清理推送缓存
sudo rm -rf /Library/Application\ Support/Apple/PushService/
总结
UserNotifications框架为macOS应用提供了完整的通知解决方案。从简单的本地提醒到复杂的APNs远程推送,开发者可以灵活地选择适合自己场景的实现方式。
关键要点:
- 使用.provisional权限策略降低用户拒绝概率
- 利用UNNotificationAttachment提供富媒体通知体验
- 通过UNNotificationCategory和Action实现通知内交互
- APNs远程推送使用.p8 Token认证更方便
- 黑苹果环境下注意TCC权限和APNs网络连通性
- 静默推送有频率限制,不适合高频实时消息
希望本文能帮助你完善macOS应用的通知系统体验!


评论(0)