swiftui · tutorial · animation

Animating View Transitions with .transition

Published March 12, 2026 · 4 min read · intermediate

Goal

By the end of this tutorial, you will build a SwiftUI panel that appears and disappears with a clean transition, and you will understand why some transitions seem to “not work”.

State it explicitly:

“Animate conditional views in and out reliably using transition and state-driven updates.”


Requirements

  • Basic SwiftUI knowledge
  • Xcode 15+
  • iOS 17+

The use case

Most SwiftUI animation confusion happens when a view is conditionally inserted with if and developers expect a modifier animation to handle everything automatically. Sometimes it works, sometimes it snaps.

The root issue is simple: transitions apply when a view enters or exits the hierarchy. If your state updates and hierarchy changes are not coordinated, transition timing feels random.

This tutorial builds the pattern in small steps so behavior stays predictable.


Step 1 - The simplest working version

Create one state flag and one conditional panel.

import SwiftUI

struct TransitionDemoView: View {
    @State private var isOpen = false

    var body: some View {
        VStack(spacing: 16) {
            Button(isOpen ? "Hide details" : "Show details") {
                withAnimation(.easeInOut(duration: 0.25)) {
                    isOpen.toggle()
                }
            }

            if isOpen {
                RoundedRectangle(cornerRadius: 16, style: .continuous)
                    .fill(.blue.opacity(0.15))
                    .frame(height: 140)
                    .overlay {
                        Text("Transition panel")
                            .font(.headline)
                    }
                    .transition(.opacity)
            }
        }
        .padding()
    }
}

What this already does correctly:

  • state controls insertion/removal
  • withAnimation wraps the hierarchy change
  • transition is attached to the conditional view

Quick verification checkpoint: click quickly multiple times and confirm fade-in and fade-out both animate.


Step 2 - Make it reusable

Extract a reusable transition container so feature screens do not repeat transition boilerplate.

struct TransitionContainer<Content: View>: View {
    let isVisible: Bool
    let transition: AnyTransition
    @ViewBuilder let content: () -> Content

    var body: some View {
        Group {
            if isVisible {
                content()
                    .transition(transition)
            }
        }
    }
}

Use it:

TransitionContainer(
    isVisible: isOpen,
    transition: .opacity.combined(with: .move(edge: .top))
) {
    RoundedRectangle(cornerRadius: 16, style: .continuous)
        .fill(.mint.opacity(0.2))
        .frame(height: 140)
        .overlay { Text("Reusable transition") }
}

What changed: transition behavior is now a reusable contract instead of inlined conditional logic.

Quick verification checkpoint: reuse TransitionContainer with a second block and a different transition.


Step 3 - SwiftUI-specific refinement

Add layout stability and predictable motion when content size changes.

struct TransitionDemoView: View {
    @State private var isOpen = false
    @State private var showExtraLine = false

    var body: some View {
        VStack(spacing: 16) {
            HStack(spacing: 12) {
                Button(isOpen ? "Hide details" : "Show details") {
                    withAnimation(.snappy(duration: 0.3)) {
                        isOpen.toggle()
                    }
                }

                Button(showExtraLine ? "Compact" : "Expand text") {
                    withAnimation(.easeInOut(duration: 0.2)) {
                        showExtraLine.toggle()
                    }
                }
                .disabled(!isOpen)
            }

            TransitionContainer(
                isVisible: isOpen,
                transition: .opacity.combined(with: .move(edge: .top))
            ) {
                VStack(alignment: .leading, spacing: 8) {
                    Text("Shipping details")
                        .font(.headline)

                    Text("Your package will be dispatched in 24 hours.")
                        .foregroundStyle(.secondary)

                    if showExtraLine {
                        Text("Express options are available at checkout.")
                            .foregroundStyle(.secondary)
                            .transition(.opacity)
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(16)
                .background(
                    RoundedRectangle(cornerRadius: 16, style: .continuous)
                        .fill(.ultraThinMaterial)
                )
            }
        }
        .padding()
    }
}

Refinement points:

  • snappy makes the main panel feel responsive
  • nested conditional text gets its own transition
  • background shape stays stable, which reduces visual jump

Quick verification checkpoint: open panel, toggle extra line repeatedly, then close panel. Motion should remain readable and not collapse abruptly.


Result

You now have a transition pattern that is:

  • state-driven
  • reusable
  • stable under content variation

Common SwiftUI pitfalls

  • Mistake: Adding .transition(...) without conditional insertion/removal
    Why it happens: expecting transition to animate static views
    How to fix it: attach transition to a view controlled by if or dynamic collection changes

  • Mistake: Placing withAnimation in unrelated scopes
    Why it happens: animation wrapped around side effects instead of hierarchy change
    How to fix it: wrap the exact state mutation that inserts/removes the view

  • Mistake: Mixing container size animation and transition without intent
    Why it happens: multiple implicit animations compete
    How to fix it: apply focused animations and keep one source of truth for visibility


When NOT to use this

If your transition depends on shared geometry between two different screens, use a matched-geometry strategy. transition is ideal for local hierarchy insertion/removal within one view tree.


Takeaway

If you remember one thing: Transitions animate hierarchy changes, not static views.

Next steps

If you want to go deeper into motion and state continuity, these are the most useful follow-ups: