Transitions in SwiftUI

DevTechie Inc
May 11, 2023

Transition happens when a view is added or removed from the view tree. SwiftUI doesn't directly supports adding or removing view but instead all of that is driven through the state change and combination of if/switch/foreach statements.

Let’s understand this with examples.

Getting Started

We will start with a simple view.

struct DevTechieTransitionsExample: View {
    var body: some View {
        NavigationStack {
            VStack {
                Text("SwiftUI")
                Text("UIKit")
                Text("iOS")
            }
            .font(.title)
            .navigationTitle("DevTechie.com")
        }
    }
}

Believe it or not but we have a transition in our view. Default transition is the fade transition. We can change transition using combination of AnyTransition and transition modifier.

There are a few types of transitions available for example, scalemoveoffsetslide, and opacity

Move transition

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.move(edge: .leading)
            VStack {
                if toggleVisibility {
                    Text("SwiftUI")
                        .transition(transition)
                    Text("UIKit")
                        .transition(transition)
                    Text("iOS")
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(.easeInOut, value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

Scale transition

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.scale
            VStack {
                if toggleVisibility {
                    Text("SwiftUI")
                        .transition(transition)
                    Text("UIKit")
                        .transition(transition)
                    Text("iOS")
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(.easeInOut, value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

Scale transition also takes the scale amount as a parameter and we can change that to another value.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.scale(scale: 2.0)
            VStack {
                if toggleVisibility {
                    Text("SwiftUI")
                        .transition(transition)
                    Text("UIKit")
                        .transition(transition)
                    Text("iOS")
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(.easeInOut, value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

We can change anchor for the scale as well. Scale takes anchor as a parameter so let’s apply anchor.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.scale(scale: 100.0, anchor: .center)
            VStack {
                if toggleVisibility {
                    Text("SwiftUI")
                        .transition(transition)
                    Text("UIKit")
                        .transition(transition)
                    Text("iOS")
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(.easeInOut, value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

We can use scale with anchor transition to build an interesting zoom-in/out transition.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.scale(scale: 100.0, anchor: .center)
            VStack {
                if toggleVisibility {
                    Image(systemName: "apple.logo")
                        .font(.largeTitle)
                        .foregroundStyle(.pink.gradient)
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(.easeInOut, value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

We can even slow down the animation by adding animation duration.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.scale(scale: 100.0, anchor: .center)
            VStack {
                if toggleVisibility {
                    Image(systemName: "apple.logo")
                        .font(.largeTitle)
                        .foregroundStyle(.pink.gradient)
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(Animation.easeInOut(duration: 2), value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

Offset transition

We can set offset for transition as well where the view will be shown/hidden from x, y or x and y directions.

Let’s try transition on x-axis with interactiveSpring animation

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.offset(x: -500)
            VStack {
                if toggleVisibility {
                    Image(systemName: "apple.logo")
                        .font(.system(size: 100))
                        .foregroundStyle(.pink.gradient)
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(Animation.interactiveSpring(dampingFraction: 0.3), value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

Let’s replace x with y axis and try that out.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.offset(y: -500)
            VStack {
                if toggleVisibility {
                    Image(systemName: "apple.logo")
                        .font(.system(size: 100))
                        .foregroundStyle(.pink.gradient)
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(Animation.interactiveSpring(dampingFraction: 0.3), value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

Let’s apply offset for both x and y axis.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.offset(x: -500, y: -500)
            VStack {
                if toggleVisibility {
                    Image(systemName: "apple.logo")
                        .font(.system(size: 100))
                        .foregroundStyle(.pink.gradient)
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(Animation.interactiveSpring(dampingFraction: 0.3), value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

Asymmetric transition

So far our view’s transition has been single sided, meaning upon removal of the view it goes in the same direction it came from. We can define different transition technique for insertion and removal with the help of asymmetric transition.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            let transition = AnyTransition.asymmetric(insertion: .move(edge: .top), removal: .opacity)
            VStack {
                if toggleVisibility {
                    Image(systemName: "apple.logo")
                        .font(.system(size: 100))
                        .foregroundStyle(.pink.gradient)
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
            }
            .font(.title)
            .animation(Animation.interactiveSpring(dampingFraction: 0.3), value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}

Combined Transition

We can use combine function with AnyTransition to add another transition on the top of existing one. This helps us build complex transitions.

struct DevTechieTransitionsExample: View {
    @State private var toggleVisibility = false
    var body: some View {
        NavigationStack {
            
            VStack {
                let transition = AnyTransition.asymmetric(insertion: .slide, removal: .move(edge: .bottom)).combined(with: .opacity)
                if toggleVisibility {
                    Image(systemName: "carrot")
                        .font(.system(size: 100))
                        .foregroundStyle(.pink.gradient)
                        .transition(transition)
                }
                Toggle("Show/Hide", isOn: $toggleVisibility)
                
                Image(systemName: "trash.fill")
                    .font(.system(size: 150))
            }
            .font(.title)
            .animation(Animation.interactiveSpring(dampingFraction: 0.3), value: toggleVisibility)
            .padding()
            .navigationTitle("DevTechie.com")
        }
    }
}