agentsclimarketplace

Swiftui animation

Skill Tyr0/agent-skills/plugins/swiftui-expert/skills/swiftui-animation

A collection of skills, plugins, and agents for AI workflows.

Install
npx -y skills add Tyr0/agent-skills --skill swiftui-animation

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use this skill whenever the user asks about SwiftUI animation — `withAnimation`, `.animation` modifier, `Transaction`, implicit vs explicit animations, `matchedGeometryEffect`, phase animators (`PhaseAnimator`), keyframe animators (`KeyframeAnimator`), custom `Animatable` and `AnimatableModifier`, transitions, `AnyTransition`, spring animations, custom timing curves, animating shape/path, animating gradient, or any "why isn't my view animating" / "why is the wrong thing animating" / "how do I do shared element transitions" question. Targets iOS 17+. Triggers on questions about animation correctness, animation timing, or animation performance.

SKILL.md

10.2 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it

SwiftUI Animation

Animation in SwiftUI is state-driven: you change a value in an animated transaction; the framework interpolates animatable properties on every affected view. Targets iOS 17+.


Two Worlds: Implicit and Explicit

Explicit — withAnimation

Wrap the state mutation that should be animated:

withAnimation(.easeInOut(duration: 0.3)) {
    isExpanded.toggle()
}

Anything that re-evaluates because of that mutation animates. This is the default tool — reach for it first.

Implicit — .animation(_:value:)

Attach an animation to a value; the framework animates whenever that value changes.

Circle()
    .scaleEffect(scale)
    .animation(.spring, value: scale)

Always use the value-bound form .animation(_:value:). The legacy .animation(_:) (no value) is deprecated and dangerous — it animates every state change in scope, often things you didn't intend.

When to use which

  • withAnimation when the change happens at one place (button tap, gesture end). You control the animation per call site.
  • .animation(_:value:) when a property changes from many places and the rule "this view always animates this value with this curve" is what you want.

Animatable Properties

A view animates animatable properties on its modifier chain:

  • .scaleEffect, .rotationEffect, .offset, .position, .frame, .padding
  • .opacity, .foregroundStyle (when between two color values)
  • Shape paths via Animatable
  • Gradient stops, etc.

Not animatable on the render thread:

  • Text content (the string itself)
  • Conditional view presence — that's a transition, not an animation
  • Stored state values that aren't bound to an animatable view modifier

If you need to animate a non-animatable thing (e.g., a counter going from 0→100 with text changes), use Text(value, format:) and animate the source Double — SwiftUI animates the binding (iOS 17 has contentTransition(.numericText())).


Animation Curves

CurveUse
.linear(duration:)Linear interpolation; rare in UI
.easeIn / .easeOut / .easeInOut(duration:)Generic UI
.spring(duration:, bounce:) (iOS 17)Default-feeling iOS motion
.smooth / .snappy / .bouncy (iOS 17)Pre-tuned springs
.interpolatingSpring(stiffness:, damping:)Physics-based, momentum-preserving
.timingCurve(c0x, c0y, c1x, c1y, duration:)Custom Bézier

.spring is the most natural for UI gestures; .smooth for ambient changes; .snappy for confirmation-style feedback.

Modifying an animation

.animation(.spring.delay(0.2).speed(1.5), value: x)
.animation(.linear.repeatForever(autoreverses: true), value: pulse)

Transitions

A transition describes how a view appears/disappears when added/removed from the hierarchy:

if showing {
    Banner()
        .transition(.move(edge: .top).combined(with: .opacity))
}

For the transition to actually run, the change in showing must happen inside an animated context (withAnimation).

Built-ins: .opacity, .scale, .move(edge:), .slide, .identity, .asymmetric(insertion:removal:), .combined(with:).

Custom: implement Transition (iOS 17+) or AnyTransition.modifier(active:identity:).


matchedGeometryEffect — Shared Element Transitions

Two views in different parts of the tree can be linked so that when one disappears and the other appears, they appear to morph:

@Namespace private var ns

if expanded {
    LargeCard().matchedGeometryEffect(id: "card", in: ns)
} else {
    SmallCard().matchedGeometryEffect(id: "card", in: ns)
}

Wrap the toggle in withAnimation. Use the same id: on both sides; only one should be present at a time.

iOS 18 introduces zoom/zoom transitions for nav-stack-level shared element moves.


PhaseAnimator (iOS 17+)

Steps through a sequence of phases automatically:

PhaseAnimator([0, 1, 2]) { phase in
    Star()
        .scaleEffect(phase == 1 ? 1.5 : 1)
        .rotationEffect(.degrees(Double(phase) * 90))
} animation: { phase in
    .easeInOut(duration: 0.4)
}

Use for canned multi-step animations driven by the framework rather than explicit timers.

.phaseAnimator(_:trigger:content:) modifier form re-runs when a trigger value changes — perfect for "celebrate on success" effects.


KeyframeAnimator (iOS 17+)

Multi-track keyframe animations:

KeyframeAnimator(initialValue: AnimationValues(), trigger: bump) { values in
    Logo()
        .scaleEffect(values.scale)
        .rotationEffect(values.rotation)
} keyframes: { _ in
    KeyframeTrack(\.scale) {
        CubicKeyframe(1.2, duration: 0.2)
        SpringKeyframe(1.0, duration: 0.5)
    }
    KeyframeTrack(\.rotation) {
        LinearKeyframe(.degrees(20), duration: 0.2)
        SpringKeyframe(.zero, duration: 0.5)
    }
}

Each track interpolates an independent property over time. Choose LinearKeyframe, CubicKeyframe, SpringKeyframe, MoveKeyframe.


Custom Animatable

For animating a value SwiftUI doesn't already know how to interpolate, conform Animatable:

struct CircleProgress: Shape, Animatable {
    var progress: Double
    var animatableData: Double {
        get { progress }
        set { progress = newValue }
    }
    func path(in rect: CGRect) -> Path { ... uses progress ... }
}

For two-dimensional animation: animatableData = AnimatablePair<Double, Double>.


Transactions

A Transaction is the context-bag carrying animation info through body evaluation. You can inspect or override it:

withTransaction(\.disablesAnimations, true) {
    state = newValue   // mutate without animating, even inside animation scope
}

Transaction(animation: .spring) constructs a transaction explicitly. Useful in custom controls that compose with caller animations.


contentTransition (iOS 16+)

Animate textual content changes:

Text(count, format: .number)
    .contentTransition(.numericText())
    .animation(.snappy, value: count)

Variants: .numericText(), .opacity, .identity, .symbolEffect (for SF Symbols).

Image(systemName:).symbolEffect(.bounce, value: bounceTrigger) — system-symbol animations.


Animation Performance

  • Animating transform-like modifiers (scaleEffect, rotationEffect, offset, opacity) is render-thread cheap.
  • Animating frame/layout reflows the layout pass each frame — cheaper than UIKit's equivalent but more expensive than transforms.
  • Animating something that re-runs body on each frame (e.g., a ForEach whose count changes) is the most expensive — usually reflects a design problem.
  • drawingGroup() flattens an animated subtree into a single Metal layer — useful for complex composites.
  • Avoid .animation(.default) blanket modifiers; they animate state changes you didn't think about.

Gestures and Animation

Spring physics with interactiveSpring give tactile feel to drag gestures:

@State private var drag: CGSize = .zero

DragGesture()
    .onChanged { drag = $0.translation }
    .onEnded { _ in
        withAnimation(.interactiveSpring) { drag = .zero }
    }

.interactiveSpring(response:dampingFraction:blendDuration:) is tuned for follow-finger interactions.


Recipes

GoalApproach
Toggle visibility with fadeif x { V() } + .transition(.opacity) + withAnimation { x.toggle() }
Animate number counterText(n, format: .number).contentTransition(.numericText()).animation(.snappy, value: n)
Animate SF SymbolImage(systemName: ...).symbolEffect(.bounce, value: trigger)
Shared element morph@Namespace + matchedGeometryEffect(id:in:) on both endpoints + withAnimation
Pulse loop.scaleEffect(pulse).animation(.easeInOut.repeatForever(autoreverses: true), value: pulse)
Confetti / one-shotPhaseAnimator with trigger
Multi-track choreographedKeyframeAnimator
Path morphConform shape to Animatable

Anti-Patterns

Anti-patternProblemFix
.animation(.default) (no value) at top of viewAnimates every unrelated state changeUse .animation(_:value:) or withAnimation
withAnimation around the consequence of stateNothing animates; you wrapped the wrong thingWrap the mutation
matchedGeometryEffect with both copies presentNo transition; just two viewsEnsure exactly one is in the tree per state
Animating text by recomputing whole subtree per frameHigh body costcontentTransition(.numericText())
Reaching for Timer to animateRe-renders every tick; fights the frameworkPhaseAnimator / KeyframeAnimator / .repeatForever
.transition without withAnimationTransition never runsWrap the toggling state change
Missing @Namespace scope (declared in different views)Matched geometry breaksOne namespace, shared via parent or environment
Animating frame when transform sufficesTriggers layout each framePrefer scaleEffect / offset
Long repeatForever on hidden viewsWastes CPU offscreenStop animation when offscreen (.onDisappear)
Using the legacy AnimatableModifierDeprecated; clunkier than Animatable on the type itselfConform shape/view to Animatable
Interactive drag without interactiveSpring"Follow then snap" feels mushy.interactiveSpring for in-gesture, .spring for release
Animating a @State change without a UI consequenceNothing happens; "why doesn't it animate?"Bind the state to an animatable modifier

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.