--- name: axiom-swiftui-animation-ref description: Use when implementing SwiftUI animations, understanding VectorArithmetic, using @Animatable macro, zoom transitions, UIKit/AppKit animation bridging, choosing between spring and timing curve animations, or debugging animation behavior - comprehensive animation reference from iOS 13 through iOS 26 license: MIT metadata: version: "1.1.0" --- # SwiftUI Animation ## Overview Comprehensive guide to SwiftUI's animation system, from foundational concepts to advanced techniques. This skill covers the Animatable protocol, the iOS 26 @Animatable macro, animation types, and the Transaction system. **Core principle** Animation in SwiftUI is mathematical interpolation over time, powered by the VectorArithmetic protocol. Understanding this foundation unlocks the full power of SwiftUI's declarative animation system. ## System Requirements - iOS 13+: Animatable protocol, timing/spring animations - iOS 17+: Default spring animations, scoped animations, PhaseAnimator, KeyframeAnimator - iOS 18+: Zoom transitions, UIKit/AppKit animation bridging - iOS 26+: @Animatable macro --- ## Part 1: Understanding Animation ### What Is Interpolation Animation is the process of generating intermediate values between a start and end state. #### Example: Opacity animation ```swift .opacity(0) → .opacity(1) ``` While this animation runs, SwiftUI computes intermediate values: ``` 0.0 → 0.02 → 0.05 → 0.1 → 0.25 → 0.4 → 0.6 → 0.8 → 1.0 ``` **How values are distributed** - Determined by the animation's timing curve or velocity function - Spring animations use physics simulation - Timing curves use bezier curves - Each animation type calculates values differently ### VectorArithmetic Protocol SwiftUI requires animated data to conform to `VectorArithmetic` — providing subtraction, scaling, addition, and a zero value. This enables SwiftUI to interpolate between any two values. **Built-in conforming types**: `CGFloat`, `Double`, `Float`, `Angle` (1D), `CGPoint`, `CGSize` (2D), `CGRect` (4D). **Key insight** Vector arithmetic abstracts over dimensionality. SwiftUI animates all these types with a single generic implementation. ### Why Int Can't Be Animated `Int` doesn't conform to VectorArithmetic — no fractional intermediates exist between 3 and 4. SwiftUI simply snaps the value. **Solution**: Use `Float`/`Double` and display as `Int`: ```swift @State private var count: Float = 0 // ... Text("\(Int(count))") .animation(.spring, value: count) ``` ### Model vs Presentation Values Animatable attributes conceptually have two values: #### Model Value - The target value set by your code - Updated immediately when state changes - What you write in your view's body #### Presentation Value - The current interpolated value being rendered - Updates frame-by-frame during animation - What the user actually sees **Example** ```swift .scaleEffect(selected ? 1.5 : 1.0) ``` When `selected` becomes `true`: - **Model value**: Immediately becomes `1.5` - **Presentation value**: Interpolates `1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 1.5` over time --- ## Part 2: Animatable Protocol ### Overview The `Animatable` protocol allows views to animate their properties by defining which data should be interpolated. ```swift protocol Animatable { associatedtype AnimatableData: VectorArithmetic var animatableData: AnimatableData { get set } } ``` SwiftUI builds an animatable attribute for any view conforming to this protocol. ### Built-in Animatable Views Many SwiftUI modifiers conform to Animatable: #### Visual Effects - `.scaleEffect()` — Animates scale transform - `.rotationEffect()` — Animates rotation - `.offset()` — Animates position offset - `.opacity()` — Animates transparency - `.blur()` — Animates blur radius - `.shadow()` — Animates shadow properties #### All Shape types - `Circle`, `Rectangle`, `RoundedRectangle` - `Capsule`, `Ellipse`, `Path` - Custom `Shape` implementations ### AnimatablePair for Multi-Dimensional Data When animating multiple properties, use `AnimatablePair` to combine vectors. For example, `scaleEffect` combines `CGSize` (2D) and `UnitPoint` (2D) into a 4D vector via `AnimatablePair`. Access components via `.first` and `.second`. The `@Animatable` macro (iOS 26+) eliminates this boilerplate entirely. ### Custom Animatable Conformance #### When to use - Animating custom layout (like RadialLayout) - Animating custom drawing code - Animating properties that affect shape paths #### Example: Animated number view ```swift struct AnimatableNumberView: View, Animatable { var number: Double var animatableData: Double { get { number } set { number = newValue } } var body: some View { Text("\(Int(number))") .font(.largeTitle) } } // Usage AnimatableNumberView(number: value) .animation(.spring, value: value) ``` **How it works** 1. `number` changes from 0 to 100 2. SwiftUI calls `body` for every frame of the animation 3. Each frame gets a new `number` value: 0 → 5 → 15 → 30 → 55 → 80 → 100 4. Text updates to show the interpolated integer ### Performance Warning **Custom Animatable conformance is expensive** — SwiftUI calls `body` for every frame on the main thread. Built-in effects (`.scaleEffect()`, `.opacity()`) run off-main-thread and don't call `body`. Use custom conformance only when built-in modifiers can't achieve the effect (e.g., animating a custom `Layout` that repositions subviews per-frame). --- ## Part 3: @Animatable Macro (iOS 26+) ### Overview The `@Animatable` macro eliminates the boilerplate of manually conforming to the Animatable protocol. **Before iOS 26**, you had to: 1. Manually conform to `Animatable` 2. Write `animatableData` getter and setter 3. Use `AnimatablePair` for multiple properties 4. Exclude non-animatable properties manually **iOS 26+**, you just add `@Animatable`: ```swift @MainActor @Animatable struct MyView: View { var scale: CGFloat var opacity: Double var body: some View { // ... } } ``` The macro automatically: - Generates `Animatable` conformance - Inspects all stored properties - Creates `animatableData` from VectorArithmetic-conforming properties - Handles multi-dimensional data with `AnimatablePair` ### Before/After Comparison #### Before @Animatable macro ```swift struct HikingRouteShape: Shape { var startPoint: CGPoint var endPoint: CGPoint var elevation: Double var drawingDirection: Bool // Don't want to animate this // Tedious manual animatableData declaration var animatableData: AnimatablePair, AnimatablePair>> { get { AnimatablePair( AnimatablePair(startPoint.x, startPoint.y), AnimatablePair(elevation, AnimatablePair(endPoint.x, endPoint.y)) ) } set { startPoint = CGPoint(x: newValue.first.first, y: newValue.first.second) elevation = newValue.second.first endPoint = CGPoint(x: newValue.second.second.first, y: newValue.second.second.second) } } func path(in rect: CGRect) -> Path { // Drawing code } } ``` #### After @Animatable macro ```swift @Animatable struct HikingRouteShape: Shape { var startPoint: CGPoint var endPoint: CGPoint var elevation: Double @AnimatableIgnored var drawingDirection: Bool // Excluded from animation func path(in rect: CGRect) -> Path { // Drawing code } } ``` **Lines of code**: 20 → 12 (40% reduction) ### @AnimatableIgnored Use `@AnimatableIgnored` to exclude properties from animation. #### When to use - **Debug values** — Flags for development only - **IDs** — Identifiers that shouldn't animate - **Timestamps** — When the view was created/updated - **Internal state** — Non-visual bookkeeping - **Non-VectorArithmetic types** — Colors, strings, booleans #### Example ```swift @MainActor @Animatable struct ProgressView: View { var progress: Double // Animated var totalItems: Int // Animated (if Float, not if Int) @AnimatableIgnored var title: String // Not animated @AnimatableIgnored var startTime: Date // Not animated @AnimatableIgnored var debugEnabled: Bool // Not animated var body: some View { VStack { Text(title) ProgressBar(value: progress) if debugEnabled { Text("Started: \(startTime.formatted())") } } } } ``` ### Real-World Use Case @Animatable works for any numeric display — stock prices, heart rate, scores, timers, progress bars: ```swift @MainActor @Animatable struct AnimatedValueView: View { var value: Double var changePercent: Double @AnimatableIgnored var label: String var body: some View { VStack(alignment: .trailing) { Text("\(value, format: .number.precision(.fractionLength(2)))") .font(.title) Text("\(changePercent > 0 ? "+" : "")\(changePercent, format: .percent)") .foregroundStyle(changePercent > 0 ? .green : .red) } } } // Usage AnimatedValueView(value: currentPrice, changePercent: 0.025, label: "Price") .animation(.spring(duration: 0.8), value: currentPrice) ``` --- ## Part 4: Animation Types ### Timing Curve Animations Timing curve animations use bezier curves to control the speed of animation over time. #### Built-in presets ```swift .animation(.linear) // Constant speed .animation(.easeIn) // Starts slow, ends fast .animation(.easeOut) // Starts fast, ends slow .animation(.easeInOut) // Slow start and end, fast middle ``` #### Custom timing curves ```swift let customCurve = UnitCurve( startControlPoint: CGPoint(x: 0.2, y: 0), endControlPoint: CGPoint(x: 0.8, y: 1) ) .animation(.timingCurve(customCurve, duration: 0.5)) ``` #### Duration All timing curve animations accept an optional duration: ```swift .animation(.easeInOut(duration: 0.3)) .animation(.linear(duration: 1.0)) ``` **Default**: 0.35 seconds ### Spring Animations Spring animations use physics simulation to create natural, organic motion. #### Built-in presets ```swift .animation(.smooth) // No bounce (default since iOS 17) .animation(.snappy) // Small amount of bounce .animation(.bouncy) // Larger amount of bounce ``` #### Custom springs ```swift .animation(.spring(duration: 0.6, bounce: 0.3)) ``` **Parameters** - `duration` — Perceived animation duration - `bounce` — Amount of bounce (0 = no bounce, 1 = very bouncy) **Much more intuitive** than traditional spring parameters (mass, stiffness, damping). ### Higher-Order Animations Modify base animations to create complex effects. #### Delay ```swift .animation(.spring.delay(0.5)) ``` Waits 0.5 seconds before starting the animation. #### Repeat ```swift .animation(.easeInOut.repeatCount(3, autoreverses: true)) .animation(.linear.repeatForever(autoreverses: false)) ``` Repeats the animation multiple times or infinitely. #### Speed ```swift .animation(.spring.speed(2.0)) // 2x faster .animation(.spring.speed(0.5)) // 2x slower ``` Multiplies the animation speed. ### Default Animation Changes (iOS 17+) **Before iOS 17** ```swift withAnimation { // Used timing curve by default } ``` **iOS 17+** ```swift withAnimation { // Uses .smooth spring by default } ``` **Why the change**: Spring animations feel more natural and preserve velocity when interrupted. **Recommendation**: Embrace springs. They make your UI feel more responsive and polished. --- ## Part 5: Transaction System ### withAnimation The most common way to trigger an animation. ```swift Button("Scale Up") { withAnimation(.spring) { scale = 1.5 } } ``` **How it works** 1. `withAnimation` opens a transaction 2. Sets the animation in the transaction dictionary 3. Executes the closure (state changes) 4. Transaction propagates down the view hierarchy 5. Animatable attributes check for animation and interpolate #### Explicit animation ```swift withAnimation(.spring(duration: 0.6, bounce: 0.4)) { isExpanded.toggle() } ``` #### No animation ```swift withAnimation(nil) { // Changes happen immediately, no animation resetState() } ``` ### animation() View Modifier Apply animations to specific values within a view. #### Basic usage ```swift Circle() .fill(isActive ? .blue : .gray) .animation(.spring, value: isActive) ``` **How it works**: Animation only applies when `isActive` changes. Other state changes won't trigger this animation. #### Multiple animations on same view ```swift Circle() .scaleEffect(scale) .animation(.bouncy, value: scale) .opacity(opacity) .animation(.easeInOut, value: opacity) ``` Different animations for different properties. ### Scoped Animations (iOS 17+) Narrowly scope animations to specific animatable attributes. #### Problem with old approach ```swift struct AvatarView: View { var selected: Bool var body: some View { Image("avatar") .scaleEffect(selected ? 1.5 : 1.0) .animation(.spring, value: selected) // ⚠️ If image also changes when selected changes, // image transition gets animated too (accidental) } } ``` #### Solution: Scoped animation ```swift struct AvatarView: View { var selected: Bool var body: some View { Image("avatar") .animation(.spring, value: selected) { $0.scaleEffect(selected ? 1.5 : 1.0) } // ✅ Only scaleEffect animates, image transition doesn't } } ``` **How it works** - Animation only applies to attributes in the closure - Other attributes are unaffected - Prevents accidental animations ### Custom Transaction Keys Define custom `TransactionKey` types to propagate context through the transaction system. Use `withTransaction` to set values and `.transaction` modifier to read them. This enables applying different animations based on how a state change was triggered (tap vs programmatic). --- ## Part 6: Advanced Topics ### CustomAnimation Protocol Implement your own animation algorithms. ```swift protocol CustomAnimation { // Calculate current value func animate( value: V, time: TimeInterval, context: inout AnimationContext ) -> V? // Optional: Should this animation merge with previous? func shouldMerge(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext) -> Bool // Optional: Current velocity func velocity( value: V, time: TimeInterval, context: AnimationContext ) -> V? } ``` #### Example: Linear timing curve ```swift struct LinearAnimation: CustomAnimation { let duration: TimeInterval func animate( value: V, // Delta vector: target - current time: TimeInterval, context: inout AnimationContext ) -> V? { if time >= duration { return nil } return value.scaled(by: time / duration) } } ``` **Critical understanding**: `value` is the **delta vector** (target - current), not the target. Return `nil` when done. SwiftUI adds the scaled delta to the current value automatically. ### Animation Merging Behavior What happens when a new animation starts before the previous one finishes? #### Timing curve animations (default: don't merge) ```swift func shouldMerge(...) -> Bool { return false // Default implementation } ``` **Behavior**: Both animations run together, results are combined additively. **Example** - First tap: animate 1.0 → 1.5 (running) - Second tap (before finish): animate 1.5 → 1.0 - Result: Both animations run, values combine #### Spring animations (merge and retarget) ```swift func shouldMerge(...) -> Bool { return true // Springs override this } ``` **Behavior**: New animation incorporates state of previous animation, preserving velocity. **Example** - First tap: animate 1.0 → 1.5 with velocity V - Second tap (before finish): retarget to 1.0, preserving current velocity V - Result: Smooth transition, no sudden velocity change **Why springs feel more natural**: They preserve momentum when interrupted. --- ## Part 7: Multi-Step Animations (iOS 17+) ### PhaseAnimator Cycles through a sequence of phases, applying different modifiers at each phase. Each phase transition is independently animated. ```swift PhaseAnimator([false, true]) { phase in Image(systemName: "star.fill") .scaleEffect(phase ? 1.5 : 1.0) .opacity(phase ? 1.0 : 0.5) .rotationEffect(.degrees(phase ? 360 : 0)) } animation: { phase in phase ? .spring(duration: 0.8, bounce: 0.3) : .easeInOut(duration: 0.4) } ``` **How it works**: Begins at first phase, animates to second, then loops. The `animation` closure returns the animation used to transition INTO that phase. Phases can be any `Equatable` type — use an enum for complex multi-step sequences: ```swift enum PulsePhase: CaseIterable { case idle, expand, contract } PhaseAnimator(PulsePhase.allCases) { phase in Circle() .scaleEffect(phase == .expand ? 1.3 : phase == .contract ? 0.9 : 1.0) } ``` **Trigger**: Add a `trigger` parameter to run the animation only when a value changes (instead of looping continuously). ### KeyframeAnimator Provides per-property keyframe tracks for precise, timeline-based animations. More control than PhaseAnimator. ```swift struct AnimationValues { var scale: Double = 1.0 var rotation: Angle = .zero var yOffset: Double = 0 } KeyframeAnimator(initialValue: AnimationValues()) { values in Image(systemName: "heart.fill") .scaleEffect(values.scale) .rotationEffect(values.rotation) .offset(y: values.yOffset) } keyframes: { _ in KeyframeTrack(\.scale) { SpringKeyframe(1.5, duration: 0.3) SpringKeyframe(1.0, duration: 0.3) } KeyframeTrack(\.rotation) { LinearKeyframe(.degrees(15), duration: 0.15) LinearKeyframe(.degrees(-15), duration: 0.3) LinearKeyframe(.zero, duration: 0.15) } KeyframeTrack(\.yOffset) { CubicKeyframe(-20, duration: 0.3) CubicKeyframe(0, duration: 0.3) } } ``` **Keyframe types**: `LinearKeyframe` (constant velocity), `SpringKeyframe` (spring physics), `CubicKeyframe` (bezier curves), `MoveKeyframe` (instant jump, no interpolation). **vs PhaseAnimator**: Use PhaseAnimator for simple state cycling. Use KeyframeAnimator when different properties need independent timing. ### .transition() Defines how a view animates when inserted/removed from the view hierarchy. ```swift if showDetail { DetailView() .transition(.slide) // Slide in/out .transition(.scale.combined(with: .opacity)) // Combine transitions .transition(.move(edge: .bottom)) // Move from edge .transition(.asymmetric( // Different in/out insertion: .scale.combined(with: .opacity), removal: .opacity )) } ``` **Requires animation context** — wrap the state change in `withAnimation` or use `.animation()` modifier. Without animation, the view appears/disappears instantly. ### matchedGeometryEffect Smoothly animate a view's frame between two positions in the hierarchy. Commonly used for hero transitions and shared element animations. ```swift @Namespace private var animation // Source if !isExpanded { RoundedRectangle(cornerRadius: 10) .matchedGeometryEffect(id: "card", in: animation) .frame(width: 100, height: 100) } // Destination if isExpanded { RoundedRectangle(cornerRadius: 20) .matchedGeometryEffect(id: "card", in: animation) .frame(width: 300, height: 400) } ``` **Key rules**: Same `id` + same `Namespace` = matched pair. Only one view with a given ID should be `isSource: true` (default) at a time. Wrap state change in `withAnimation` for smooth interpolation. ### contentTransition Animates changes to text and symbol content within a view (iOS 16+). ```swift Text(value, format: .number) .contentTransition(.numericText(countsDown: value < previous)) Image(systemName: isFavorite ? "heart.fill" : "heart") .contentTransition(.symbolEffect(.replace)) ``` --- ## Part 8: Zoom Transitions (iOS 18+) ### Overview iOS 18 introduces the zoom transition, where a tapped cell morphs into the incoming view. This transition is continuously interactive—users can grab and drag the view during or after the transition begins. **Key benefit** In parts of your app where you transition from a large cell, zoom transitions increase visual continuity by keeping the same UI elements on screen across the transition. ### SwiftUI Implementation Two steps to adopt zoom transitions: #### Step 1: Declare the transition style on the destination ```swift NavigationLink { BraceletEditor(bracelet) .navigationTransition(.zoom(sourceID: bracelet.id, in: namespace)) } label: { BraceletPreview(bracelet) } ``` #### Step 2: Mark the source view ```swift BraceletPreview(bracelet) .matchedTransitionSource(id: bracelet.id, in: namespace) ``` #### Complete example ```swift struct BraceletListView: View { @Namespace private var braceletList let bracelets: [Bracelet] var body: some View { NavigationStack { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) { ForEach(bracelets) { bracelet in NavigationLink { BraceletEditor(bracelet: bracelet) .navigationTransition( .zoom(sourceID: bracelet.id, in: braceletList) ) } label: { BraceletPreview(bracelet: bracelet) } .matchedTransitionSource(id: bracelet.id, in: braceletList) } } } } } } ``` ### UIKit Implementation Set `preferredTransition = .zoom { context in ... }` on the pushed view controller. The closure returns the source view and is called on both zoom in and zoom out — capture a stable identifier (model object), not a view directly. ### Presentations Zoom transitions also work with `fullScreenCover` and `sheet`: ```swift .fullScreenCover(item: $selectedBracelet) { bracelet in BraceletEditor(bracelet: bracelet) .navigationTransition(.zoom(sourceID: bracelet.id, in: namespace)) } ``` ### Styling the Source View ```swift .matchedTransitionSource(id: bracelet.id, in: namespace) { source in source.cornerRadius(8.0).shadow(radius: 4) } ``` ### Fluid Transition Lifecycle Push transitions cannot be cancelled — when interrupted, they convert to pop transitions. The view controller always reaches the Appeared state. Don't guard against overlapping transitions; let the system handle them. --- ## Part 9: UIKit/AppKit Animation Bridging (iOS 18+) ### Overview iOS 18 enables using SwiftUI `Animation` types to animate UIKit and AppKit views. This provides access to the full suite of SwiftUI animations, including custom animations. ### Basic Usage ```swift // Old way UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) { bead.center = endOfBracelet } // New way: Use SwiftUI Animation type UIView.animate(.spring(duration: 0.5)) { bead.center = endOfBracelet } ``` All SwiftUI animations work: `.linear`, `.easeIn/Out`, `.spring`, `.smooth`, `.snappy`, `.bouncy`, `.repeatForever()`, and custom animations. **Architecture note**: Unlike old UIKit APIs, no `CAAnimation` is generated — presentation values are animated directly. --- ## Part 10: UIViewRepresentable Animation Bridging (iOS 18+) ### The Problem When wrapping UIKit views in SwiftUI, animations don't automatically bridge: ```swift struct BeadBoxWrapper: UIViewRepresentable { @Binding var isOpen: Bool func updateUIView(_ box: BeadBox, context: Context) { // ❌ Animation on binding doesn't affect UIKit box.lid.center.y = isOpen ? -100 : 100 } } // Usage BeadBoxWrapper(isOpen: $isOpen) .animation(.spring, value: isOpen) // No effect on UIKit view ``` ### The Solution: context.animate() Use `context.animate()` to bridge SwiftUI animations: ```swift struct BeadBoxWrapper: UIViewRepresentable { @Binding var isOpen: Bool func makeUIView(context: Context) -> BeadBox { BeadBox() } func updateUIView(_ box: BeadBox, context: Context) { // ✅ Bridges animation from Transaction to UIKit context.animate { box.lid.center.y = isOpen ? -100 : 100 } } } ``` ### How It Works 1. SwiftUI stores animation info in the current `Transaction` 2. `context.animate()` reads the Transaction's animation 3. Applies that animation to UIView changes in the closure 4. If no animation in Transaction, changes happen immediately (no animation) ### Key Behavior ```swift context.animate { // Changes here } completion: { // Called when animation completes // If not animated, called immediately inline } ``` **Works whether animated or not** — safe to always use this pattern. ### Perfect Synchronization A single animation running across SwiftUI Views and UIViews runs **perfectly in sync**. This enables seamless mixed hierarchies. --- ## Part 11: Gesture-Driven Animations (iOS 18+) ### Automatic Velocity Preservation SwiftUI animations automatically preserve velocity through animation merging — no manual velocity calculation needed: ```swift // UIKit with SwiftUI animations func handlePan(_ gesture: UIPanGestureRecognizer) { switch gesture.state { case .changed: UIView.animate(.interactiveSpring) { bead.center = gesture.location(in: view) } case .ended: UIView.animate(.spring) { // Inherits velocity automatically bead.center = endOfBracelet } default: break } } // Pure SwiftUI equivalent DragGesture() .onChanged { value in withAnimation(.interactiveSpring) { position = value.location } } .onEnded { _ in withAnimation(.spring) { position = targetPosition } } ``` Each `.interactiveSpring` retargets the previous animation, and the final `.spring` inherits the accumulated velocity for smooth deceleration. --- --- ## Troubleshooting ### Property Not Animating Check in order: 1. **Type conforms to VectorArithmetic?** — `Int` can't animate; use `Double`/`Float` 2. **Animation modifier present?** — Need `.animation(.spring, value: x)` or `withAnimation` 3. **Correct value tracked?** — `.animation(.spring, value: progress)` not `.animation(.spring, value: title)` 4. **View conforms to Animatable?** — Custom views need `@Animatable` (iOS 26+) or manual `animatableData` ### Animation Stuttering Custom `Animatable` conformance calls `body` every frame on main thread. Use built-in effects (`.opacity()`, `.scaleEffect()`) when possible — they run off-main-thread. Profile with Instruments for complex cases. ### Unexpected Animation Merging Spring animations merge by default, preserving velocity. Use timing curve animations (`.easeInOut`) if you don't want merging behavior. See **Animation Merging Behavior** section above. --- ## Resources **WWDC**: 2023-10156, 2023-10157, 2023-10158, 2024-10145, 2025-256 **Docs**: /swiftui/animatable, /swiftui/animation, /swiftui/vectorarithmetic, /swiftui/transaction, /swiftui/view/navigationtransition(_:), /swiftui/view/matchedtransitionsource(id:in:configuration:), /uikit/uiview/animate(_:changes:completion:) **Skills**: axiom-swiftui-26-ref, axiom-swiftui-nav-ref, axiom-swiftui-performance, axiom-swiftui-debugging, axiom-sf-symbols-ref