Skip to content

View Transitions

A primer on animating the appearing and disappearing of views using transitions in SwiftUI.

  1. When a value change causes a View to appear or disappear (it is inserted into or removed from the view hierarchy), ...
  2. and an animation is applied to the change with withAnimation or .animation (see Animating value changes), ...
  3. by default, SwiftUI animates that change with an opacity transition (fade in and out):

swift
// » SwiftUI Garden
// » https://swiftui-garden.com/Animations/View-Transitions

import SwiftUI

struct DefaultTransitionExample: View {
    @State var showShape = false

    var body: some View {
        VStack {
            Toggle("Show shape", isOn: $showShape)
                .padding()

            ZStack {
                if showShape {
                    RoundedRectangle(cornerRadius: 20)
                        .fill(.blue)
                        .frame(width: 200, height: 100)
                }
            }
            .frame(height: 200)
            .animation(.easeInOut(duration: 0.5), value: showShape)
        }
    }
}

#Preview {
    DefaultTransitionExample()
}

The .transition modifier allows to configure the transition effect. It is applied to the element that is inserted or removed. Without an animation for the triggering value change, this modifier is without effect. Example using .transition(.blurReplace):

swift
// » SwiftUI Garden
// » https://swiftui-garden.com/Animations/View-Transitions

import SwiftUI

struct ViewTransitionExample: View {
    @State var shapeVisible = false

    var body: some View {
        VStack {
            Toggle("Show shape", isOn: $shapeVisible)

            ZStack {
                if shapeVisible {
                    Capsule()
                        .fill(.blue)
                        .frame(width: 150, height: 60)
                        .transition(.blurReplace)
                }
            }
            .frame(height: 200)
            .animation(.default, value: self.shapeVisible)
        }
        .padding()
    }
}

#Preview {
    ViewTransitionExample()
}

TIP

Recommended structure: .transition() as last modifier in the if-condition block and a dedicated container around for the .animation().

Triggering transitions with changed view identity

View identity .id() can be used to trigger transitions. If the id of a View changes, SwiftUI will see this as "old View removed, new View inserted" and if you change the id together with an animation, a transition will be applied. In this example, the old View is removed and the new View is inserted using the push effect:

swift
// » SwiftUI Garden
// » https://swiftui-garden.com/Animations/View-Transitions

import SwiftUI

struct ViewTransitionByIdExample: View {
    @State var slide = 1

    var body: some View {
        VStack {
            Button("Next slide") {
                self.slide += 1
            }

            ZStack {
                Capsule()
                    .fill((slide % 2 == 0) ? .blue : .red)
                    .overlay {
                        Text("\(slide)")
                    }
                    .frame(width: 150, height: 60)
                    .id(slide)
                    .transition(.push(from: .leading))
            }
            .frame(height: 200)
            .animation(.default, value: self.slide)
        }
        .padding()
    }
}

#Preview {
    ViewTransitionByIdExample()
}

Transition playground

Explore the built-in transitions:

swift
// » SwiftUI Garden
// » https://swiftui-garden.com/Animations/View-Transitions

import SwiftUI

struct TransitionPlayground: View {
    @State private var showItem = false
    @State private var selectedTransition = TransitionType.opacity

    enum TransitionType: String, CaseIterable {
        case opacity = "Opacity (Default)"

        case slide = "Slide"
        case move = "Move (Leading)"
        case push = "Push"

        case scale = "Scale"
        case blurReplace = "Blur Replace"

        case asymmetric = "Asymmetric"
        case combined = "Combined"

        @ViewBuilder func apply(to view: some View) -> some View {
            switch self {
            case .opacity:
                view.transition(.opacity)
            case .slide:
                view.transition(.slide)
            case .scale:
                view.transition(.scale)
            case .move:
                view.transition(.move(edge: .leading))
            case .asymmetric:
                view.transition(.asymmetric(
                    insertion: .move(edge: .leading).combined(with: .opacity),
                    removal: .scale.combined(with: .opacity)
                ))
            case .combined:
                view.transition(.scale.combined(with: .opacity))
            case .blurReplace:
                view.transition(.blurReplace)
            case .push:
                view.transition(.push(from: .leading))
            }
        }
    }

    var body: some View {
        VStack {
            HStack {
                Picker("Transition Type", selection: $selectedTransition) {
                    ForEach(TransitionType.allCases, id: \.self) { type in
                        Text(type.rawValue).tag(type)
                    }
                }
                .buttonStyle(.bordered)
                .pickerStyle(.menu)

                Button("Toggle View") {
                    showItem.toggle()
                }
                .buttonStyle(.borderedProminent)
            }

            ZStack {
                if showItem {
                    selectedTransition.apply(to:
                        RoundedRectangle(cornerRadius: 10)
                            .fill(Color.blue)
                            .frame(width: 200, height: 60)
                            .overlay(
                                Text("Transitioning View")
                                    .foregroundColor(.white)
                            )
                    )
                }
            }
            .frame(height: 150)
            .animation(.easeInOut(duration: 0.5), value: showItem)
        }
    }
}

#Preview {
    TransitionPlayground()
}

Custom transitions

Custom transitions can be created by implementing the Transition protocol:

swift
// » SwiftUI Garden
// » https://swiftui-garden.com/Animations/View-Transitions

import SwiftUI

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

    var body: some View {
        VStack {
            Button("Toggle Card") {
                showCard.toggle()
            }

            ZStack {
                if showCard {
                    RotationSparklesCard()
                        .transition(Rotate3DTransition())
                }
            }
            .frame(height: 200)
            .animation(.spring, value: showCard)
        }
    }
}

struct Rotate3DTransition: Transition {
    func body(content: Content, phase: TransitionPhase) -> some View {
        let progress = phase.isIdentity ? 1.0 : 0.0
        content
            .scaleEffect(progress)
            .opacity(progress)
            .blur(radius: (1 - progress) * 10)
            .rotation3DEffect(
                .degrees((1 - progress) * 180),
                axis: (x: 1, y: 0, z: 0),
                anchor: .center,
                perspective: 0.5
            )
    }
}

struct RotationSparklesCard: View {
    var body: some View {
        RoundedRectangle(cornerRadius: 15)
            .fill(
                LinearGradient(
                    colors: [.purple, .indigo],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
            )
            .frame(width: 250, height: 150)
            .overlay(
                VStack {
                    Image(systemName: "sparkles")
                        .font(.largeTitle)
                    Text(".rotate3D transition")
                        .font(.headline)
                }
                .foregroundColor(.white)
            )
    }
}

#Preview {
    CustomTransitionExample()
}