In Xcode 16, to improve SwiftUI’s performance under Swift 6 mode, Apple made several adjustments to the SwiftUI framework’s APIs to meet stricter concurrency checks. The most notable change is the comprehensive annotation of the View
protocol with @MainActor
. While these optimizations generally enhance the developer experience in Swift 6 mode, they also introduce some seemingly anomalous compile-time errors in specific scenarios. This article delves into why certain view modifiers cannot directly use @State
properties and provides corresponding solutions.
The Problem
A typical case on the Apple Developer Forums illustrates the issue: a piece of code that compiles successfully in non-Swift 6 mode triggers a compile error when switched to Swift 6 mode, provided that an @State
property is referenced within an alignmentGuide
:
struct ContentView: View {
@State var isVisible = false
var body: some View {
Text("Hello, world!")
.alignmentGuide(.bottom) {
// error: Main actor-isolated property 'isVisible' cannot be referenced from a Sendable closure
isVisible ? $0[.bottom] : $0[.top]
}
}
}
This error message is perplexing: Does Swift 6 mode prohibit the use of @State
properties within view modifiers? However, testing other view modifiers reveals that most do not produce errors. This raises a critical question: Why do only certain specific view modifiers exhibit this behavior?
Analyzing the Cause
To understand this issue, we need to consider several key points:
1. View
Protocol Annotated with @MainActor
Starting with Xcode 16, the View
protocol is entirely annotated with @MainActor
, which means that any context within it, including isVisible
, inherits the @MainActor
attribute. Examining the alignmentGuide
function’s declaration, we find that it only accepts a @Sendable
synchronous closure:
nonisolated public func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping @Sendable (ViewDimensions) -> CGFloat) -> some View
From the error message, the Swift compiler perceives isVisible
as not being a Sendable
type. This might be confusing since, according to State
’s declaration:
extension State: Sendable where Value: Sendable {}
When WrappedValue
conforms to Sendable
, State
should also conform to Sendable
. Therefore, using isVisible
(i.e., the wrappedValue
of State
) within the closure should be legitimate.
2. @State
and Property Wrappers Are More Complex
However, the reality is more nuanced. @State
and other property wrappers aren’t simple stored properties; they are syntactic sugar. At compile time, an @State
property is transformed into the following form:
struct Demo {
@State var isVisible = false
}
// After transformation
struct Demo {
private var _isVisible = State(wrappedValue: isVisible)
var isVisible: Bool {
get { _isVisible.wrappedValue }
set { _isVisible.wrappedValue = newValue }
}
}
This means that accessing isVisible
within a closure actually invokes the getter
method of the computed property:
isVisible ? $0[.bottom] : $0[.top]
// Corresponds to
isVisible.getter() ? $0[.bottom] : $0[.top]
Since this getter
method is not @Sendable
, the Swift 6 compiler raises an error: “Main actor-isolated property ‘isVisible’ cannot be referenced from a Sendable closure.”
3. Verifying with Swift Intermediate Language (SIL)
We can verify this analysis by inspecting the SIL (Swift Intermediate Language) file:
@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)
}
The SIL code clearly shows that the compiler creates a _isVisible: State<Bool>
annotated with @_hasStorage
, while isVisible
is merely a computed property for accessing this storage property.
Further inspecting the alignmentGuide
’s SIL code reveals that reading isVisible
indeed involves calling a @MainActor
-annotated getter
method:
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
From this, we can conclude:
Because in Swift 6 mode, @Sendable
synchronous closures are not allowed to call @MainActor
methods, it is impossible to directly use the @State
property isVisible
within alignmentGuide
.
Solutions
Understanding the root cause allows us to implement the following two solutions:
Solution 1: Use the Underlying Value of State
As seen from the SIL file, _isVisible
is a stored property. Therefore, we can directly use its wrappedValue
within the alignmentGuide
closure (since Bool
conforms to Sendable
):
.alignmentGuide(.bottom) {
_isVisible.wrappedValue ? $0[.bottom] : $0[.top]
}
Solution 2: Pre-fetch the Sendable
Value Outside the Closure
While Solution 1 addresses the issue, it has two notable drawbacks:
- Using a property name with an underscore prefix affects code readability.
- This approach is difficult to generalize to other property wrappers (such as
@StateObject
,@Environment
, etc.).
Therefore, a more elegant solution is to pre-fetch the State
value outside the closure:
struct ContentView: View {
@State var isVisible = false
var body: some View {
let isVisible = isVisible // Calls the getter on MainActor
Text("Hello, world!")
.alignmentGuide(.bottom) {
isVisible ? $0[.bottom] : $0[.top]
}
}
}
Here, the newly declared isVisible
is a pure Sendable
value, safe to use within the @Sendable
closure.
Conclusion
The issue discussed in this article is not limited to alignmentGuide
but also occurs in several other view modifiers that use @Sendable
synchronous closures, including scrollTransition
, visualEffect
, and keyframeAnimator
. The solutions outlined above apply equally to these cases.
When migrating to Swift 6 mode, developers often encounter various warnings and compile errors. In facing these issues, we should not merely focus on making the code compile but also strive to understand the underlying causes of the errors. This deep understanding not only helps us write better code but also enables us to navigate this significant language version update smoothly, making more informed technical decisions.
This is the final article I published in 2024. I will be taking a break for a while and plan to resume publishing new articles after the Chinese New Year (mid-February 2025). During this time, Fatbobman’s Swift Weekly will continue to be released as usual.
Happy New Year to everyone! 🎉