How to make zoom transition animation in iOS 18

Issue #995

With iOS 18, SwiftUI introduces matchedTransitionSource and navigationtransition as a powerful new way to create zoom animations between views. This allows you to smoothly transition from a small view to a larger, more detailed view, creating a delightful user experience. This works for both navigating to a new screen or presenting a view as a sheet.

This new feature works very much like the matchedGeometryEffect, but it is simpler to use and works across different types of presentations like sheets and navigation pushes, where matchedGeometryEffect can be tricky.

Firstly, we need to create a namespace that both the starting view and the destination view can share. Think of it as a private channel that connects the two views. You create it using the @Namespace property wrapper, like this:

@Namespace private var animation
  • To set the animation’s starting point, add the .matchedTransitionSource() modifier to the original view the user interacts with.
  • To set the navigation transition style on the destination, add the .navigationTransition() modifier to the destination view that is being presented.

Below is an example where we have 2 buttons to present and push a detail view

struct MatchedTransitionSourceView: View {
    @Namespace private var animation
    
    @State private var showsSheet = false
    @State private var navigationPath = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            VStack(spacing: 50) {
                presentButton
                navigateButton
            }
            .navigationTitle("Zoom Animations")
            .navigationDestination(for: String.self) { id in
                Text("Detail View")
                     .navigationTransition(.zoom(sourceID: "push", in: namespace))
            }
        }
        .sheet(isPresented: $showsSheet) {
            Text("Detail View")
                 .navigationTransition(.zoom(sourceID: "present", in: namespace))
        }
    }
    
    private var presentButton: some View {
        Button {
            showsSheet = true
        } label: {
            Image(systemName: "moon.stars.fill")
                .font(.system(size: 100))
                .foregroundStyle(.purple.gradient)
        }
        .matchedTransitionSource(id: "present", in: animation)
    }
    
    private var navigateButton: some View {
        Button {
            navigationPath.append("push")
        } label: {
            Image(systemName: "sun.max.fill")
                .font(.system(size: 100))
                .foregroundStyle(.orange.gradient)
        }
        .matchedTransitionSource(id: "push", in: animation)
    }
}
Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation