Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dpearson2699 avatar

Swiftui Animation

  • 3.7k installs
  • 944 repo stars
  • Updated July 15, 2026
  • dpearson2699/swift-ios-skills

How to implement, review, and fix SwiftUI animations and transitions using modern iOS 17+ APIs (withAnimation, PhaseAnimator, KeyframeAnimator, matchedGeometryEffect) with correct timing curves, accessibility handling, a

About

SwiftUI Animation skill covers implementing, reviewing, and fixing animations using explicit withAnimation, implicit .animation modifiers, spring presets, PhaseAnimator for multi-step sequences, KeyframeAnimator for multi-property choreography, matchedGeometryEffect for hero transitions, symbol effects, and custom transitions. Includes triage workflow, animation curve selection, accessibility (reduceMotion) handling, common mistakes, and a review checklist. Master timing curves (.smooth, .snappy, .bouncy), phase/keyframe timing, and modern iOS 17+ APIs while respecting user motion preferences.

  • Explicit withAnimation vs implicit .animation(.body:) vs .animation(.value:) scoping patterns
  • Spring presets (.smooth, .snappy, .bouncy) with custom Spring(duration:bounce:) for natural motion
  • PhaseAnimator for discrete multi-phase sequences and KeyframeAnimator for choreographed multi-property timelines
  • matchedGeometryEffect hero transitions and iOS 18 matchedTransitionSource + .navigationTransition(.zoom)
  • accessibilityReduceMotion integration and common pitfalls (bare .animation, expensive closures, multiple matched sources

Swiftui Animation by the numbers

  • 3,682 all-time installs (skills.sh)
  • +177 installs in the week ending Jul 29, 2026 (Skillselion tracking)
  • Ranked #134 of 2,244 Frontend Development skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-animation

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs3.7k
repo stars944
Security audit3 / 3 scanners passed
Last updatedJuly 15, 2026
Repositorydpearson2699/swift-ios-skills

What it does

Implement, review, and improve SwiftUI animations and transitions with modern APIs, spring curves, keyframe choreography, and accessibility compliance.

Who is it for?

iOS/SwiftUI engineers writing animations for state-driven UI, hero transitions, symbol effects, and complex choreography; code reviewers ensuring animation quality and accessibility compliance.

Skip if: Android developers, web animation frameworks, Core Animation bridging (covered separately), custom timing curve math, high-performance graphics optimization.

When should I use this skill?

Adding explicit animations to state changes, reviewing animation code, building multi-phase sequences, creating hero transitions, implementing symbol effects, ensuring reduce-motion support, or debugging animation perfor

What you get

Team reviews and ships smooth, accessible, performant animations aligned with platform idioms, using spring presets, phase/keyframe choreography, and proper scope binding to state.

  • SwiftUI animation code snippets
  • PhaseAnimator configurations
  • Transition implementations

By the numbers

  • Covers iOS 17+ CustomAnimation protocol and all transition types
  • Documents every Spring initializer variant and UnitCurve types

Files

SKILL.mdMarkdownGitHub ↗

SwiftUI Animation (iOS 26+)

Review, write, and fix SwiftUI animations. Apply modern animation APIs with correct timing, transitions, and accessibility handling using Swift 6.3 patterns.

Contents

Triage Workflow

Step 1: Identify the animation category

CategoryAPIWhen to use
State-drivenwithAnimation, .animation(_:body:), .animation(_:value:)Explicit state changes, selective modifier animation, or simple value-bound changes
Multi-phasePhaseAnimatorSequenced multi-step animations
KeyframeKeyframeAnimatorComplex multi-property choreography
Shared elementmatchedGeometryEffectLayout-driven hero transitions
NavigationmatchedTransitionSource + .navigationTransition(.zoom)NavigationStack push/pop zoom
View lifecycle.transition()Insertion and removal
Text content.contentTransition()In-place text/number changes
Symbol.symbolEffect()SF Symbol animations
CustomCustomAnimation protocolNovel timing curves

Step 2: Choose the animation curve

// Timing curves
.linear                              // constant speed
.easeIn(duration: 0.3)              // slow start
.easeOut(duration: 0.3)             // slow end
.easeInOut(duration: 0.3)           // slow start and end

// Spring presets (preferred for natural motion)
.smooth                              // no bounce, fluid
.smooth(duration: 0.5, extraBounce: 0.0)
.snappy                              // small bounce, responsive
.snappy(duration: 0.4, extraBounce: 0.1)
.bouncy                              // visible bounce, playful
.bouncy(duration: 0.5, extraBounce: 0.2)

// Custom spring
.spring(duration: 0.5, bounce: 0.3, blendDuration: 0.0)
.spring(Spring(duration: 0.6, bounce: 0.2), blendDuration: 0.0)
.interactiveSpring(response: 0.15, dampingFraction: 0.86)

Step 3: Apply and verify

  • Confirm animation triggers on the correct state change.
  • Test with Accessibility > Reduce Motion enabled.
  • Verify no expensive work runs inside animation content closures.

withAnimation (Explicit Animation)

withAnimation(.spring) { isExpanded.toggle() }

// With completion (iOS 17+)
withAnimation(.smooth(duration: 0.35), completionCriteria: .logicallyComplete) {
    isExpanded = true
} completion: { loadContent() }

Implicit Animation

Prefer .animation(_:body:) when only specific modifiers should animate. Use .animation(_:value:) for simple value-bound changes that can animate the view's animatable modifiers together.

Badge()
    .foregroundStyle(isActive ? .green : .secondary)
    .animation(.snappy) { content in
        content
            .scaleEffect(isActive ? 1.15 : 1.0)
            .opacity(isActive ? 1.0 : 0.7)
    }
Circle()
    .scaleEffect(isActive ? 1.2 : 1.0)
    .opacity(isActive ? 1.0 : 0.6)
    .animation(.bouncy, value: isActive)

Spring Type (iOS 17+)

Four initializer forms for different mental models.

// Perceptual (preferred)
Spring(duration: 0.5, bounce: 0.3)

// Physical
Spring(mass: 1.0, stiffness: 100.0, damping: 10.0)

// Response-based
Spring(response: 0.5, dampingRatio: 0.7)

// Settling-based
Spring(settlingDuration: 1.0, dampingRatio: 0.8)

Three presets mirror Animation presets: .smooth, .snappy, .bouncy.

PhaseAnimator (iOS 17+)

Cycle through discrete phases with per-phase animation curves.

enum PulsePhase: CaseIterable {
    case idle, grow, shrink
}

struct PulsingDot: View {
    var body: some View {
        PhaseAnimator(PulsePhase.allCases) { phase in
            Circle()
                .frame(width: 40, height: 40)
                .scaleEffect(phase == .grow ? 1.4 : 1.0)
                .opacity(phase == .shrink ? 0.5 : 1.0)
        } animation: { phase in
            switch phase {
            case .idle: .easeIn(duration: 0.2)
            case .grow: .spring(duration: 0.4, bounce: 0.3)
            case .shrink: .easeOut(duration: 0.3)
            }
        }
    }
}

Trigger-based variant runs one cycle per trigger change:

PhaseAnimator(PulsePhase.allCases, trigger: tapCount) { phase in
    // ...
} animation: { _ in .spring(duration: 0.4) }

KeyframeAnimator (iOS 17+)

Animate multiple properties along independent timelines.

struct AnimValues {
    var scale: Double = 1.0
    var yOffset: Double = 0.0
    var opacity: Double = 1.0
}

struct BounceView: View {
    @State private var trigger = false

    var body: some View {
        Button { trigger.toggle() } label: {
            Image(systemName: "star.fill")
                .font(.largeTitle)
                .keyframeAnimator(
                    initialValue: AnimValues(),
                    trigger: trigger
                ) { content, value in
                    content
                        .scaleEffect(value.scale)
                        .offset(y: value.yOffset)
                        .opacity(value.opacity)
                } keyframes: { _ in
                    KeyframeTrack(\.scale) {
                        SpringKeyframe(1.5, duration: 0.3)
                        CubicKeyframe(1.0, duration: 0.4)
                    }
                    KeyframeTrack(\.yOffset) {
                        CubicKeyframe(-30, duration: 0.2)
                        CubicKeyframe(0, duration: 0.4)
                    }
                    KeyframeTrack(\.opacity) {
                        LinearKeyframe(0.6, duration: 0.15)
                        LinearKeyframe(1.0, duration: 0.25)
                    }
                }
        }
        .buttonStyle(.plain)
    }
}

Keyframe types: LinearKeyframe (linear), CubicKeyframe (smooth curve), SpringKeyframe (spring physics), MoveKeyframe (instant jump).

Use repeating: true for looping keyframe animations.

@Animatable Macro

Replaces manual AnimatableData boilerplate. Attach to any type with animatable stored properties.

// Replaces manual AnimatableData boilerplate
@Animatable
struct WaveShape: Shape {
    var frequency: Double
    var amplitude: Double
    var phase: Double
    @AnimatableIgnored var lineWidth: CGFloat

    func path(in rect: CGRect) -> Path {
        // draw wave using frequency, amplitude, phase
    }
}

Rules:

  • Stored properties must conform to VectorArithmetic.
  • Use @AnimatableIgnored to exclude non-animatable properties.
  • Computed properties are never included.

matchedGeometryEffect (iOS 14+)

Synchronize geometry between views for shared-element animations.

struct HeroView: View {
    @Namespace private var heroSpace
    @State private var isExpanded = false

    var body: some View {
        Group {
            if isExpanded {
                Button {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = false
                    }
                } label: {
                    DetailCard()
                        .matchedGeometryEffect(id: "card", in: heroSpace)
                }
            } else {
                Button {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = true
                    }
                } label: {
                    ThumbnailCard()
                        .matchedGeometryEffect(id: "card", in: heroSpace)
                }
            }
        }
        .buttonStyle(.plain)
    }
}

Exactly one view per ID must be visible at a time for the interpolation to work.

Navigation Zoom Transition (iOS 18+)

Pair matchedTransitionSource on the source view with .navigationTransition(.zoom(...)) on the destination.

struct GalleryView: View {
    @Namespace private var zoomSpace
    let items: [GalleryItem]

    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
                    ForEach(items) { item in
                        NavigationLink {
                            GalleryDetail(item: item)
                                .navigationTransition(
                                    .zoom(sourceID: item.id, in: zoomSpace)
                                )
                        } label: {
                            ItemThumbnail(item: item)
                                .matchedTransitionSource(
                                    id: item.id, in: zoomSpace
                                )
                        }
                    }
                }
            }
        }
    }
}

Apply .navigationTransition on the destination view, not on inner containers.

Transitions (iOS 17+)

Control how views animate on insertion and removal.

if showBanner {
    BannerView()
        .transition(.move(edge: .top).combined(with: .opacity))
}

Built-in types: .opacity, .slide, .scale, .scale(_:anchor:), .move(edge:), .push(from:), .offset(x:y:), .identity, .blurReplace, .blurReplace(_:), .symbolEffect, .symbolEffect(_:options:).

Asymmetric transitions:

.transition(.asymmetric(
    insertion: .push(from: .bottom),
    removal: .opacity
))

ContentTransition (iOS 16+)

Animate in-place content changes without insertion/removal.

Text("\(score)")
    .contentTransition(.numericText(countsDown: false))
    .animation(.snappy, value: score)

// For SF Symbols
Image(systemName: isMuted ? "speaker.slash" : "speaker.wave.3")
    .contentTransition(.symbolEffect(.replace.downUp))

Types: .identity, .interpolate, .opacity, .numericText(countsDown:), .numericText(value:), .symbolEffect.

Symbol Effects (iOS 17+)

Animate SF Symbols with semantic effects.

// Discrete (triggers on value change)
Image(systemName: "bell.fill")
    .symbolEffect(.bounce, value: notificationCount)

Image(systemName: "arrow.clockwise")
    .symbolEffect(.wiggle.clockwise, value: refreshCount)

// Indefinite (active while condition holds)
Image(systemName: "wifi")
    .symbolEffect(.pulse, isActive: isSearching)

Image(systemName: "mic.fill")
    .symbolEffect(.breathe, isActive: isRecording)

// Variable color with chaining
Image(systemName: "speaker.wave.3.fill")
    .symbolEffect(
        .variableColor.iterative.reversing.dimInactiveLayers,
        options: .repeating,
        isActive: isPlaying
    )

All effects: .bounce, .pulse, .variableColor, .scale, .appear, .disappear, .replace, .breathe, .rotate, .wiggle.

Scope: .byLayer, .wholeSymbol. Direction varies per effect.

Symbol Rendering Modes

Control how SF Symbol layers are colored with .symbolRenderingMode(_:).

ModeEffectWhen to use
.monochromeSingle color applied uniformly (default)Toolbars, simple icons matching text
.hierarchicalSingle color with opacity layers for depthSubtle depth without multiple colors
.multicolorSystem-defined fixed colors per layerWeather, file types — Apple's intended palette
.paletteCustom colors per layer via .foregroundStyleBrand colors, custom multi-color icons
// Hierarchical — single tint, opacity layers for depth
Image(systemName: "speaker.wave.3.fill")
    .symbolRenderingMode(.hierarchical)
    .foregroundStyle(.blue)

// Palette — custom color per layer
Image(systemName: "person.crop.circle.badge.plus")
    .symbolRenderingMode(.palette)
    .foregroundStyle(.blue, .green)

// Multicolor — system-defined colors
Image(systemName: "cloud.sun.rain.fill")
    .symbolRenderingMode(.multicolor)

Variable color: .symbolVariableColor(value:) for percentage-based fill (signal strength, volume):

Image(systemName: "wifi")
    .symbolVariableColor(value: signalStrength) // 0.0–1.0
Docs: SymbolRenderingMode · symbolRenderingMode(_:))

Common Mistakes

1. Using bare .animation(_:) when you need precise scope

// TOO BROAD — applies when the view changes
.animation(.easeIn)

// CORRECT — bind animation to one value
.animation(.easeIn, value: isVisible)

// CORRECT — scope animation to selected modifiers
.animation(.easeIn) { content in
    content.opacity(isVisible ? 1.0 : 0.0)
}

2. Expensive work inside animation closures

Never run heavy computation in keyframeAnimator / PhaseAnimator content closures — they execute every frame. Precompute outside, animate only visual properties.

3. Missing reduce motion support

@Environment(\.accessibilityReduceMotion) private var reduceMotion
withAnimation(reduceMotion ? .none : .bouncy) { showDetail = true }

4. Multiple matchedGeometryEffect sources

Only one view per ID should be visible at a time. Two visible views with the same ID causes undefined layout.

5. Using DispatchQueue or UIView.animate

// WRONG
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { isVisible = true } }
// CORRECT
withAnimation(.spring.delay(0.5)) { isVisible = true }

6. Forgetting animation on ContentTransition

// WRONG — no animation, content transition has no effect
Text("\(count)").contentTransition(.numericText(countsDown: true))
// CORRECT — pair with animation
Text("\(count)")
    .contentTransition(.numericText(countsDown: true))
    .animation(.snappy, value: count)

7. navigationTransition on wrong view

Apply .navigationTransition(.zoom(sourceID:in:)) on the outermost destination view, not inside a container.

Review Checklist

  • [ ] Animation curve matches intent (spring for natural, ease for mechanical)
  • [ ] withAnimation wraps the state change; implicit animation uses .animation(_:body:) for selective modifier scope or .animation(_:value:) with an explicit value
  • [ ] matchedGeometryEffect has exactly one source per ID; zoom uses matching id/namespace
  • [ ] @Animatable macro used instead of manual animatableData
  • [ ] accessibilityReduceMotion checked; no DispatchQueue/UIView.animate
  • [ ] Transitions use .transition(); contentTransition is paired with animation and uses the narrowest implicit animation scope that fits
  • [ ] Animated state changes on @MainActor; animation-driving types are Sendable

References

  • See references/animation-advanced.md for CustomAnimation protocol, full Spring variants, all Transition types, symbol effect details, Transaction system, UnitCurve types, and performance guidance.
  • Core Animation bridging patterns: references/core-animation-bridge.md

Related skills

FAQ

Which SwiftUI animation APIs does swiftui-animation cover?

swiftui-animation covers CustomAnimation on iOS 17+, Spring and UnitCurve types, PhaseAnimator, KeyframeAnimator multi-track patterns, Transaction keys, scoped implicit animation, and all iOS 17+ transition types.

When should developers invoke swiftui-animation?

Developers should invoke swiftui-animation when building iOS interfaces that need choreographed motion beyond basic withAnimation calls. The skill supplies deep reference examples for Claude Code and Cursor agents.

Is Swiftui Animation safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.