Navigation transition updates in SwiftUI on iOS 27
The NavigationTransition protocol has been available in SwiftUI since iOS 18, letting us control how views animate when pushed onto a NavigationStack and when presenting sheets and full-screen covers. We specify the transition using the navigationTransition(_:) modifier on the destination or presented view, and SwiftUI uses it instead of the default animation for that context. Before iOS 27, SwiftUI provided two built-in conforming types: AutomaticNavigationTransition, used via the automatic static value, which defers to the system default for the current context, and ZoomNavigationTransition, used via zoom(sourceID:in:), which animates the presented view expanding from a source view marked with matchedTransitionSource(). iOS 27 introduces CrossFadeNavigationTransition, a new built-in transition that cross-fades between views without requiring a source, and adds AnyNavigationTransition, a type eraser that lets us select a transition at runtime.
# CrossFadeNavigationTransition
Without a navigationTransition(_:) modifier, SwiftUI uses the automatic transition, which on a sheet produces the standard slide-up animation. The crossFade static value replaces that with a fade transition as the sheet appears over the content.
We can use it by passing crossFade to navigationTransition(_:) on the sheet or full-screen cover content.
struct LandmarkView: View {
@State private var showInfo = false
var body: some View {
LandmarkPhoto()
.onTapGesture {
showInfo = true
}
.sheet(isPresented: $showInfo) {
LandmarkInfo()
.presentationDetents([.medium])
.navigationTransition(.crossFade)
}
}
}
# AnyNavigationTransition
The navigationTransition(_:) modifier accepts some NavigationTransition, which means we need to supply a single concrete type at the call site. This becomes a limitation when the same view can be reached in more than one way and we want to select the transition dynamically, for example based on whether the presentation was triggered by a user interaction or opened programmatically from a deep link or notification.
AnyNavigationTransition solves this by type-erasing any NavigationTransition value into a single concrete type that we can store in a property or pass as a parameter. We construct it by wrapping any conforming value in AnyNavigationTransition(_:).
In the example below, LandmarkInfo accepts a useCrossFade boolean and uses AnyNavigationTransition to set the appropriate transition.
struct LandmarkInfo: View {
var useCrossFade: Bool
var transition: AnyNavigationTransition {
useCrossFade
? AnyNavigationTransition(.crossFade)
: AnyNavigationTransition(.automatic)
}
var body: some View {
InfoContent()
.presentationDetents([.medium])
.navigationTransition(transition)
}
}
With CrossFadeNavigationTransition and AnyNavigationTransition, iOS 27 expands both the range of available presentation animations and the flexibility with which we can apply them. The NavigationTransition protocol now has a richer set of conforming types to choose from, and with type erasure we are no longer constrained to a fixed transition at the call site. Custom NavigationTransition conformances are still not supported, which remains something to look forward to in future releases.
If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects. And my new book The SwiftUI Way helps you adopt recommended patterns, avoid common pitfalls, and use SwiftUI's native tools appropriately to work with the framework rather than against it.
For more resources on Swift and SwiftUI, check out my other books and book bundles.
I’m currently running a WWDC 2026 promotion with 30% off my books, plus additional savings when purchased as a bundle. Visit the books page to learn more.



