Creating Custom Transitions
One of the most powerful features of SwiftUINavigationTransitions
is its ability to define completely custom transitions using a declarative, composable API. This guide explains the core concepts and shows you how to build your own.
Core Concepts
The library's transition system is built on two key protocols: NavigationTransitionProtocol
and AtomicTransition
.
NavigationTransitionProtocol
This is the high-level protocol that defines a complete push and pop transition between two views (a "from" view and a "to" view). Transitions like .slide
are implementations of this protocol. You typically build a NavigationTransitionProtocol
by composing smaller, single-purpose transitions.
AtomicTransition
This is the low-level building block. An AtomicTransition
defines a single, isolated animation effect for a single view, specifying how it should behave on insertion and removal. Examples include Opacity
, Scale
, and Move
. You combine these atomic transitions to create a NavigationTransitionProtocol
.
Building a Custom Transition
Let's build the swing
transition seen in the demo app. It slides the new view in, while the old view swings out with a rotation and offset.
We will define a new struct that conforms to NavigationTransitionProtocol
.
import SwiftUINavigationTransitions
import SwiftUI
struct Swing: NavigationTransitionProtocol {
var body: some NavigationTransitionProtocol {
// The implementation will go here
}
}
// It's good practice to add a static helper for convenience
extension AnyNavigationTransition {
static var swing: Self {
.init(Swing())
}
}
Composing Atomic Transitions
The body
of a NavigationTransitionProtocol
uses a result builder to combine other transitions. The swing
transition can be broken down into two parts:
- The standard
Slide
transition for the new view coming in. - A custom "swing out" effect for the old view going out.
We can compose these like so:
struct Swing: NavigationTransitionProtocol {
var body: some NavigationTransitionProtocol {
// Apply a standard slide transition to both views.
Slide(axis: .horizontal)
// Define the custom "swing" effect.
MirrorPush {
let angle = 70.0
let offset = 150.0
// On insertion, the "to" view is pushed on top.
OnInsertion {
ZPosition(1)
Rotate(.degrees(-angle))
Offset(x: offset)
Opacity()
Scale(0.5)
}
// On removal, the "from" view swings out.
OnRemoval {
Rotate(.degrees(angle))
Offset(x: -offset)
}
}
}
}
Let's break down the MirrorPush
block:
MirrorPush
: This is a container that defines a transition for apush
operation. The library automatically creates the inverse ("mirrored") transition for thepop
operation. This saves you from having to define the pop animation separately.OnInsertion
: This block applies only to the view being inserted (the destination view in a push). We apply multiple atomic transitions:ZPosition(1)
: Puts the new view on top.Rotate
,Offset
,Scale
,Opacity
: These define the initial state of the incoming view (rotated, offset, scaled down, and transparent) before it animates to its final state.
OnRemoval
: This block applies only to the view being removed (the source view in a push). It's simpler: it just rotates and offsets the view as it animates out.
By combining the base Slide
with this custom MirrorPush
block, we override parts of the slide behavior to create the unique swing effect, while keeping the core slide movement for the incoming view. This composability is what makes the system so flexible.