How do you implement custom transitions?
Custom transitions in SwiftUI are built on the AnyTransition type combined with ViewModifier. Here’s how it works, from basic to advanced.
1. The core building block: .modifier
Every custom transition ultimately wraps a ViewModifier that changes appearance based on whether the view is being inserted/removed. SwiftUI calls your modifier twice during a transition: once in the “active” (initial/final) state, once in the “identity” (normal, settled) state, and animates between them.
struct ScaleAndFadeModifier: ViewModifier {
let scale: CGFloat
let opacity: Double
func body(content: Content) -> some View {
content
.scaleEffect(scale)
.opacity(opacity)
}
}
extension AnyTransition {
static var scaleAndFade: AnyTransition {
.modifier(
active: ScaleAndFadeModifier(scale: 0.5, opacity: 0),
identity: ScaleAndFadeModifier(scale: 1.0, opacity: 1)
)
}
}
Usage:
if showView {
MyView()
.transition(.scaleAndFade)
}
active: the modifier state applied at the extreme (fully removed / just about to appear)identity: the modifier state applied when the view is fully present
SwiftUI interpolates between these using whatever animation is currently in effect (withAnimation, .animation(), etc.).
2. Asymmetric transitions
Often you want a different transition for insertion vs. removal:
extension AnyTransition {
static var slideInFadeOut: AnyTransition {
.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .opacity
)
}
}
3. Combining transitions
.combined(with:) layers multiple transitions on the same view simultaneously:
.transition(.scale.combined(with: .opacity))
You can chain several — each contributes its own active/identity modifier pair, and SwiftUI applies them all.
4. Custom transitions with animatable data
If your transition needs a continuous parameter (not just two endpoints — e.g., a rotation that unwinds progressively), give your ViewModifier an animatableData property:
struct RotateInModifier: ViewModifier {
var progress: Double // 0 = fully transitioned out, 1 = identity
var animatableData: Double {
get { progress }
set { progress = newValue }
}
func body(content: Content) -> some View {
content
.rotationEffect(.degrees((1 - progress) * 90))
.opacity(progress)
.scaleEffect(0.8 + 0.2 * progress)
}
}
extension AnyTransition {
static var rotateIn: AnyTransition {
.modifier(
active: RotateInModifier(progress: 0),
identity: RotateInModifier(progress: 1)
)
}
}
This gives SwiftUI’s animation system a continuous value to interpolate, rather than just snapping between two modifier states — useful for spring animations where overshoot matters, or when you want the same modifier logic reused for partial/interactive transitions.
5. Transitions driven by geometry (matchedGeometryEffect)
For “hero” style transitions (a view morphing from one position/size to another, e.g., a card expanding into a detail view), transitions alone aren’t enough — you pair matchedGeometryEffect with a @Namespace:
@Namespace private var animation
// Source
RoundedRectangle(cornerRadius: 12)
.matchedGeometryEffect(id: "card", in: animation)
// Destination (different view hierarchy, same id)
RoundedRectangle(cornerRadius: 0)
.matchedGeometryEffect(id: "card", in: animation)
SwiftUI interpolates frame/position between the two matched views across a state change, which is a different (but related) mechanism from AnyTransition — it’s solving “morph between two layouts” rather than “appear/disappear.”
6. Practical tips
- Trigger transitions only on insertion/removal, not property changes. Transitions fire when a view enters/exits the tree (e.g., via
if,ForEachwith changing identity,.id()changes) — not when its properties merely update. - Wrap the state change in
withAnimation, not the transition declaration itself:withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) { showView.toggle() } - Keep the modifier cheap — it runs on every frame of the animation, so avoid expensive work inside
body. - Test both directions — asymmetric transitions are easy to get right one way and janky the other; preview insertion and removal separately.
The mental model: an AnyTransition is really just “two snapshots of a ViewModifier, plus instructions for how to reach in and interpolate between them,” and SwiftUI’s animation engine does the rest.