TechnologiesSwiftUINavigation

AnyNavigationTransition struct

iOSmacOStvOSwatchOSvisionOSiOS 27.0+✓ renders

A type-erasing navigation transition that allows for providing any

How it works

AnyNavigationTransition is a type-erased wrapper that lets you store and pass around any navigation transition as a single, uniform value. When you apply a custom animation to a push or pop, the concrete transition types differ, so AnyNavigationTransition gives the navigationTransition(_:) modifier a common currency it can accept without exposing the underlying type. Reach for it when you want to choose a transition dynamically, hold one in a property, or supply a built-in style like a zoom without committing to a specific transition struct.

  1. Apply a transition with navigationTransition(_:)

    The view modifier that consumes the transition takes an AnyNavigationTransition value and drives how the destination animates as it pushes and pops. Here .navigationTransition(.zoom(sourceID: "hero", in: ns)) attaches the transition to the destination's Text("Detail") content inside the NavigationLink.

  2. Build a value with the .zoom factory

    Static factory methods produce concrete transitions already erased to AnyNavigationTransition, so you write .zoom(...) rather than instantiating a type directly. The .zoom(sourceID:in:) form creates a transition that grows the destination out of, and shrinks it back into, a matched source element.

  3. Identify the animating element with sourceID and a Namespace

    The zoom transition needs to know which on-screen element it expands from, identified by a sourceID scoped to a Namespace. The example declares @Namespace private var ns and passes the id "hero" together with in: ns so the transition and its source share the same identity.

  4. Mark the originating view with matchedTransitionSource(id:in:)

    The source side of the zoom is tagged so SwiftUI can pair it with the destination's transition during navigation. Applying .matchedTransitionSource(id: "hero", in: ns) to the NavigationLink registers it as the element the AnyNavigationTransition zooms from, using the same "hero" id and ns namespace as the destination.

  5. Drive it through NavigationStack

    Navigation transitions take effect during stack-based pushes and pops, so the whole interaction is hosted in a NavigationStack. The AnyNavigationTransition you supply governs the animation each time the NavigationLink presents or dismisses its Detail destination.

Try it — Change .navigationTransition(.zoom(sourceID: "hero", in: ns)) so its sourceID no longer matches the "hero" id on matchedTransitionSource and watch the zoom fall back to the default slide, revealing how AnyNavigationTransition relies on a paired source.

Example & preview

Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.

AnyNavigationTransition.swift
struct AnyNavigationTransitionDemo: View {
    @Namespace private var ns
    var body: some View {
        NavigationStack {
            NavigationLink("Show Detail") {
                Text("Detail")
                    .font(.largeTitle)
                    .navigationTransition(.zoom(sourceID: "hero", in: ns))
            }
            .matchedTransitionSource(id: "hero", in: ns)
            .padding()
        }
    }
}
Live preview
Show Detail 9:41
Show Detail 9:41
swift → lexer → parser → sema → uiir → canvas Open in Studio ↗
What's new in SwiftUI 27 →