swiftcnearly preview

Animations

Let SwiftUI interpolate state changes with familiar animation classes.

Use animation classes to animate changes to the modifiers that .tw owns:

Button("Rename", action: rename)
    .buttonStyle(.tw("dropdown-item active:opacity-80 animate-spring duration-150"))

Text("Details")
    .tw("\(expanded ? "p-6 rounded-xl" : "p-3 rounded-md") bg-surface animate-ease-out duration-200", value: expanded)

SwiftUI owns the state and interpolates the native modifier values. Plain button press and release, hover, explicit focus, and computed strings use the same animation rules. Apple animates native button styles itself. You do not need an extra .animation(..., value:) call for these styled values. The modifier structure stays stable during these changes. Pass value: for layout changes, including padding that changes a view's intrinsic size. The Motion demo uses this form so its card grows and shrinks through intermediate sizes.

Native presets

ClassNative animation
animate-linear.linear(duration:)
animate-ease-in, animate-ease-out, animate-ease-in-outNative timing curves
animate-spring.spring(duration:)
animate-smooth, animate-snappy, animate-bouncyNative spring presets
animate-noneDisable animation for the styled modifiers
duration-200, delay-100Duration and delay in milliseconds

The default duration is 300 milliseconds. The default delay is zero. Timing utilities accept finite, nonnegative decimal values. Later utilities override the same property. State variants use the normal precedence rules.

The destination state selects the animation, including during release or focus loss. Keep a base animation class when both entry and exit need animation.

Transactions and accessibility

Without an animation class, .tw preserves the caller's transaction. Duration and delay alone do not enable animation. Explicit animation classes respect Reduce Motion and Transaction.disablesAnimations. The scoped transaction affects the styled modifiers and preserves the content's existing transaction. Native modifiers outside .tw keep their own animation settings.

Animation classes configure value changes; they do not start a repeating animation on appearance. View insertion and removal continue to use native .transition and application transactions.

Custom presets

Set custom presets through global rules:

let rules = TWGlobalRules(
    named: ["motion-card": "card animate-settle duration-250"],
    animations: [
        "settle": TWAnimation { duration in
            .spring(duration: duration, bounce: 0.15)
        },
        "press": TWAnimation(.interactiveSpring(response: 0.25, dampingFraction: 0.8))
    ]
)

ContentView().twRules(rules)
// Inside ContentView:
Text("Details").tw("motion-card")
Button("Rename", action: rename).buttonStyle(.tw("dropdown-item animate-press"))

Preset factories receive the resolved duration in seconds. A fixed native preset keeps its own duration; duration-* does not retime it. Both forms accept delay-*. Partial preset dictionaries preserve built-in presets. Subtree updates can replace individual presets through rules.animations. String presets resolve against the current rules during rendering.

Typed animation utilities

Typed utilities use the same rules:

Text("Details").tw(.p(4), .animation(.spring), .duration(0.2), .delay(0.05))

Typed duration and delay utilities accept seconds.

Animate the subtree

Pass a watched value when changes include child content or native layout:

VStack {
    if expanded {
        Text("More details").transition(.opacity)
    }
    Button("Toggle") { expanded.toggle() }
}
.tw("animate-smooth duration-300", value: expanded)

Use .twAnimation("animate-smooth duration-300", value: expanded) when you only need an animation wrapper.

The wrapper also accepts a shared recipe containing source-specific tags. It validates the classes and extracts motion without applying the recipe's styling or source transforms. For example, an image recipe containing resize animate-spring can also supply its animation through .twAnimation. Unknown classes and invalid arguments still reject the recipe.

Without value:, animation classes affect the styling properties owned by .tw. With value:, the explicit wrapper animates the subtree when the value changes.

Choose the animation scope

Pass animationScope: to select where the preset applies:

Text("A steady label")
    .tw("p-[\(CGFloat(expanded ? 32 : 12))] rounded-xl bg-accent animate-spring",
        value: expanded, animationScope: .layout)
ScopeReceives the explicit preset
.surfaceOwned text attributes, layout, decoration, effects, and general registered modifiers
.layoutOwned padding, frames, shared geometry, and .layout registrations
.contentChild content, native source changes, text attributes, and .content registrations
.allBoth the surface and child content

The defaults stay compatible: .surface without value:, and .all with value:. Explicit .surface, .layout, and .content scopes watch only the supplied value. Unrelated state changes do not enable their preset. The compatible .all scope continues to animate owned styling changes and watches the value for child animation.

Layout animation also drives native child placement so content stays aligned with its surface. Watched .surface and .layout scopes set the inherited content transition to .identity. A descendant can choose its own explicit content transition. The .content scope includes child-owned layout, while the surrounding surface changes immediately. Use it on a stable surface when you want a counter or another content transition.

Scopes select where swiftcn applies its preset. They do not cancel an ancestor's native animation. With no preset, the native caller transaction remains unchanged. Every explicit preset respects Reduce Motion and disabled animation transactions.

Content transitions remain native and explicit:

Text("\(count)")
    .contentTransition(.numericText())
    .tw("text-xl animate-smooth", value: count, animationScope: .content)

Use .contentTransition(.identity) on a label when its content changes should appear immediately. This does not cancel the surrounding surface's size animation.

Continue with shared elements to connect geometry between states.

On this page