← Back to archive

Development · SwiftUI

Animating insertion and removal in SwiftUI

Use transitions and animation to control how views enter and leave the hierarchy.

Read the Korean original on Velog ↗English translation

Development archive cover

Animating a view's position or color is different from animating its insertion or removal. Understanding that distinction also helps explain why a removal transition can sometimes appear not to run.

Animated transitions

Apply a transition to a conditionally displayed view, then animate the state change that inserts or removes it.

struct TransitionStudy: View {
    
    @State var isAnimation: Bool = false
    
    var body: some View {
        VStack {
            Button("Button") {
            	// Apply an animation
                withAnimation {
                    isAnimation.toggle()
                }
            }
            Spacer()
            if isAnimation {
                RoundedRectangle(cornerRadius: 24)
                    .frame(height: UIScreen.main.bounds.height / 2)
                    // Apply a transition
                    .transition(.slide)
            }
        }
        .edgesIgnoringSafeArea(.bottom)
    }
}

Transition types

AnyTransition provides several effects for inserting and removing views.

.transition(.opacity)
.transition(.scale)
.transition(.slide)
.transition(.move(edge: .leading))
.transition(.push(from: .leading))
.transition(.offset(y: 100))

Use asymmetric to give insertion and removal different effects.

.transition(.asymmetric(
	insertion: .move(edge: .leading), // Insertion transition
	removal: .opacity // Removal transition
))

Use combined to apply multiple transition effects together.

.transition(.opacity.combined(with: .scale))

When a removal seems not to animate

In a layered layout, the disappearing view can move behind another view and make its animation look absent. Setting an explicit zIndex can preserve the intended stacking order during removal.

struct TransitionStudy: View {
    
    @State var isAnimating = false
    
    var body: some View {
        ZStack {
            Color.green
            	.zIndex(-1) // Set the background's zIndex
                .ignoresSafeArea()
                .onTapGesture {
                    withAnimation(.easeInOut) {
                        isAnimating.toggle()
                    }
                }
            if isAnimating {
                RoundedRectangle(cornerRadius: 24)
                    .zIndex(1) // Set the foreground's zIndex
                    .transition(.move(edge: .bottom))
                    .onTapGesture {
                        withAnimation(.easeInOut) {
                            isAnimating.toggle()
                        }
                    }
            }
        }
    }
}
← Back to archive