swiftuiswift-concurrency

Xcode 16.2 beta 2 renders `onPreferenceChange` unusable


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?


Solution

  • 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.