🚀

Mastering SwiftUI Animations: Implicit vs. Explicit Guide

(Updated on )

TL;DR: Implicit animation (.animation) is bound to a view and works passively, defining “how to move if a change happens.” Explicit animation (withAnimation) wraps the state change logic and acts proactively, defining “every change caused by this specific update should be animated.”

SwiftUI provides two distinct mechanisms for animation: implicit and explicit. Beginners often confuse the priority and scope of these two, leading to lost animations or unexpected visual glitches. This article breaks down the core differences and best use cases for each.

Implicit Animation

Implicit animation is declared on a view using modifiers (like .animation). It acts like a property, telling the view system: “Whenever the monitored value changes, animate the transition using this specific curve.”

Key Characteristics:

  1. Proximity Rule: An implicit animation on a child view overrides animation settings from a parent view.
  2. Automatic Propagation: It propagates down the view hierarchy until it is intercepted by a deeper animation modifier.
Swift
struct ImplicitAnimationDemo: View {
    @State private var isActive = false
    
    var body: some View {
        VStack {
            VStack {
                Text("Hello") // Defines .smooth itself; highest priority
                    .offset(x: isActive ? 200 : 0)
                    .animation(.smooth, value: isActive)

                Text("World") // Inherits .linear from the parent VStack
                    .offset(x: isActive ? 200 : 0)
            }
            // Parent container defines .linear
            .animation(.linear.speed(3), value: isActive)

            // No animation defined, nor inherited (unless a Transaction is involved)
            Text("No Animation")
                .offset(x: isActive ? 200 : 0)

            Toggle("Active", isOn: $isActive)
        }
    }
}

Explicit Animation

Explicit animation is imperative. It is triggered via withAnimation or a withTransaction closure. It tells the system: “For every state change happening inside this closure, apply an animation to the resulting view updates.”

Key Characteristics:

  1. Global Coverage: It provides a “default animation” for all views affected by the state change.
  2. Priority: If a view does not have an implicit animation, it uses the explicit animation. If a view does have an implicit animation, the implicit animation usually overrides the explicit one.
Swift
struct ExplicitAnimationDemo: View {
    @State private var isActive = false
    
    var body: some View {
        VStack {
            // ... (Same Hello/World structure as above) ...

            // Has no implicit animation modifier
            Text("Default Spring")
                .offset(x: isActive ? 200 : 0)
            
            Button("Toggle") {
                // Explicit animation: All affected views without their own opinion use .spring
                withAnimation(.spring) {
                    isActive.toggle()
                }
            }
        }
    }
}

In this example, when the button is clicked:

  • Text("Hello") still uses its own .smooth (Implicit overrides Explicit).
  • Text("Default Spring") has no implicit animation, so it obeys withAnimation(.spring).

Core Differences Summary

FeatureImplicit Animation (.animation)Explicit Animation (withAnimation)
Definition LocationView Hierarchy (View Modifier)Logic Level (State Change)
ScopeRestricted to the modified view and its childrenAll views affected by the state inside the closure
PriorityHigh (Can override explicit animation)Low (Acts as a default fallback)
Use CaseFine-grained control for specific viewsGlobal state changes, unified transitions, List operations

Advanced Tip: Disabling Animations

Sometimes you need to force a specific view not to animate within the context of an explicit animation. You can achieve this using a Transaction.

Swift
// Forcefully disable animation
var transaction = Transaction(animation: .none)
transaction.disablesAnimations = true

withTransaction(transaction) {
    isActive.toggle()
}

Or disable it at the view level:

Swift
Text("No Animation")
    .animation(nil, value: isActive) // Forcefully set to nil

Modern Best Practices (Swift 6)

As SwiftUI evolves, adhere to these standards to avoid warnings and undefined behavior:

  1. Must Bind a Value: ❌ Deprecated: .animation(.spring()) ✅ Recommended: .animation(.spring, value: isActive) Reason: The old API caused performance issues and bugs where unrelated state changes in the view tree could trigger animations.

  2. Scoped Animations (iOS 17+): Use .animation(_:body:) to precisely control the scope of an animation without affecting child views.

Swift
Text("Title")
    .animation(.default) { content in
        content.scaleEffect(isActive ? 1.2 : 1.0)
    }

Further Reading

Related Tips

Subscribe to Fatbobman

Weekly Swift & SwiftUI highlights. Join developers.

Subscribe Now