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…

🎬 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:
- Animate a view as it travels across screens?
- Stay in sync with real-world time?
- Morph shapes, animate custom paths, or react to gestures with physics?
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:
- How to animate layout transitions between screens using
matchedGeometryEffect - How to sync animations with time and frame updates using
TimelineView - How to stage multi-step animations with
PhaseAnimator(iOS 17+) - How to animate anything — shapes, colors, even text — using
AnimatableModifierandAnimatableData - How gesture-driven spring animations actually work under the hood
- What’s new in SwiftUI animations with iOS 17 & 18 — including the long-awaited
withAnimationcompletion handler
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:
- Already know
withAnimationand.transition() - Are ready to explore layout continuity, time-based motion, and gesture-driven dynamics
- Want to understand SwiftUI animations like a system — not a mystery
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:
- The original view disappears (grid)
- The destination view appears (detail)
- 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)- response: How fast the spring responds (lower = snappier)
- dampingFraction: How much it resists bouncing (1 = no bounce, 0 = very bouncy)
This combo gives us a natural, slightly soft effect — good for UI cards.
⚠️ Common Mistakes to Avoid
- ❌ Views must be in different branches of the view hierarchy (like grid vs detail), not just conditionally shown
- ❌ Views must have the same type (e.g.
RoundedRectangle → RoundedRectangle) - ❌ You must use the same ID and
@Namespaceon both views - ❌ Don’t try to add
.animation()—matchedGeometryEffecthandles the animation internally
🎨 Bonus: Animate Other Properties
You can also animate:
- corner radius
- size and position
- font size and layout
- padding
- even overlays and backgrounds (to some extent)
📦 Use Cases:
- Grid → detail transitions
- Tab view swaps
- Avatar zoom-in on profile tap
- Interactive carousels
- List → modal animations
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:
- Countdowns & timers
- Clock animations
- Progress spinners
- Real-time graphs
- Breathing effects or pulsating visuals
🧠 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:
.periodic(from:startDate, by:interval)→ refresh at custom intervals (e.g. every 1 sec).animation→ frame-by-frame updates (great for smooth motion)
✅ 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
- Analog/digital clocks
- Countdown rings
- Live sensor visualizations (e.g. health/fitness metrics)
- Progress bars for timed tasks
- Breathing animations (inhale/exhale loops)
⚠️ Best Practices
- ❗ Use
.animationonly if you truly need per-frame updates (it’s more demanding) - ✅ Use
.periodicfor slower refreshes (e.g..periodic(from: now, by: 1)for 1-second intervals) - ✅ Combine with
Canvasif you want low-level drawing - ✅ Avoid unnecessary computation inside the loop — SwiftUI is redrawing every tick!
🧠 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:
- Pulse through multiple stages?
- Change color in a loop?
- Bounce between sizes repeatedly?
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:
- Define multiple phases (e.g.
.first,.second,.third) - Apply animations between those phases
- Build UI that changes gradually, visually, and loopably
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:
- Numbers (like here)
- Enums (e.g.,
.start,.middle,.end) - Any
Hashabletype
✅ 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:
- The circle’s scale is set to
phase - Its color changes depending on the value
✅ .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:
- Use
.spring(...)for a bouncing effect - Add delays or chained timing curves (by nesting PhaseAnimators or using
DispatchQueue)
🔁 How It Actually Animates
PhaseAnimator automatically:
- Animates from one phase to the next in order
- Loops back to the beginning when it hits the end
- Animates using the defined
.animation
You don’t need to manage timers or state — SwiftUI handles the sequencing for you.
🧩 Real-World Use Cases
- Pulsing recording indicators 🔴
- Shimmer effects
- Animated onboarding sequences
- Keyframe-like animations (progressive state transitions)
- Emojis that bounce between expressions 😄➡️😐➡️😮
⚠️ Gotchas
- Only available on iOS 17 and above
- Needs unique
**Hashable**values for phases - Don’t overload it with state-driven logic — keep it declarative
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:
- A number counting from 0 to 100?
- A wave shape changing in amplitude?
- A ring chart that fills up based on progress?
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:
- Wrap a view in custom animation logic
- Define which value should interpolate
- Tell SwiftUI how to visually reflect the animation progress
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
- Counting labels
- Custom loading indicators
- Animated pie charts, ring graphs
- Path morphing (via custom
Shape+ animatable data) - Animating any number or geometry property not handled by default SwiftUI
⚠️ Gotchas
- SwiftUI calls
**body()**for every interpolated frame, so avoid heavy computations inside it animatableDatamust conform toVectorArithmetic(most numbers, tuples do)- You must use
**withAnimation**or apply.animation(..., value:)to trigger the interpolation
💡 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:
- How to animate views based on gestures
- What the spring parameters mean
- Why SwiftUI’s physics feel so… real
🧪 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:
gesture.translationis the current drag offset- You store it in
@State var offsetso the view moves live with the finger
✅ 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
- Change
.offsetto.scaleEffectfor pinch/bounce animations - Use
.rotationEffectwithDragGesturefor flick/spin effects - Combine gestures and
.gesture(DragGesture().simultaneously(with: TapGesture())) - Add thresholds: fling the view offscreen if drag exceeds 150pt
📌 Real-World Use Cases
- Cards, panels, and modals
- Scrollable carousels with snap points
- Drag-to-dismiss interactions
- Physics-based pull-to-refresh
⚠️ Common Gotchas
- Don’t forget to wrap your
.onEndedanimation inwithAnimation, or the view will teleport back to its original state. - Avoid animating layout (like
.frame) during gestures — use.offsetor.transformEffect - Don’t over-bounce! High spring energy with low damping can make your UI feel… haunted 👻
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!")
}- Cleanly chain animations or trigger follow-up actions
- No more async delays or janky workarounds
- Works on iOS 17+ only (check availability if supporting older devices)
🆕 PhaseAnimator — iOS 17
Introduced a native way to:
- Animate through multiple phases
- Avoid custom timers or hacks
- Loop/pulse keyframe-like animations declaratively
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
- Profile how long each animated view takes to redraw
- Detect frame drops and bottlenecks
- Essential for debugging
.matchedGeometryEffect, large view trees, or interactive transitions
💡 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:
- Preserves layout
- Animates position, size, corner radius, and more
- Makes transitions feel natural and connected
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:
- Stiffness → how aggressively the animation snaps
- Damping → how bouncy or soft it feels
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:
- On slower devices
- In low-power mode
- With multiple background processes
- On high-refresh-rate displays (ProMotion!)
Use Xcode Instruments > Animation Timeline (iOS 26+) to detect frame drops and test different rendering loads.
✅ Bonus Tips
- Use
CanvaswithTimelineViewfor ultra-custom visuals - Use
AnimatableModifierfor animating any number or shape - For sequential animations, use
PhaseAnimatoror.delay()chaining - Use
transaction { ... }to coordinate animation state
🎉 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:
- You explored layout-driven transitions with
matchedGeometryEffect - Built time-based visuals with
TimelineView - Created keyframe-like flows using
PhaseAnimator - Dove into custom interpolation using
AnimatableModifier - And made your UI respond naturally with gesture-based spring physics
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:
- How SwiftUI apps launch and respond to system events (
@main,scenePhase) - View lifecycle patterns (
onAppear,.task,@State) - Why UIKit still matters under the hood — and how to bridge into it when needed
- Best practices for state restoration using
@SceneStorageand@AppStorage
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! 🚀
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
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read