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.
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'sText("Detail")content inside theNavigationLink.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.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
sourceIDscoped to aNamespace. The example declares@Namespace private var nsand passes the id"hero"together within: nsso the transition and its source share the same identity.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 theNavigationLinkregisters it as the element the AnyNavigationTransition zooms from, using the same"hero"id andnsnamespace as the destination.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 theNavigationLinkpresents or dismisses itsDetaildestination.
.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.
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()
}
}
}