View Transitions
A primer on animating the appearing and disappearing of views using transitions in SwiftUI.
How are the .animation and .transition modifiers related?
- When a value change causes a View to appear or disappear (it is inserted into or removed from the view hierarchy), ...
- and an animation is applied to the change with withAnimation or .animation (see Animating value changes), ...
- by default, SwiftUI animates that change with an opacity transition (fade in and out):
// » 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):
// » 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:
// » 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:
// » 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:
// » 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()
}