onPreferenceChange closure is now nonIsolated?

Running up Xcode 16.2 Beta 1, a lot of my code that used onPreferenceChange in Views to change @State properties of those views, such as some notion of a measured width is now complaining about mutating the @MainActor-isolated properties from Sendable closures.

Now I've got to hoop-jump to change @State properties from onPreferenceChange? OK, but seems a bit of extra churn.

How are you getting around this? My spontaneous thought was to use a Task to dispatch to the MainActor, but that seems to introduce visual glitches that weren't there previously.

The level of change in this migration period is insane. The preference is isolated on main actor and managed by view layer, why the closure is not MainActor?

This feels like a very awkward change. I'm not sure what the appropriate resolution for this is, but I guess we could assume it's isolated?

.onPreferenceChange(MyPreference.self) { value in
  MainActor.assumeIsolated {
    myValue = value
  }
}

Of course this will crash at runtime if that assumption is incorrect.

This feels particularly bizarre now that all View conformances are automatically @MainActor now.

Same boat. This change breaks our SDK and just seems odd. Now that Xcode 16.2 has released, using a Task for this seems inappropriate so I'd +1 the MainActor.assumeIsolated but honestly what nonsense and it's frustrating to not know if this change is purposeful. Why are no devs commenting on this; incredibly frustrating.

I like Swift Concurrency but I reckon the devs might be at the point of 'can we make SwiftUI truly concurrent for performance gains' and finding that realistically the answer is no so they need to @MainActor it all. I'm at the same pain point and the biggest and obvious issue is nonisolated contexts that need to access isolated contexts need to be made async so really I need to either @MainActor or async it all.

Just separate your struct from the View conformance and do the conformance in extension

You could capture the state/binding and mutate the wrapped value, e.g.

.onPreferenceChange(SizeKey.self) { [$size] size in
  $size.wrappedValue = size
}

Btw the thread title is misleading, onPreferenceChange was always nonisolated, what changed is the action closure becoming @Sendable.

It seems the swift team is tightening the screws on concurrency, but ...

The error is: Main actor-isolated property 'buttonMaxWidth' can not be mutated from a Sendable closure

Where:

@State private var buttonMaxWidth: CGFloat = 120

Now looking under the hood:

    @preconcurrency @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

but, I wonder why they did not mark this func as @MainActor isolated? I suspect the 2m lines behind that would have to be refactored.

for now my solution 1 is to assume

.onPreferenceChange(ButtonWidthPreferenceKey.self) { newSize in
    MainActor.assumeIsolated {
        buttonMaxWidth = newSize
    }
}

What's the point of all these rules if we can say ignore the rules in one line ?

For solution 2, you can be more pedantic and declare a MainActor isolated func that you can than open a Task that calls it, too much work ...

.onPreferenceChange(ButtonWidthPreferenceKey.self) { newSize in
    Task { @MainActor in
        setButtonMaxWidth(newSize)
    }
}

There is a response here "from an Apple engineer" that works for me:

.onPreferenceChange(HeightKey.self) { [$height] newValue in
  $height.wrappedValue = newValue
}
onPreferenceChange closure is now nonIsolated?
 
 
Q