axiom-swiftui-animation-ref by CharlesWiltgen
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
Content & Writing
238 Stars
16 Forks
Updated Jan 16, 2026, 03:16 PM
Why Use This
This skill provides specialized capabilities for CharlesWiltgen's codebase.
Use Cases
- Developing new features in the CharlesWiltgen repository
- Refactoring existing code to follow CharlesWiltgen standards
- Understanding and working with CharlesWiltgen's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 1014 Lines
Total Files 1
Total Size 0 B
License MIT
---
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<CGSize.AnimatableData, UnitPoint.AnimatableData>`. 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<CGFloat, CGFloat>,
AnimatablePair<Double, AnimatablePair<CGFloat, CGFloat>>> {
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<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: inout AnimationContext<V>
) -> V?
// Optional: Should this animation merge with previous?
func shouldMerge<V>(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>) -> Bool
// Optional: Current velocity
func velocity<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: AnimationContext<V>
) -> V?
}
```
#### Example: Linear timing curve
```swift
struct LinearAnimation: CustomAnimation {
let duration: TimeInterval
func animate<V: VectorArithmetic>(
value: V, // Delta vector: target - current
time: TimeInterval,
context: inout AnimationContext<V>
) -> 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
Name Size