在 Xcode 16 中,为了改善 SwiftUI 在 Swift 6 模式下的表现,苹果对 SwiftUI 框架的 API 进行了多项调整,以满足更严格的并发检查要求。其中最显著的变化是将 View
协议全面标注为 @MainActor
。这些优化虽然总体上改善了开发者在 Swift 6 模式下的编程体验,但也在某些特定场景中引发了一些看似反常的编译错误。本文将详细探讨为什么某些视图修饰器中无法直接使用 @State
属性值,并提供相应的解决方案。
问题
在苹果开发者论坛上有一个典型案例:一段在非 Swift 6 模式下可以正常编译的代码,当切换到 Swift 6 模式后,只要 alignmentGuide
中引用了 @State
属性,就会触发编译错误:
struct ContentView: View {
@State var isVisible = false
var body: some View {
Text("Hello, world!")
.alignmentGuide(.bottom) {
// error: Main actor-isolated property 'isVisible' can not be referenced from a Sendable closure
isVisible ? $0[.bottom] : $0[.top]
}
}
}
这个错误提示令人困惑:难道 Swift 6 模式下无法在视图修饰器中使用 @State
属性了?然而当我们在其他视图修饰器中测试时,发现大多数情况下并不会报错。这就引出了一个关键问题:为什么只有某些特定的视图修饰器会出现这种情况?
分析原因
要理解这个问题,我们需要从几个关键点入手:
首先,从 Xcode 16 开始,View
协议被整体标注为 @MainActor
,这使得上下文中的 isVisible
也继承了 @MainActor
属性。通过查看 alignmentGuide
的函数声明,我们可以看到它只接受一个 @Sendable
的同步闭包:
nonisolated public func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping @Sendable (ViewDimensions) -> CGFloat) -> some View
从错误提示来看,Swift 编译器认为 isVisible
不是一个 Sendable
类型。这可能会让人感到困惑,因为根据 State
的声明:
extension State : Sendable where Value : Sendable {}
当 WrappedValue
符合 Sendable
时,State
也应该符合 Sendable
。那么在闭包中使用 isVisible
(即 State
的 wrappedValue
值)应该是合法的才对。
然而实际情况更为复杂。@State
和其他属性包装器并不是简单的存储属性,而是一种语法糖。在编译时,一个 @State
属性会被转译成如下形式:
struct Demo {
@State var isVisible = false
}
// 转译后
struct Demo {
private var _isVisible = State(wrappedValue: isVisible)
var isVisible: Bool {
get { _isVisible.wrappedValue }
set { _isVisible.wrappedValue = newValue }
}
}
这意味着在闭包中访问 isVisible
时,实际上是在调用 isVisible
计算属性的 getter
方法:
isVisible ? $0[.bottom] : $0[.top]
// 对应
isVisible.getter() ? $0[.bottom] : $0[.top]
由于这个 getter 方法并非 @Sendable
,因此 Swift 6 编译器会报错:“Main actor-isolated property ‘isVisible’ can not be referenced from a Sendable closure”。
我们可以通过查看 SIL(Swift Intermediate Language)文件来验证这一分析。
@MainActor @preconcurrency struct ContentView : View {
@State @_projectedValueProperty($isVisible) @MainActor @preconcurrency var isVisible: Bool {
get
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
nonmutating set
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
nonmutating _modify
}
@MainActor @preconcurrency var $isVisible: Binding<Bool> { get }
@_hasStorage @MainActor @preconcurrency @_hasInitialValue var _isVisible: State<Bool> { get set }
@MainActor @preconcurrency var body: some View { get }
typealias Body = @_opaqueReturnTypeOf("$s11ContentViewAAV4bodyQrvp", 0) __
@MainActor @preconcurrency init()
nonisolated init(isVisible: Bool = false)
}
SIL 代码清楚地显示了编译器创建了一个标注为 @_hasStorage
的 _isVisible: State<Bool>
,而 isVisible
仅仅是一个用于读写这个存储属性的计算属性。
通过进一步检查 alignmentGuide
的 SIL 代码,我们可以看到读取 isVisible
的值确实是通过调用一个被标注为 @MainActor
的 getter
方法实现的( %4
和 %5
清晰的展示了调用方法的过程 )。
bb0(%0 : $*ViewDimensions, %1 : @closureCapture $ContentView):
debug_value %0 : $*ViewDimensions, let, name "$0", argno 1, expr op_deref // id: %2
debug_value %1 : $ContentView, let, name "self", argno 2 // id: %3
// function_ref ContentView.isVisible.getter
%4 = function_ref @$s11ContentViewAAV3topSbvg : $@convention(method) (@guaranteed ContentView) -> Bool // user: %5
%5 = apply %4(%1) : $@convention(method) (@guaranteed ContentView) -> Bool // user: %6
%6 = struct_extract %5 : $Bool, #Bool._value // user: %7
cond_br %6, bb1, bb2 // id: %7
这样我们就可以得出结论:由于在 Swift 6 模式下,@Sendable
的同步闭包中不允许调用 @MainActor
方法,因此我们无法在 alignmentGuide
中直接使用 @State
属性 isVisible
。
解决方案
理解了问题的本质后,我们可以采用以下两种方案来解决:
方案一:使用 State 的底层值
从 SIL 文件可以看出,_isVisible
是一个存储属性。因此,我们可以在 alignmentGuide
的闭包中直接使用其 wrappedValue
(因为 Bool
符合 Sendable
协议):
.alignmentGuide(.bottom) {
_isVisible.wrappedValue ? $0[.bottom] : $0[.top]
}
方案二:闭包外预先获取 Sendable 值
虽然方案一能够解决问题,但它存在两个明显的缺点:
- 使用下划线前缀的属性名影响代码可读性
- 这种方式难以推广到其他属性包装器(如 @StateObject、@Environment 等)
因此,一个更优雅的解决方案是在闭包外预先获取 State
的值:
struct ContentView: View {
@State var isVisible = false
var body: some View {
let isVisible = isVisible // 在 MainActor 上调用 getter
Text("Hello, world!")
.alignmentGuide(.bottom) {
isVisible ? $0[.bottom] : $0[.top]
}
}
}
这样,新声明的 isVisible
就是一个纯粹的 Sendable
值,可以安全地在 @Sendable
闭包中使用。
总结
本文讨论的问题不仅存在于 alignmentGuide
,还出现在其他几个使用 @Sendable
同步闭包的视图修饰器中,包括:scrollTransition
、visualEffect
和 keyframeAnimator
。上述解决方案同样适用于这些情况。
在向 Swift 6 模式迁移的过程中,开发者经常会遇到各种警告和编译错误。面对这些问题时,我们不应该仅仅关注如何让代码通过编译,而是应该深入理解错误产生的根本原因。这种深入理解不仅能帮助我们写出更好的代码,也能让我们在这次重要的语言版本更新中游刃有余,作出更明智的技术决策。
本文是我在 2024 年发表的最后一篇文章。接下来,我将休息一段时间,计划于春节过后( 2025 年 2 月中旬)恢复发布新文章。在此期间,周报仍将正常发布。
祝大家新年快乐!🎉