尽管 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.sync {
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 迁移后,我们终将获得更加轻松的安全并发编程体验。
如果这篇文章对你有帮助,可以 买杯咖啡 ☕️ 支持一下.
"加入我们的 Discord 社区,与超过 2000 名苹果生态的中文开发者一起交流!"