All articles
iOS20 min read

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 timeline-based rendering…

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

🎬 Introduction: Animations That Feel Alive

SwiftUI makes simple animations… simple. A fade here, a slide there, and boom — your app feels a little more polished.

But what if your animation needs to:

That’s where SwiftUI’s advanced animation tools come in — and where most developers either get stuck or give up.

This article is here to bridge that gap.

🔍 What You’ll Learn

In this detailed guide, we’ll explore:

This isn’t just copy-paste code with a fade and a prayer. Every section explains not just how something works — but why it works, what each parameter means, and how to use it to build delightful, high-performance animations.

🧠 Who This Is For

This guide is for iOS developers who:

Whether you’re building a multi-screen app, crafting onboarding flows, or trying to make a list expand without flickering, this guide will give you the tools and confidence to do it right.

Let’s begin with the effect that feels like UI magic: matchedGeometryEffect.

🧩 1: matchedGeometryEffect — Animate Across Views Like a Pro

Let’s start with one of the most jaw-dropping SwiftUI animation tools: **matchedGeometryEffect**

If you’ve ever seen a card animate from a list into a full-screen detail view — like it’s teleporting without blinking — you’ve seen this in action.

🧠 What is matchedGeometryEffect?

matchedGeometryEffect allows two views in different places to be treated as one continuous visual identity.

When SwiftUI sees two views sharing the same ID and namespace, it animates between their frames, positions, and even shape — making layout transitions seamless.

🧪 Real Example: Card Expand from Grid to Detail

Let’s say you have a grid of cards. Tapping one should expand it into a larger, detailed view — but animated like it’s the same element.

Here’s how to do that:

struct CardAnimationView: View {
    @Namespace private var animationNamespace
    @State private var selectedCard: Int?

    var body: some View {
        ZStack {
            if selectedCard == nil {
                // Grid view
                VStack(spacing: 20) {
                    ForEach(0..<3) { index in
                        RoundedRectangle(cornerRadius: 12)
                            .fill(.blue)
                            .frame(height: 100)
                            .matchedGeometryEffect(id: index, in: animationNamespace)
                            .onTapGesture {
                                withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                                    selectedCard = index
                                }
                            }
                    }
                }
                .padding()
            } else {
                // Detail view
                RoundedRectangle(cornerRadius: 24)
                    .fill(.blue)
                    .matchedGeometryEffect(id: selectedCard!, in: animationNamespace)
                    .frame(maxWidth: .infinity, maxHeight: 400)
                    .padding()
                    .onTapGesture {
                        withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                            selectedCard = nil
                        }
                    }
            }
        }
    }
}

🔍 Let’s Break It Down

@Namespace private var animationNamespace This declares a shared animation space. Think of it as a visual playground — any views using the same namespace and same id can morph into each other.

💡 Without a shared _@Namespace_, SwiftUI has no idea those two views are connected.

.matchedGeometryEffect(id: index, in: animationNamespace) This tells SwiftUI:

“This view’s geometry is part of an animatable link identified by _index_, inside the _animationNamespace_.”

When you tap the card:

  1. The original view disappears (grid)
  2. The destination view appears (detail)
  3. SwiftUI animates between their frames, cornerRadius, and position

It’s not animating the view — it’s animating the difference in layout.

withAnimation(.spring(...))

Used to wrap the state change (selectedCard = index) so SwiftUI knows to animate the layout change.

Here’s what the parameters mean:

.spring(response: 0.5, dampingFraction: 0.8)

This combo gives us a natural, slightly soft effect — good for UI cards.

⚠️ Common Mistakes to Avoid

🎨 Bonus: Animate Other Properties

You can also animate:

📦 Use Cases:

Coming up next: let’s explore TimelineView, SwiftUI’s animation engine for time-based rendering — perfect for clocks, live data, and animations that don’t rely on state changes.

⏳ 2: TimelineView — Animating with Time Instead of State

Most SwiftUI animations are triggered by a state change — you toggle a variable, SwiftUI animates something in response. But what if you want to animate something continuously over time?

Enter TimelineView.

It gives you a persistent, clock-driven update loop — perfect for:

🧠 What is TimelineView?

TimelineView lets you refresh a view on a schedule, instead of waiting for a state change. It’s essentially SwiftUI’s version of a “render loop.”

You choose how often it should refresh — every second, every frame, or at custom intervals — and SwiftUI will invalidate and redraw the body accordingly.

🧪 Example: Analog Clock Using TimelineView

struct ClockView: View {
    var body: some View {
        TimelineView(.animation) { context in
            let date = context.date
            let calendar = Calendar.current

            let seconds = Double(calendar.component(.second, from: date))
            let angle = Angle.degrees(seconds * 6)

            ZStack {
                Circle()
                    .stroke(lineWidth: 2)
                    .frame(width: 150, height: 150)

                Rectangle()
                    .fill(Color.red)
                    .frame(width: 2, height: 70)
                    .offset(y: -35)
                    .rotationEffect(angle)
            }
        }
    }
}

🔍 Code Breakdown

TimelineView(.animation)

This tells SwiftUI to refresh the view in sync with animation frames — typically ~60 times per second, depending on device.

Other modes you can use:

context.date

This gives you the current date/time at the moment of refresh, injected by SwiftUI.

So every frame, context.date gets updated automatically — no state or timer needed.

💡 This is the key difference: you’re not observing state — SwiftUI gives you time itself.

Angle.degrees(seconds * 6)

Since each second is 6 degrees (360° / 60), we use this to rotate the second hand on the clock.

The view updates every animation frame, so it’s buttery smooth and stays in sync with the system clock.

📌 Real-World Use Cases

⚠️ Best Practices

🧠 Bonus: Frame-Tied Animations

Want to build something like a progress ring that fills smoothly over time? TimelineView + Canvas + AnimatableData = full control over animation without relying on state.

We’ll explore that kind of setup later when we dive into custom drawing and AnimatableModifier.

Coming up next: the under-appreciated but incredibly elegant **PhaseAnimator** — introduced in iOS 17, it lets you animate through multiple phases with clarity and composability.

🎭 3: PhaseAnimator — Multi-Stage Animation Made Elegant (iOS 17+)

Most SwiftUI animations handle a change between two states: on to off, visible to hidden, 0 to 100.

But what if you want an animation to:

That’s what PhaseAnimator is built for — animating over a sequence of predefined states, with smooth transitions between them.

🧠 What is PhaseAnimator?

PhaseAnimator is a declarative container introduced in iOS 17 that lets you:

Think of it like a SwiftUI-friendly keyframe animator 🧑‍🎨

🧪 Example: Pulsing Circle with Color & Size Transitions

struct PulsingCircleView: View {
    let phases = [0.8, 1.0, 1.2]

    var body: some View {
        PhaseAnimator(phases) { phase in
            Circle()
                .fill(phase > 1.0 ? .purple : .blue)
                .scaleEffect(phase)
                .frame(width: 100, height: 100)
        } animation: { _ in
            .easeInOut(duration: 0.6)
        }
    }
}

🔍 Code Breakdown

phases = [0.8, 1.0, 1.2] This is the array of values the animation will cycle through. These become “animation phases”. In this case, we’re using them to change the scale of the circle (from smaller to larger and back).

You can use:

PhaseAnimator(phases) { phase in ... } This is the view being animated per phase.

On each animation frame, phase will be one of the values in the array. SwiftUI interpolates from one phase to the next using the animation you define.

In our view:

.animation { _ in .easeInOut(duration: 0.6) } This defines how the transition between phases happens — here we use a simple ease in/out curve, defining each phase to be of 0.6 seconds.

You can also:

🔁 How It Actually Animates

PhaseAnimator automatically:

You don’t need to manage timers or state — SwiftUI handles the sequencing for you.

🧩 Real-World Use Cases

⚠️ Gotchas

Coming up next: we’ll go deeper into SwiftUI’s most powerful customization tool — **AnimatableModifier** — which lets you animate literally anything, including your own properties.

🔧 4: Custom Animations with AnimatableModifier & AnimatableData

SwiftUI gives us many built-in animation modifiers (.opacity, .scaleEffect, .rotationEffect, etc.). But what if you want to animate:

That’s where AnimatableModifier comes in — it lets you define exactly what gets interpolated and how.

🧠 What Is AnimatableModifier?

AnimatableModifier is a protocol you can use to:

It works by leveraging a special property called animatableData.

🧪 Example: Animating a Counting Number Label

Let’s animate a number counting smoothly from 0 to 100.

struct CountingNumberModifier: AnimatableModifier {
    var number: Double

    var animatableData: Double {
        get { number }
        set { number = newValue }
    }

    func body(content: Content) -> some View {
        content
            .overlay(
                Text(String(format: "%.0f", number))
                    .font(.largeTitle)
                    .bold()
            )
    }
}

extension View {
    func counting(to number: Double) -> some View {
        self.modifier(CountingNumberModifier(number: number))
    }
}

And here’s how you’d use it:

struct CountingExampleView: View {
    @State private var target = 0.0

    var body: some View {
        VStack(spacing: 20) {
            Color.clear
                .frame(height: 100)
                .counting(to: target)

            Button("Count to 100") {
                withAnimation(.easeInOut(duration: 2.0)) {
                    target = 100
                }
            }
        }
        .padding()
    }
}

🔍 Code Breakdown

AnimatableModifier A protocol that lets SwiftUI animate a value you define, not just built-in view properties.

var animatableData: Double This is the magic property. SwiftUI animates this value from the old to the new, and re-renders the view as it updates.

In our case, the value is number, and SwiftUI interpolates it from 0 to 100.

You can also animate _multiple values_ using a tuple like _(CGFloat, CGFloat)_ if needed.

Text(String(format: "%.0f", number)) This updates on every frame with the new interpolated number — visually giving us the counting animation.

📌 Use Cases

⚠️ Gotchas

💡 Pro Tip

Use this together with a Shape to animate custom graphics — like wavy paths, gauges, or dynamically changing SVG-style visuals.

✋ 5: Gesture-Driven Animations & Spring Physics

Animations don’t just happen when the user taps a button. Sometimes, you want views to react to swipes, drags, long presses, or gestures — and spring back or fling offscreen in a natural, delightful way.

This section covers:

🧪 Example: Draggable Card That Snaps Back Into Place

struct DraggableCardView: View {
    @State private var offset: CGSize = .zero

    var body: some View {
        VStack {
            Spacer()

            RoundedRectangle(cornerRadius: 16)
                .fill(.blue)
                .frame(width: 200, height: 120)
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged { gesture in
                            offset = gesture.translation
                        }
                        .onEnded { _ in
                            withAnimation(.interpolatingSpring(stiffness: 300, damping: 20)) {
                                offset = .zero
                            }
                        }
                )

            Spacer()
        }
        .padding()
    }
}

🔍 Code Breakdown

DragGesture() Detects user dragging input. You get .onChanged and .onEnded callbacks:

withAnimation(.interpolatingSpring(...)) Explained here 👉🏻 SwiftUI Animations for Beginners: Learn with Simple Examples (2025 Edition)

**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…_medium.comhttps://medium.com/swift-pal/swiftui-animations-for-beginners-learn-with-simple-examples-2025-edition-76eb224eb633

🧩 Variations You Can Try

📌 Real-World Use Cases

⚠️ Common Gotchas

Next up: let’s wrap up with everything new and notable in iOS 17 and iOS 18 animation APIs — including the long-awaited withAnimation completion and the animation debugging tools in Xcode 26.

📦 6: What’s New in SwiftUI Animations (iOS 17 → iOS 26)

Over the past few years, SwiftUI’s animation system has evolved from basic and mysterious… to powerful, predictable, and measurable.

From iOS 17’s **withAnimation** completion to iOS 26’s timeline-based debugging tools, SwiftUI has grown into a full-blown animation engine that rivals UIKit — and in many ways, surpasses it.

Let’s look at what’s improved in recent versions:

✅ iOS 17 & 18 (The Foundation of Control)

Although iOS 17 is now two versions behind, it brought essential improvements that still power most of today’s animations:

🆕 withAnimation(..., completion:) — iOS 17

You can now run logic after an animation ends:

withAnimation(.easeInOut(duration: 1.0)) {
    showBox = true
} completion: {
    print("Animation done!")
}

🆕 PhaseAnimator — iOS 17

Introduced a native way to:

Still one of the cleanest ways to build breathing UIs or onboarding steps.

✅ iOS 26 (Latest in Beta) — Animation Debugging & Refinement

Apple finally gave us developer tooling to go with all that power:

🧪 SwiftUI Animation Timeline in Xcode 26 / iOS 26

💡 Why it matters:

Until now, animations were a “feel-it-out” black box. With iOS 26, you can measure, optimize, and ship animations that feel great and perform great.

✅ 7: Tips, Pitfalls & Best Practices

Whether you’re animating a subtle pulse or a full-screen transition, these best practices can help keep your animations smooth, stable, and production-ready.

🧠 DO: Use withAnimation for State Changes

Use withAnimation {} when changing any @State, @Binding, or @ObservedObject that affects the layout or visuals.

withAnimation(.spring()) {
    isExpanded.toggle()
}

🔍 Without it, SwiftUI may skip animating changes — or animate the wrong thing.

🙅‍♂️ DON’T: Stack Too Many .animation() Modifiers

// ❌ Anti-pattern
.opacity(isVisible ? 1 : 0)
.animation(.easeInOut(duration: 0.5), value: isVisible)
.scaleEffect(isVisible ? 1.0 : 0.5)
.animation(.spring(), value: isVisible)

⚠️ This can lead to conflicting animations and unexpected jank. Instead, group animations with a single withAnimation {} or combine into a container view.

🧠 DO: Use matchedGeometryEffect for View Transitions

When transitioning a view from one screen to another, matchedGeometryEffect:

Use a shared **@Namespace** and same ID on both views.

🙅‍♂️ DON’T: Animate Layout Directly with .frame

Animating .frame(width: height:) often causes layout conflicts and poor performance.

✅ Prefer .scaleEffect, .offset, .padding, or .transformEffect — these modify visuals, not layout.

🧠 DO: Use .interpolatingSpring() for Natural Physics

withAnimation(.interpolatingSpring(stiffness: 200, damping: 25)) {
    offset = .zero
}

his gives you control over:

It’s perfect for interactive gestures, drag-release, and material-style movement.

🙅‍♂️ DON’T: Forget About Accessibility

Animations should never get in the way of usability.

✅ Respect Reduce Motion settings:

@Environment(\.accessibilityReduceMotion) var reduceMotion

withAnimation(reduceMotion ? nil : .easeInOut) {
    isVisible.toggle()
}

🧠 DO: Test on Real Devices & Slow Connections

Animations can behave differently:

Use Xcode Instruments > Animation Timeline (iOS 26+) to detect frame drops and test different rendering loads.

✅ Bonus Tips

🎉 That’s a Wrap!

You now have the full toolbox — from withAnimation to TimelineView, from gesture-driven springs to frame-by-frame custom modifiers. Whether you’re animating a login screen, onboarding flow, or an AR view tracker — SwiftUI has your back.

🏁 Wrapping Up: Animation is Code That Feels

Animations are more than decoration — they’re how we communicate intention, guide attention, and make interfaces feel alive.

In this guide, you learned not just how to animate in SwiftUI — but how to think about animation like an engineer:

All while understanding why each animation works, what each parameter does, and how it fits into SwiftUI’s rendering system.

No magic, no voodoo — just a fully unpacked animation engine at your fingertips.

📚 What’s Next?

**The Ultimate iOS Article Index by Karan Pal 🚀** _A growing collection of all my Swift and iOS development articles — neatly organized and updated regularly._medium.comhttps://medium.com/swift-pal/the-ultimate-ios-article-index-by-karan-pal-eb4fb5a42caf

Ready to go beyond UIKit-style transitions and into the world of SwiftUI wizardry? Coming up next in the SwiftUI Mastery Series:

🔗 Mastering the SwiftUI Lifecycle

Before you go multiplatform, make sure you’ve nailed down how SwiftUI handles app and view lifecycle events — because those fundamentals are exactly what power adaptive behavior across platforms.

📖 If you haven’t read it yet, check out: 👉 SwiftUI Lifecycle: How SwiftUI & iOS App Lifecycle Really Work

You’ll learn:

Understanding lifecycle = understanding how your app behaves on every platform.

🎉 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