smr
smr

Reputation: 1243

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?

Upvotes: 9

Views: 994

Answers (3)

Kai Huppmann
Kai Huppmann

Reputation: 10775

We went with:

.onPreferenceChange(HeightKey.self) { height in
    Task { @MainActor in
        self.height = height
    }
 }

Upvotes: 0

smr
smr

Reputation: 1243

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.

Upvotes: 15

Aloisius
Aloisius

Reputation: 79

You can work around it by wrapping everything in MainActor.assumeIsolated:

.onPreferenceChange(HeightKey.self) { height in
    MainActor.assumeIsolated {
        self.height = height
    }
 }

This should be fine as long as it's being called from the main thread.

Upvotes: 4

Related Questions