Support @Observable view models#1
Merged
Merged
Conversation
A fully clean rebuild (wiped DerivedData/module cache, not just xcodebuild clean) surfaced this among several concurrency errors the prior Swift 6 migration's verification builds never caught, since Xcode's incremental build state was stale throughout that work and silently hid real failures behind cached success. These inits/statics default to EnvironmentValues.defaultEnvironment, which is @mainactor, so they need @mainactor themselves.
_AnimationModifier and its private ContentWrapper both conform to Equatable alongside ViewModifier/View, so their == witnesses need to be nonisolated to match Equatable's (non-isolated) requirement despite the enclosing MainActor-inferred type. Their stored properties (animation, value, lastValue) get nonisolated(unsafe) since Value is an unconstrained generic with no Sendable guarantee.
AnyLayoutBox is @mainactor to match Layout, but its animatableData requirement mirrors Animatable's (non-isolated) requirement, so it needs a per-requirement nonisolated override - same shape as the AnyShapeBox fix from the original migration, just missed for the Fiber layout system's equivalent type.
Marks animatableData and the opacity property it reads nonisolated, matching Animatable's non-isolated requirement despite _OpacityEffect also conforming to the MainActor-inferred ViewModifier.
Same fix as _OpacityEffect: animatableData and the angle/anchor properties it reads need nonisolated(unsafe) to match Animatable's non-isolated requirement.
Element conforms to View and Identifiable in the same declaration, so whole-type isolation inference made its id witness main-actor-isolated even though Identifiable's requirement isn't. nonisolated(unsafe) resolves it, matching the same pattern used for Text/Image's Equatable witnesses.
Same pattern as other @State properties fixed during the original migration (TaskModifier, DisclosureGroup, etc.): the property wrapper's isolation inference can't resolve a plain mutable stored property without an explicit nonisolated(unsafe).
Same @State/@binding property pattern as ButtonBase's isPressed - nonisolated(unsafe) needed since SelectionValue is an unconstrained generic with no Sendable guarantee.
Same @binding property pattern as Picker's selection.
Image conforms to _PrimitiveView and Equatable directly in its primary declaration, the same shape that required marking Text nonisolated during the original migration - whole-type isolation inference was making its == witness main-actor-isolated even though Equatable's requirement isn't. Marks Image nonisolated at the struct level and its @State _intrinsicSize nonisolated(unsafe).
Same @State property pattern as ButtonBase's isPressed.
NavigationContext is a plain ObservableObject class, not Sendable, so the @StateObject property wrapper's isolation inference can't resolve it without an explicit nonisolated(unsafe).
ToolbarItem conditionally conforms to Identifiable via its plain id stored property; nonisolated(unsafe) is needed since ID is an unconstrained generic with no Sendable guarantee.
@Observable/Observation requires iOS 17+. No @available gating is introduced for it; the whole package now targets iOS 17 as its floor.
Wraps the call that evaluates a View's real body (the bodyClosure(view) branch of render(compositeView:), which invokes the user's View.body computed property) in withObservationTracking. Any @observable property read synchronously during that evaluation gets tracked; mutating it later re-triggers the exact same re-render path @State and ObservableObject already use - onChange calls the existing queueUpdate(for:transaction:), so it shares the same Set<Rerender> dedup and scheduler hop, no parallel scheduling system. render(compositeView:) already re-runs on every mount and re-render, so a fresh withObservationTracking call is naturally re-established each time - no manual re-registration loop needed, matching the same "tear down and re-subscribe every render" shape the existing Combine ObservedProperty path already uses for @ObservedObject/@StateObject/ @EnvironmentObject. withObservationTracking's onChange is a plain non-isolated @sendable closure, but StackReconciler and MountedCompositeElement are MainActor-isolated and not Sendable, so they can't be captured directly in a @sendable closure even weakly. WeakBox<T: AnyObject>: @unchecked Sendable holds the weak references across that boundary; onChange hops back onto the main actor via Task { @mainactor in } to dereference them before calling queueUpdate. No changes needed to State (it's unconstrained on Value, so it already preserves an @observable class instance's identity across re-renders via its existing storage/getter mechanism) or to the DynamicProperty reflection layer (@observable view models are plain stored properties on classes, not property-wrapper structs, so they're never discovered via - or need to be discovered via - the reflection-based DynamicProperty walk).
Adds InformationViewModel (@observable) alongside the existing @State counter, with its own increment button and detail row, as a side-by-side comparison verifying the new auto-tracking works without disturbing the pre-existing @State/@observableobject re-render path.
…e Swift compiler
…e Swift compiler
…e Swift compiler
…e Swift compiler
…e Swift compiler
…ble Swift compiler
…ble Swift compiler
…e Swift compiler
… stable Swift compiler
…stale item management
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@Observableproperties read duringView.bodyby wrapping body evaluation inwithObservationTracking, routing changes through the existingqueueUpdatere-render path (same mechanism@State/ObservableObjectalready use, no parallel scheduling system)@Observableview model toCarInformationViewfor side-by-side comparison against the existing@StatecounterTest plan
xcodebuild clean+ wipe DerivedData) shows only the pre-existing, unrelatedCGSize/Animatableconformance-ambiguity baseline errors (confirmed identical onmasterbefore this branch)@Statecounter/"Increment" button still works unaffectedNavigationLink, confirm the new screen's observable counter starts at 0 while the previous screen's value is preserved on popping back