黑苹果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富媒体附件(图片、音频、视频)

远程推送特殊组件

组件职责
PKPushRegistryVoIP推送注册(macOS上较少使用)
NSApplicationDelegate接收远程推送的deviceToken回调
APNs ServerApple服务器端,负责转发推送消息

第一步:请求通知权限

在发送任何通知之前,必须先获得用户的授权。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证书配置流程

  1. 在Apple Developer后台创建App ID,启用Push Notifications能力
  2. 生成APNs密钥(推荐使用.p8格式的Token-Based认证)或证书(.p12格式)
  3. 在Xcode项目中启用Push Notifications Capability
  4. 注册远程推送并获取Device Token
  5. 将Device Token发送到自己的服务器
  6. 服务器通过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远程推送,开发者可以灵活地选择适合自己场景的实现方式。

关键要点

  1. 使用.provisional权限策略降低用户拒绝概率
  2. 利用UNNotificationAttachment提供富媒体通知体验
  3. 通过UNNotificationCategory和Action实现通知内交互
  4. APNs远程推送使用.p8 Token认证更方便
  5. 黑苹果环境下注意TCC权限和APNs网络连通性
  6. 静默推送有频率限制,不适合高频实时消息

希望本文能帮助你完善macOS应用的通知系统体验!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。