Creating Your First Animation

In SwiftUI, your user interface is a direct function of your app’s state. When a value in your state changes, SwiftUI automatically and efficiently re-renders the parts of your UI that depend on that value. Because SwiftUI knows the “before” and “after” states, it can seamlessly and automatically calculate all the in-between steps to create a fluid animation.

The modern and recommended way to create an animation is to wrap the state change itself—the cause of the UI update—in a withAnimation block. This paradigm shift is powerful because it attaches the animation to the action, not the view. It makes your code’s intent clearer: “When this happens, animate the consequences.”

In the example below, we will schedule a series of state changes to happen over time using DispatchQueue.

A Quick Intro to DispatchQueue

Before looking at the code, let’s briefly touch on DispatchQueue. At this stage, you can think of it as a manager for your app’s to-do list.

  • What it does: It organizes tasks that your app needs to perform. The most important list is the main queue (DispatchQueue.main), which is exclusively responsible for all tasks that update the user interface.

  • Why we use it: We are using a specific function called .asyncAfter. This is like telling the manager, “Add this task to the UI to-do list, but don’t do it right away. Wait for a specific amount of time to pass first.” This allows us to schedule our circle’s state changes to happen one after another with a delay, creating a timed sequence of animations.

Here is the complete view for an animating circle that cycles through different colors and positions.

// MARK: - 1. Basic Animation: withAnimation
struct BasicAnimationView: View {
    // Data for our animation
    struct AnimationData: Equatable {
        var color: Color
        var offset: CGSize
    }

    private let animationDataPoints: [AnimationData] = [
        .init(color: .green, offset: .init(width: -100, height: -100)),
        .init(color: .blue, offset: .init(width: 100, height: -100)),
        .init(color: .red, offset: .init(width: 100, height: 100)),
        .init(color: .orange, offset: .init(width: -100, height: 100)),
        .init(color: .green, offset: .init(width: -100, height: -100))
    ]

    @State private var currentDataPoint = AnimationData(color: .green, offset: .init(width: -100, height: -100))

    var body: some View {
        VStack {
            Text("withAnimation")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding(.bottom, 50)

            Circle()
                .foregroundStyle(currentDataPoint.color)
                .frame(width: 80, height: 80)
                .offset(currentDataPoint.offset)
            
            Spacer()
        }
        .onAppear(perform: cycleAnimationData)
    }

    func cycleAnimationData() {
        for (index, data) in animationDataPoints.enumerated() {
            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(index * 2)) {
                withAnimation(.spring(duration: 0.8, bounce: 0.5)) {
                    currentDataPoint = data
                }
            }
        }
    }
}

Combining Animations

The power of state-driven animation becomes truly clear when you modify multiple view properties with a single state change. Imagine an app for booking tours where tapping a tour’s thumbnail image makes it expand to fill the screen. We want this interaction to feel dynamic and satisfying.

We can achieve this with a single @State boolean, zoomed, which will act as our “source of truth”. This single variable will control the image’s scale (.scaleEffect), shape (.clipShape), color saturation (.foregroundStyle), and rotation (.rotationEffect).

When the user taps the image, we toggle the zoomed property from false to true. Because this change is wrapped in a withAnimation block, SwiftUI looks at all the properties that depend on zoom, calculates their start and end values, and animates all of them in perfect concert. This creates a complex, professional-looking effect from a very simple action.

Here is the code for the combined animation example.

// MARK: - 2. Combined Animation: One State, Many Effects
struct CombinedAnimationView: View {
    @State private var zoomed = false

    var body: some View {
        VStack {
            Text("Combined Animations")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding(.bottom, 50)

            Image(systemName: "photo.artframe")
                .font(.system(size: 100))
                .foregroundStyle(.white, zoomed ? .cyan : .blue)
                .clipShape(RoundedRectangle(cornerRadius: zoomed ? 40 : 100))
                .scaleEffect(zoomed ? 2.0 : 1.0)
                .rotationEffect(zoomed ? .degrees(0) : .degrees(360))
                .shadow(radius: 10)
                .onTapGesture {
                    withAnimation(.spring(duration: 0.7, bounce: 0.5)) {
                        zoomed.toggle()
                    }
                }
            
            Text("Tap the image!")
                .padding(.top)
            
            Spacer()
        }
    }
}

SwiftUI provides several built-in animation types beyond .spring, and you can create custom curves.

Built-in Animations

.default

withAnimation(.default) { ... }
  • A standard ease-in-out animation.
  • Good general-purpose choice. Equivalent to:
Animation.easeInOut(duration: 0.35)

.linear(duration: )

withAnimation(.linear(duration: 1.0)) { ... }
  • Constant speed from start to finish.
  • Useful for continuous, uniform motion (e.g., loading indicators).

.easeIn(duration:)

withAnimation(.easeIn(duration: 1.0)) { ... }
  • Starts slowly, speeds up at the end.
  • Great for introducing motion naturally (e.g., something dropping).

.easeOut(duration:)

withAnimation(.easeOut(duration: 1.0)) { ... }
  • Starts quickly, slows down toward the end.
  • Good for things exiting the screen.

.easeInOut(duration:)

withAnimation(.easeInOut(duration: 1.0)) { ... }
  • Slow start and end, fast in the middle.
  • Feels very “natural” and is commonly used for transitions.

.spring(response:dampingFraction:blendDuration:)

withAnimation(.spring(response: 0.5, dampingFraction: 0.6)) { ... }
  • Physics-based animation.
  • Parameters:
    • response: how quickly it starts responding
    • dampingFraction: how “bouncy” it is (lower = more bounce)
    • blendDuration: time to blend between animations

.interpolatingSpring(stiffness:damping:)

withAnimation(.interpolatingSpring(stiffness: 100, damping: 5)) { ... }
  • Another type of spring animation with a more direct physics model.
  • stiffness: how resistant it is to movement
  • damping: how much friction it has

Custom Timing Curve (Bezier)

.timingCurve (via Animation.timingCurve)

withAnimation(Animation.timingCurve(0.25, 0.1, 0.25, 1, duration: 1.0)) { ... }
  • Define a custom cubic Bézier curve.
  • Gives you control like in CSS (cubic-bezier()).
  • Common for very fine-tuned animations (but more complex).

Repeating Animation

Any of the above can be combined with .repeatForever() or repeatCount().

.withAnimation(.linear(duration: 1).repeatForever(autoreverses: true)) { ... }

Summary

Here is a handy chart showing all of the built in animation types and where to use them.

Good for Description Animation Type Timers, progress bars Constant speed . linear General Use Smooth ease in out .default Reveals, intros Starts slow . easeIn Exits, disappearing elements Ends slow .easeOut Buttons, playful motion Springy, bouncy .spring Precise custom timing Custom Bezier curve .timingCurve Transitions, slides Slow > fast > slow .easeInOut Custom stiffness/damping Physics‑style spring .interpolatingSpring

Lets move on to some advanced animation techniques.

See forum comments
Download course materials from Github
Previous: Introduction to SwiftUI Animation Next: Advanced Animation Techniques