Swift6下改写UNNotificationServiceExtension子类:安全传参至Task
迁移Swift6时UNNotificationServiceExtension中安全传递bestAttemptContent与contentHandler至Task的方案
在Swift6严格并发检查规则下,直接将UNMutableNotificationContent(引用类型)和contentHandler闭包传入异步Task会触发并发安全警告甚至崩溃,核心原因是这些对象并非线程安全,且Swift6对逃逸闭包、引用类型的并发访问要求更严格。你尝试的MainActor.run和consuming方案踩坑的具体原因:
MainActor.run仅将任务调度到主线程,但未解决引用类型的并发访问权限问题,警告转移到内部是因为对象仍未被正确标记为MainActor管辖;consuming关键字专为值类型的所有权转移设计,对引用类型不适用,强行使用会导致所有权管理混乱,引发运行时崩溃。
正确解决方案
1. 用@MainActor标记核心属性与方法
UNNotificationServiceExtension的生命周期方法本身运行在主线程,将contentHandler、bestAttemptContent及相关处理方法标记为@MainActor,确保所有对这些对象的访问都在主线程,符合Swift6并发安全要求:
final class NotificationService: UNNotificationServiceExtension { // 标记属性归MainActor管辖 @MainActor var contentHandler: ((UNNotificationContent) -> Void)? @MainActor var bestAttemptContent: UNMutableNotificationContent? @MainActor var customNotificationTask: Task<Void, Error>? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { Task { @MainActor in self.contentHandler = contentHandler self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) guard let bestAttemptContent = self.bestAttemptContent else { invokeContentHandler(with: request.content) return } do { let notificationModel = try PushNotificationUserInfo(data: request.content.userInfo) guard let templatedImageUrl = notificationModel.templatedImageUrlString, let imageUrl = imageUrl(from: templatedImageUrl) else { invokeContentHandler(with: bestAttemptContent) return } downloadImageTask(imageUrl: imageUrl) } catch { invokeContentHandler(with: bestAttemptContent) } } } @MainActor private func invokeContentHandler(with content: UNNotificationContent) { contentHandler?(content) // 调用后清空属性,避免重复调用 contentHandler = nil bestAttemptContent = nil customNotificationTask?.cancel() customNotificationTask = nil } @MainActor private func downloadImageTask(imageUrl: URL) { // 捕获当前值,避免异步执行时属性被修改/置空 guard let bestAttemptContent = bestAttemptContent, let contentHandler = contentHandler else { return } customNotificationTask = Task { do { let (location, _) = try await URLSession.shared.download(from: imageUrl) let desiredLocation = URL(fileURLWithPath: "\(location.path)\(imageUrl.lastPathComponent)") try FileManager.default.moveItem(at: location, to: desiredLocation) let attachment = try UNNotificationAttachment(identifier: imageUrl.absoluteString, url: desiredLocation, options: nil) // 回到MainActor修改通知内容并调用handler await MainActor.run { bestAttemptContent.attachments = [attachment] invokeContentHandler(with: bestAttemptContent) } } catch { // 出错时确保调用handler,避免通知超时不显示 await MainActor.run { invokeContentHandler(with: bestAttemptContent) } } } } override func serviceExtensionTimeWillExpire() { // 扩展超时时,强制调用handler Task { @MainActor in if let content = bestAttemptContent { invokeContentHandler(with: content) } } } }
2. 关键细节说明
- 捕获当前值:启动Task前先捕获
bestAttemptContent和contentHandler的当前值,避免异步执行时这些属性被修改或置空; - MainActor修改通知内容:所有对
UNMutableNotificationContent的操作必须在MainActor上执行,因为它属于UI相关对象,并非线程安全; - 避免重复调用contentHandler:每次调用
invokeContentHandler后清空属性并取消Task,防止因多次调用导致系统报错; - 处理超时场景:重写
serviceExtensionTimeWillExpire方法,确保扩展超时前调用contentHandler,避免通知无法正常显示。
内容的提问来源于stack exchange,提问作者Seto
相关产品推荐
相关产品推荐

