You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 19:41:02