Skip to content

How to use animation with value in SwiftUI (iOS 16+) | SwiftUI Bootcamp #67

The old .animation(.spring()) modifier animated every state change that hit a view — even changes you didn't intend to animate. The value-based .animation(.spring(), value: someState) introduced in iOS 16 fixes this by making the link between a specific state change and the animation it triggers explicit and intentional.

What You'll Learn

  • Why .animation(_:value:) is preferred over the deprecated parameter-less .animation(_:)
  • How to animate two different state values with two different animation curves independently
  • How SwiftUI determines which visual changes to animate when a specific value changes

Mental Model

The old animation modifier was like installing a motion sensor that triggers every light in the house — any movement anywhere would trigger all the lights. The value-based .animation(_:value:) is like installing dedicated motion sensors per room: "this sensor only triggers the kitchen lights, and this one only triggers the bedroom lights."

Each .animation(_:value:) modifier watches one specific value. When that value changes, only the visual changes caused by that value are animated — not every other state change happening at the same time. Two different animation curves can run simultaneously on the same view hierarchy, each tied to a different value.

Detailed Explanation

.animation(_:value:) requires iOS 16+ (Xcode 14+). It takes two parameters: the animation curve to apply, and the value to watch. The modifier observes the value using Equatable conformance — when the value changes between two render cycles, SwiftUI animates the diff. If the value hasn't changed, the modifier does nothing.

The distinction between .spring() and .linear(duration:) matters in practice. Spring animations feel physical because they can overshoot their target and bounce — ideal for position changes and size changes that should feel elastic. Linear animations move at a constant rate — useful for progress indicators, loading bars, or any animation where a steady pace communicates reliability.

The commented-out .animation(.spring()) at the bottom is the deprecated form. It was deprecated because it would animate literally every state change on that view, including changes triggered by parent re-renders, making the behavior unpredictable and sometimes causing unwanted animations on unrelated state changes.

Minimum deployment target: iOS 16 is required for the value-based .animation. If you need to support iOS 15, use withAnimation { } around your state change as an alternative — it achieves similar scoping.

Code Structure

AnimationUpdatedBootcamp.swift contains one view with two independent boolean states (animate1, animate2) that control the position of a rectangle along horizontal and vertical axes. Two separate .animation modifiers are applied with different curves — this is the key demonstration: each animation curve is linked to exactly one state value.

Complete Code

AnimationUpdatedBootcamp.swift

swift
import SwiftUI

struct AnimationUpdatedBootcamp: View {
    
    @State private var animate1: Bool = false
    @State private var animate2: Bool = false

    var body: some View {
        ZStack {
            VStack(spacing: 40) {
                Button("Action 1") {
                    animate1.toggle() // Triggers the spring animation below
                }
                Button("Action 2") {
                    animate2.toggle() // Triggers the linear animation below
                }
                
                ZStack {
                    Rectangle()
                        .frame(width: 100, height: 100)
                        // animate1 controls horizontal position — leading or trailing
                        .frame(maxWidth: .infinity, alignment: animate1 ? .leading : .trailing)
                        .background(Color.green)
                        // animate2 controls vertical position — top or bottom
                        .frame(maxHeight: .infinity, alignment: animate2 ? .top : .bottom)
                        .background(Color.orange)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.red)
                
            }
        }
        // Only animates changes caused by animate1 flipping — uses a bouncy spring curve
        .animation(.spring(), value: animate1)
        // Only animates changes caused by animate2 flipping — uses a slow, steady linear curve
        .animation(.linear(duration: 5), value: animate2)
        
        // deprecated!
//        .animation(.spring())
    }
}

struct AnimationUpdatedBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        AnimationUpdatedBootcamp()
    }
}

Code Walkthrough

  1. @State private var animate1: Bool = false and animate2 — Two separate boolean values each drive a different visual change. The key lesson: keeping them separate lets you attach different animations to each.

  2. .frame(maxWidth: .infinity, alignment: animate1 ? .leading : .trailing) — The rectangle slides left or right based on animate1. SwiftUI detects the alignment change as a visual diff and animates it when animate1 changes.

  3. .frame(maxHeight: .infinity, alignment: animate2 ? .top : .bottom) — The rectangle moves up or down based on animate2. This is a second independent visual change — SwiftUI stacks the two axis movements.

  4. .animation(.spring(), value: animate1) — This modifier watches only animate1. When it flips, the horizontal position change is animated with a spring. The vertical movement (driven by animate2) is not affected by this modifier.

  5. .animation(.linear(duration: 5), value: animate2) — Watches only animate2. Vertical moves take 5 seconds at a constant rate. This shows the power of independent animation: tap "Action 1" and the rectangle springs left/right instantly; tap "Action 2" and it glides up/down over 5 seconds.

  6. // deprecated! .animation(.spring()) — The old modifier. It would apply spring animation to every state change on this view — including animate2's vertical movement, meaning you could never give them different curves.

Common Mistakes

Mistake: Using the deprecated .animation(_:) without a value parameter
The deprecation warning exists for a reason. Without a value, the modifier animates everything, including re-renders triggered by parent views. This leads to unexpected animations and is hard to debug. Always provide a value: parameter.

Mistake: Animating with .animation(_:value:) but forgetting the state variable needs to actually change
The animation only fires when the watched value changes. Setting animate1 = false when it's already false does nothing — no animation plays. This is by design, but can confuse learners who expect an animation to replay on every button tap.

Mistake: Placing .animation inside the view hierarchy rather than on the root
Placing .animation(.spring(), value: animate1) on an inner view (like directly on the Rectangle) limits which parts of the view hierarchy it animates. Placing it on the outer container (as done here) means all visual changes anywhere inside that container that result from animate1 changing are animated.

Key Takeaways

  • .animation(_:value:) is the modern API (iOS 16+) that links a specific animation curve to a specific state value — avoiding the "animate everything" behavior of the deprecated .animation(_:)
  • Multiple .animation modifiers can coexist on the same view, each watching a different value and using a different curve
  • Use withAnimation { } around a state mutation as an equivalent alternative when you need to support iOS 15

Last updated: June 27, 2026

Released under the MIT License.