尽管 Swift 提供严格并发检查已有一段时间,但许多苹果官方 API 仍未对此进行充分适配,这种情况可能还会持续相当长的时间。随着 Swift 6 的逐步普及,这个问题变得愈发突出:开发者一方面希望享受 Swift 编译器带来的并发安全保障,另一方面又对如何让代码满足编译要求感到困惑。本文将通过一个 NSTextAttachmentViewProvider 的实现案例,介绍 MainActor.assumeIsolated 在特定场景下的妙用。
一封邮件
几天前,我收到了一封来自网友 Lucas 的邮件。他遇到了一个旧 API 无法满足 Swift 6 编译要求的问题。他希望通过 NSTextAttachment + NSTextAttachmentViewProvider 在 UITextView 中实现插入自定义视图的功能。为此,他尝试在 NSTextAttachmentViewProvider 的 loadView 方法中加载一个 SwiftUI 视图:
class CustomAttachmentViewProvider: NSTextAttachmentViewProvider {
    override func loadView() {
        let hosting = UIHostingController(rootView: InlineSwiftUIButton {
            print("SwiftUI Button tapped!")
        })
        hosting.view.backgroundColor = .clear
        // Assign to the provider's view
        self.view = hosting.view
    }
}
// MARK: - SwiftUI Button View
struct InlineSwiftUIButton: View {
    var action: () -> Void
    var body: some View {
        Button("Click Me") {
            action()
        }
        .padding(6)
        .background(Color.blue.opacity(0.2))
        .cornerRadius(8)
    }
}在 Xcode 开启 Swift 6 模式后(Default Actor Isolation 设为 nonisolated),上述代码出现了如下错误/警告:
 
                  
为了解决这个编译问题,Lucas 尝试了多种方案:
- 在 loadView方法上添加@MainActor
 
                  
- 在 CustomAttachmentViewProvider类上添加@MainActor
 
                  
- 将 loadView中的代码包裹在Task中
 
                  
然而,无论采用哪种方法都无法满足 Swift 编译器的要求。
在 Xcode 26 beta 5 中,最初的代码会报错(编译失败),而在 beta 6 中则会产生警告(可以编译)。
那么,这是否意味着此类旧 API 无法在 Swift 6 目标中进行完美编译呢(没有警告、不会报错)?
分析问题
Swift 6 编译器之所以不认可上述几种写法,主要原因如下:
- UIHostingController在声明中标注了- @MainActor,这意味着它必须在 MainActor 上下文中创建
- NSTextAttachmentViewProvider的原始声明中没有明确的隔离域
- 单独为 loadView添加@MainActor与父类的要求不符
- 如果在 loadView中构建 MainActor 异步上下文,无法安全地传递self
我们似乎陷入了一个两难境地:既需要在 MainActor 中构建 UIHostingController,又不能在 MainActor 中将构建后的视图(UIView)赋值给 self.view。
有没有一种方式能够满足这种”既要又要”的需求呢?
MainActor.assumeIsolated:在同步方法中提供 MainActor 上下文
在 Swift 的并发 API 中,有一个看起来颇为特殊的存在:MainActor.assumeIsolated。与 MainActor.run 不同,它只能在同步上下文中运行,并且如果当前上下文不是 MainActor,应用会直接崩溃。
初次接触这个方法时,我对它的用途感到困惑。很长一段时间里,我只是将它与 MainActor.assertIsolated 一样,作为调试时判断当前上下文是否为 MainActor 的手段。直到遇到本文提到的问题,我才真正理解了这个 API 的设计意图。
查看 MainActor.assumeIsolated 的签名,我们可以发现该 API 会为其尾随闭包提供一个 MainActor 上下文。这意味着,我们可以在一个非 MainActor 的同步上下文中,无需创建异步环境,就能“同步”地运行一段只能在 MainActor 上下文中执行的代码,并返回一个 Sendable 结果。
public static func assumeIsolated<T>(_ operation: @MainActor () throws -> T, file: StaticString = #fileID, line: UInt = #line) rethrows -> T where T : Sendable解决方案
理解了 MainActor.assumeIsolated 的作用后,我们可以将 loadView 改写为:
class CustomAttachmentViewProvider: NSTextAttachmentViewProvider {
    override func loadView() {
        let view = MainActor.assumeIsolated { // 在同步上下文运行
            // assumeIsolated 闭包中提供了 MainActor 环境,可以安全地创建 UIHostingController 实例
            let hosting = UIHostingController(rootView: InlineSwiftUIButton {
                print("SwiftUI Button tapped!")
            })
            hosting.view.backgroundColor = .clear
            return hosting.view // view 为 UIView,标注为 MainActor,满足 Sendable
        }
        self.view = view
    }
}在上面的代码中:
- 我们在 loadView中顺利执行了MainActor.assumeIsolated方法
- MainActor.assumeIsolated的闭包提供了 MainActor 上下文,使我们能够安全地创建- UIHostingController实例
- hosting.view是- UIView类型(声明时已有- @MainActor标注),满足- Sendable要求,可以作为闭包的返回值
- 在 loadView的同步上下文中,我们将MainActor.assumeIsolated的返回值赋值给了self.view,保证了隔离域的一致性
考虑到 loadView 并非总是在 MainActor 中执行,最终的完整代码如下:
class CustomAttachmentViewProvider: NSTextAttachmentViewProvider {
    override func loadView() {
        view = getView()
    }
    // 如果 `loadView` 没有运行于主线程,切换到主线程
    func getView() -> UIView {
        if Thread.isMainThread {
            return Self.createHostingViewOnMain()
        } else {
            return DispatchQueue.main.asyncAndWait {
                Self.createHostingViewOnMain()
            }
        }
    }
    // 使用静态方法避免捕获 self
    private static func createHostingViewOnMain() -> UIView {
        MainActor.assumeIsolated {
            let hosting = UIHostingController(rootView: InlineSwiftUIButton {
                print("SwiftUI Button tapped!")
            })
            hosting.view.backgroundColor = .clear
            return hosting.view
        }
    }
}至此,我们实现了一个完全满足 Swift 6 编译器要求、与旧 API 相适配的解决方案。
或许不是最优雅,但存在即合理
为了在编译阶段发现并发问题,Swift 在近几年增加了大量相关的关键字和方法,这在一定程度上增加了开发者的学习成本和使用难度。尽管我对编程语言设计并无深入研究,但从直觉来看,这种大量堆砌功能的方式确实不够优雅,也增加了理解难度。
然而,考虑到 Swift 语言并非孤立存在——在实践中我们需要与大量旧 API,甚至是 Objective-C 代码打交道——这些看似“繁琐”的设计也就有了其合理性。
我仍然期待能尽早度过这段略显“混乱”的过渡期。或许再过几年,当大量官方和第三方框架都完成 Swift 6 迁移后,我们终将获得更加轻松的安全并发编程体验。