The view below works normally in Xcode 16.1. Build in Xcode 16.2 beta 2 and you get the following error on the closure for onPreferenceChange
: "Main actor-isolated property 'height' can not be mutated from a Sendable closure". (Swift 6 language mode).
struct HeightKey: PreferenceKey {
static var defaultValue: CGFloat { .zero }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentView: View {
@State private var height: CGFloat = 0
var body: some View {
VStack {
Text("Title")
background(
GeometryReader { proxy in
Color.clear
.preference(key: HeightKey.self, value: proxy.size.height)
}
)
.onPreferenceChange(HeightKey.self) { height in
self.height = height
}
Text("Height: \(height)")
}
}
}
Look at the signature for onPreferenceChange
in Xcode 16.1:
@inlinable nonisolated public func onPreferenceChange<K>(_ key: K.Type = K.self, perform action: @escaping (K.Value) -> Void) -> some View where K : PreferenceKey, K.Value : Equatable
And now in Xcode 16.2 beta 2:
@inlinable nonisolated public func onPreferenceChange<K>(_ key: K.Type = K.self, perform action: @escaping @Sendable (K.Value) -> Void) -> some View where K : PreferenceKey, K.Value : Equatable
The difference is the addition of @Sendable
on the closure. Unfortunately they didn't also make it @MainActor
so you can't update the containing View
's properties. Just for fun I wrapped the assignment in a Task
, but this causes a crash ("EXC_BAD_ACCESS code=2"):
.onPreferenceChange(HeightKey.self) { height in
Task { @MainActor in
self.height = height
}
}
How can we use onPreferenceChange
without going back to Swift 5 mode?
Got a response from an Apple engineer on how to correctly handle this:
.onPreferenceChange(HeightKey.self) { [$height] height in
$height.wrappedValue = height
}
I feel a little sheepish for not realizing this earlier.
The approach that avoids preferences by @Benzy Neez is also a good one.