All articles
iOS13 min read

SwiftUI Animations for Beginners: Learn with Simple Examples (2025 Edition)

Animations in SwiftUI feel like magic 🪄 — until your view just snaps instead of fades. In this beginner-friendly guide, we’ll animate…

K
Karan Pal
Author
Image Generated by AI
Image Generated by AI

🌀 Introduction: Why SwiftUI Animations Aren’t Just Eye Candy

Animations in SwiftUI can feel like magic 🪄. Tap a button, and a view gracefully fades in. Toggle a switch, and a box zooms into place. No UIView.animate, no delegates, no fuss. Just... a state change.

But there’s something deeper going on. When a view animates in SwiftUI, it’s not because a function was called to “move it.” Instead, SwiftUI re-renders your view with updated state, and — if it detects the change is part of an animation — it interpolates between the old and new values over time. That’s right: it animates the difference between two states, not the thing itself.

This approach is beautiful… and sometimes frustrating. You wrap a state change in withAnimation and nothing happens. You apply .animation() and still no fade. Or worse—your view just blinks instead of sliding.

This guide is here to clear up all that confusion.

🔍 What You’ll Learn

This beginner-friendly guide covers:

We’ll explore these concepts through simple, clean examples you can drop into a SwiftUI preview and see live in action.

And if you’re ready to go beyond the basics? Check out the advanced animations guide here, where we dive into matchedGeometryEffect, TimelineView, and more.

**Advanced Animations in SwiftUI: matchedGeometryEffect, TimelineView, PhaseAnimator & Beyond (2025…** _In this deep dive, we’re exploring SwiftUI’s most advanced animation tools — from layout-matching transitions and…_medium.comhttps://medium.com/swift-pal/advanced-animations-in-swiftui-matchedgeometryeffect-timelineview-phaseanimator-beyond-2025-da8876b7b0b9

Let’s kick things off with SwiftUI’s foundational animation tool: withAnimation.

📌 1. withAnimation — SwiftUI’s Animation Trigger

Let’s start with the most common animation tool in SwiftUI:

withAnimation {
    isVisible.toggle()
}

At first glance, this might look like a fancy UIView.animate, but it’s very different. There are no callbacks, no frames, and you're not animating a view directly. You’re telling SwiftUI:

“Hey, I’m about to change some state. Animate whatever changes result from that.”

SwiftUI takes that signal, observes the updated @State, and checks if any view properties that can animate (like **opacity**, **scale**, **position**) have changed. If yes, it interpolates them over time.

🎬 Under the Hood: What’s Actually Happening?

When you use withAnimation, SwiftUI starts an animation transaction. Think of it like a timeline: any view change that occurs during that window will be animated.

// Example 1: Implicit animation via state change
@State private var isVisible = false

var body: some View {
    VStack {
        Button("Toggle") {
            withAnimation(.easeInOut(duration: 0.5)) {
                isVisible.toggle()
            }
        }

        if isVisible {
            Rectangle()
                .fill(.blue)
                .frame(width: 100, height: 100)
                .transition(.slide)
        }
    }
}

In this code:

📌 If you remove withAnimation, the rectangle still appears—but it’ll pop into place, not slide.

🆕 New in iOS 17+: withAnimation(..., completion:)

This one’s a game-changer: SwiftUI finally lets you know when an animation is done.

withAnimation(.easeInOut(duration: 1.0)) {
    isVisible.toggle()
} completion: {
    print("Animation finished!")
}

This is especially helpful when:

🧪 Example: Animate with Completion Message

struct AnimatedToggleView: View {
    @State private var isVisible = false
    @State private var message = ""

    var body: some View {
        VStack(spacing: 20) {
            Button("Show Box") {
                if #available(iOS 17.0, *) {
                    withAnimation(.easeInOut(duration: 0.6)) {
                        isVisible.toggle()
                    } completion: {
                        message = isVisible ? "🎉 Animation complete!" : ""
                    }
                } else {
                    withAnimation {
                        isVisible.toggle()
                    }
                }
            }

            if isVisible {
                Rectangle()
                    .fill(.mint)
                    .frame(width: 120, height: 120)
                    .transition(.scale)
            }

            if !message.isEmpty {
                Text(message)
                    .font(.headline)
            }
        }
        .padding()
    }
}

📝 On iOS 17 and above, you’ll see a clean scale transition and a message that appears only after the animation is complete. Below iOS 17, it will still animate — just without the callback.

📌 2. .animation() Modifier — What It Really Does (and Why It Confuses Everyone)

The .animation() modifier was one of the original ways to animate views in SwiftUI. It looks so simple:

Circle()
    .frame(width: isExpanded ? 200 : 100)
    .animation(.spring(), value: isExpanded)

And hey, it works! The circle resizes smoothly when isExpanded changes. So what’s the problem?

🧠 What .animation() Actually Does

It attaches an implicit animation to the view, telling SwiftUI:

“Whenever the _value:_ you gave me changes, animate all animatable properties in this view using this animation.”

That value: part? Crucial. If you leave it out or set it to the wrong thing, your animation might:

⚠️ The Silent Trap

A lot of early tutorials (and devs) used .animation(.easeInOut) without the value::

.opacity(isVisible ? 1 : 0)
.animation(.easeInOut) // ❌ This might not work as expected in recent SwiftUI versions

This worked pre-iOS 15 because SwiftUI used to infer animation triggers more liberally. But since iOS 15+, you must specify the value you’re watching:

.opacity(isVisible ? 1 : 0)
.animation(.easeInOut, value: isVisible) // ✅ Works predictably

If isVisible changes, SwiftUI interpolates the .opacity from the old value to the new one using .easeInOut.

🧪 Example: Resizing Circle with .animation()

struct AnimatedCircleView: View {
    @State private var isExpanded = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Toggle Size") {
                isExpanded.toggle()
            }

            Circle()
                .fill(.blue)
                .frame(width: isExpanded ? 200 : 100, height: isExpanded ? 200 : 100)
                .animation(.spring(response: 0.4, dampingFraction: 0.6), value: isExpanded)
        }
        .padding()
    }
}

Every time you tap the button, SwiftUI compares the new isExpanded value with the previous one. Since it changed, it animates the frame difference using a spring.

🧪 What Is a Spring Animation?

Imagine stretching a rubber band and letting it go. It doesn’t just stop instantly — it overshoots, rebounds, and settles. A spring animation replicates this effect digitally by simulating mass and tension.

In SwiftUI, the .spring() modifier allows you to control that behavior with fine-tuned parameters:

🧩 Parameter Breakdown

1\. response: 0.4

🧠 Analogy: Response is like how “eager” the spring is to react.

2\. dampingFraction: 0.6

A value of 0.6 means the animation will have some bounce, but not too much — like a well-tuned UI that feels responsive yet natural.

📌 Combined Effect

So this line:

.spring(response: 0.4, dampingFraction: 0.6)

…creates an animation that:

⛔ Should You Still Use .animation()?

Well… yes and no.

✅ Use .animation() when:

❌ Avoid it when:

In newer SwiftUI designs (especially iOS 16+), developers often prefer **withAnimation** because:

🔍 TL;DR: When to Use .animation() vs withAnimation

  1. ✅ Use .animation() when you want:

2\. ❌ Avoid .animation() when:

📌 3. Making Views Appear/Disappear with transition(_:)

SwiftUI doesn’t animate views the way UIKit did. In UIKit, you’d animate opacity or frame changes directly. In SwiftUI, views don’t even exist until your state says they do — so if a view appears or disappears, you’re dealing with creation or removal, not transformation.

That’s where transition(_:) comes in. It lets SwiftUI animate the insertion or removal of a view when it appears or disappears as a result of a state change.

🪄 Basic Usage: Slide a View In

if isVisible {
    Rectangle()
        .fill(.blue)
        .frame(width: 100, height: 100)
        .transition(.slide)
}

Wrap the state change in withAnimation:

withAnimation {
    isVisible.toggle()
}

📌 This animates the view entering and leaving with a slide — as long as SwiftUI knows it’s being inserted or removed.

🧠 But Wait: Why Doesn’t It Always Work?

Here’s the catch — and it trips up a lot of beginners:

_transition(_:)_ _only works on conditional views_ — like those created with _if_, not just views being shown/hidden with opacity.

Wrong:

Rectangle()
    .opacity(isVisible ? 1 : 0) // ❌ Not a transition — no insertion/removal
    .transition(.slide)         // Doesn’t do anything

Right:

if isVisible {
    Rectangle()
        .transition(.slide) // ✅ Now SwiftUI sees it as a new view
}

🧪 Example: Slide + Fade

You can even combine transitions:

.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .opacity))

This lets you say:

💡 SwiftUI gives you .opacity, .scale, .slide, and .move(edge:) out of the box — or you can define custom transitions for advanced use cases.

🧩 Putting It All Together

struct TransitionExampleView: View {
    @State private var showCard = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Toggle Card") {
                withAnimation(.easeInOut(duration: 0.4)) {
                    showCard.toggle()
                }
            }

            if showCard {
                RoundedRectangle(cornerRadius: 16)
                    .fill(.purple)
                    .frame(width: 200, height: 120)
                    .transition(.move(edge: .bottom).combined(with: .opacity))
            }
        }
        .padding()
    }
}

Tapping the button causes the card to slide in from the bottom and fade in, and do the reverse when toggled off.

✅ Quick Tips for transition(_:):

📌 4. Try This Yourself: A SwiftUI Animation Playground 🎮

Alright, enough theory — time to get your hands on the code. SwiftUI animations are best understood when you feel them. So here’s a simple playground to start experimenting with what you’ve learned.

🧪 Example: A Bouncing Ball

struct BouncingBallView: View {
    @State private var moveDown = false

    var body: some View {
        VStack {
            Spacer()

            Circle()
                .fill(.orange)
                .frame(width: 80, height: 80)
                .offset(y: moveDown ? 300 : 0)
                .animation(.interpolatingSpring(stiffness: 120, damping: 8), value: moveDown)

            Spacer()

            Button("Bounce!") {
                moveDown.toggle()
            }
        }
        .padding()
    }
}

🧠 What Does That Animation Line Actually Do?

Let’s zoom in on this line:

.animation(.interpolatingSpring(stiffness: 120, damping: 8), value: moveDown)

This line tells SwiftUI:

“Whenever the _moveDown_ value changes, animate this view’s _offset_ using a spring-based animation.”

Let’s break it down:

🔹 .animation(...)

This attaches an animation to the view, telling SwiftUI to animate it whenever the **value:** changes — in this case, moveDown.

Without this, the circle would just jump to the new position.

🔹 .interpolatingSpring(...)

This is where the real magic happens.

It’s a spring animation — like attaching the circle to a rubber band and letting it snap into place. But unlike the older .spring() modifier, this one gives you fine control through:

Together, this combo gives the ball a clean, lively movement — not too bouncy, not too rigid.

🎯 Your Challenge (Optional, But Fun)

Try modifying the animation! See how it feels when you:

🛠️ Dev Tip:

Use Canvas Preview or Live Previews in Xcode. SwiftUI is designed to give instant feedback — and this is where it shines. No need to build and run. Just tweak the code and watch it bounce.

🏁 Wrap Up: Your First Steps into SwiftUI Animations

Animations in SwiftUI aren’t just for making things look fancy — they’re a core part of how your UI communicates intent. And as you’ve seen, they’re incredibly powerful once you understand how they work:

withAnimation wraps your state changes in an animation transaction ✅ .animation(..., value:) ties changes to specific values for implicit animations ✅ transition(_:) controls how views enter and exit ✅ And spring animations give your interface that natural, fluid feel users love

You’re no longer just flipping switches and watching boxes appear — you’re coordinating motion in a declarative, elegant way.

🎯 What You Can Do Next:

Before jumping to advanced animation APIs like matchedGeometryEffect, TimelineView, or gesture-based animations:

All of that builds on the same foundation you’ve now laid brick by brick.

📌 Next:

Our next article will dive into **advanced SwiftUI animations**, covering topics like:

It’s going to be a wild ride — but now you’ve got the skills to handle it.

**Advanced Animations in SwiftUI: matchedGeometryEffect, TimelineView, PhaseAnimator & Beyond (2025…** _In this deep dive, we’re exploring SwiftUI’s most advanced animation tools — from layout-matching transitions and…_medium.comhttps://medium.com/swift-pal/advanced-animations-in-swiftui-matchedgeometryeffect-timelineview-phaseanimator-beyond-2025-da8876b7b0b9

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading