Master SwiftUI Transitions: From AnyTransition to Custom Visual Effects
This tutorial explores SwiftUI's transition mechanisms—including AnyTransition, the new Transition protocol, visualEffect modifiers, shader‑based effects, color gradients, text rendering, scroll‑based and navigation transitions—providing step‑by‑step code examples and practical guidance for creating impressive UI animations.
Abstract: This article introduces two ways to implement transition effects in SwiftUI—AnyTransition and Transition—then systematically breaks down the steps and considerations for creating custom SwiftUI transitions, and finally demonstrates a variety of visual, shader, color, text, scroll, and navigation transition examples.
Transition Overview
A transition defines how a view changes from one state to another, providing smooth visual changes when a view appears or disappears.
Main Characteristics
Smoothness : Transitions make view insertion and removal appear fluid.
Visual Feedback : Users instantly perceive state changes, such as a dialog fading in.
Enhanced UX : Interactive, lively interfaces improve overall experience.
Typical Use Cases
View Show/Hide : Fade‑in/out modals, slide navigation.
Content Changes : List insertions/deletions during data refresh.
Control State : Button presses, focus changes.
Notifications : Show/hide banners or tooltips.
Dynamic Layout : Switch between list and grid, reveal menus.
SwiftUI Transition Mechanisms
SwiftUI handles transitions when a view is added to or removed from the hierarchy. Three approaches are provided:
AnyTransition struct (type‑erased transition, iOS 13+).
Transition protocol (iOS 17+).
Modifiers that contain the word .transition (e.g., .transition, .scrollTransition, .visualEffect).
AnyTransition Struct
AnyTransition is a type‑erased struct that defines how a view animates when it appears or disappears. It enables both built‑in and custom transitions.
Basic Usage
To apply an AnyTransition, combine the .transition modifier with an .animation that drives the change. The three required steps are:
Specify a transition with .transition.
Ensure the view’s state changes (e.g., a Boolean toggles).
Attach an animation with .animation to drive the transition.
Example of a slide transition:
#Preview("Basic Usage of AnyTransition") {
@Previewable @State var toggle: Bool = false
VStack {
if toggle {
Text("Hello, SwiftUI Transition!")
.padding()
.background(.blue.gradient)
.foregroundStyle(.white)
.transition(AnyTransition.slide)
}
}
.animation(.linear, value: toggle)
.onAppear { toggle = true }
}Default transitions include: .slide: Move view from or to a screen edge. .opacity: Fade in/out via transparency. .scale: Scale view up or down. .move(edge:): Move view from a specified edge. .push(edge:): Push view in/out from an edge. .offset(x:y:): Offset view by a given amount.
Asymmetric Transitions
Use .asymmetric to specify different effects for insertion and removal, e.g., slide in and fade out:
.transition(AnyTransition.asymmetric(insertion: .slide, removal: .opacity))Combined Transitions
Multiple effects can be merged with .combined(with:). Example combining opacity and slide:
.transition(AnyTransition.opacity.combined(with: .slide))Custom AnyTransition
Custom transitions are created by implementing .modifier(active:identity:) and returning a custom ViewModifier. Example of a custom slide that mimics the built‑in slide:
extension AnyTransition {
static var customSlide: AnyTransition {
AnyTransition.asymmetric(
insertion: .modifier(active: CustomSlideModifier(offset: -UIScreen.main.bounds.width), identity: CustomSlideModifier(offset: 0)),
removal: .modifier(active: CustomSlideModifier(offset: UIScreen.main.bounds.width), identity: CustomSlideModifier(offset: 0))
)
}
}
struct CustomSlideModifier: ViewModifier {
var offset: CGFloat
func body(content: Content) -> some View {
content.offset(x: offset)
}
}When view size is needed, a size‑aware version can be defined:
extension AnyTransition {
static func customSlide(size: CGSize) -> AnyTransition {
AnyTransition.asymmetric(
insertion: .modifier(active: CustomSlideModifier(offset: -size.width), identity: CustomSlideModifier(offset: 0)),
removal: .modifier(active: CustomSlideModifier(offset: size.width), identity: CustomSlideModifier(offset: 0))
)
}
}Transition Protocol (iOS 17+)
The Transition protocol replaces AnyTransition for newer projects. Types conform to Transition and implement
func body(content: Content, phase: TransitionPhase) -> some View. It exposes TransitionPhase (identity, willAppear, didDisappear) for finer control.
Custom slide using the protocol:
extension Transition where Self == CustomSlideTransition {
static func customSlide(size: CGSize) -> CustomSlideTransition {
CustomSlideTransition(size: size)
}
}
struct CustomSlideTransition: Transition {
let size: CGSize
public func body(content: Content, phase: TransitionPhase) -> some View {
let offset: CGFloat = switch phase {
case .willAppear: -size.width
case .identity: 0
case .didDisappear: size.width
}
content.offset(x: offset)
}
}VisualEffect Transitions (iOS 17+)
The .visualEffect(_:) modifier provides both the view and its geometry, enabling effects that depend on size or position. Available modifiers inside .visualEffect include color adjustments (brightness, contrast, grayscale, hueRotation, saturation, opacity), affine transforms (transform, offset, rotationEffect, scaleEffect), Gaussian blur, and shader effects (colorEffect, layerEffect, distortionEffect).
Example recreating a slide transition with .visualEffect:
struct MyCustomSlideTransition: Transition {
public func body(content: Content, phase: TransitionPhase) -> some View {
content.visualEffect { view, geo in
view.offset(x: phase.value * geo.size.width)
}
}
}
extension Transition where Self == MyCustomSlideTransition {
static var myCustomSlide: MyCustomSlideTransition { MyCustomSlideTransition() }
}
Text("Hello, Transition!")
.transition(.myCustomSlide)Shader‑Based Transitions
SwiftUI provides three Metal‑shader modifiers:
colorEffect(_ shader: Shader, isEnabled: Bool = true) layerEffect(_ shader: Shader, maxSampleOffset: CGSize, isEnabled: Bool = true) distortionEffect(_ shader: Shader, maxSampleOffset: CGSize, isEnabled: Bool = true)Example grayscale shader:
[[stitchable]] half4 grayColor(float2 position, half4 color) {
float gray = (color.r + color.g + color.b) / 3;
return half4(gray, gray, gray, color.a);
}Applying it in SwiftUI:
Image(.pixelsMeasure)
.resizable()
.frame(width: 256, height: 256)
.padding()
.colorEffect(ShaderLibrary.grayColor())Transparent transition using a custom opacity shader:
Image(.pixelsMeasure)
.resizable()
.frame(width: 256, height: 256)
.padding()
.transition(.myOpacity)
extension Transition where Self == MyOpacityTransition {
static var myOpacity: MyOpacityTransition { MyOpacityTransition() }
}
struct MyOpacityTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content.colorEffect(ShaderLibrary.myOpacity(Float(phase.value)))
}
}Corresponding Metal shader:
[[stitchable]] half4 myOpacity(float2 position, half4 color, float value) {
float progress = value > 0 ? 1 - value : 1 + value;
return half4(color.r * progress, color.g * progress, color.b * progress, color.a * progress);
}Mosaic transition using layerEffect:
struct MosaicTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content.visualEffect { view, proxy in
view.layerEffect(ShaderLibrary.mosaic(Float(phase.value), Float(32)), maxSampleOffset: .zero)
}
}
}Metal shader for mosaic:
[[stitchable]] half4 mosaic(float2 position, SwiftUI::Layer layer, float value, float tileSize) {
float progress = 1 - cos(value * 3.1415926 / 2);
float tile = max(progress * tileSize, 0.000001);
if (progress * tileSize < 0.00000001) {
return layer.sample(position);
}
return layer.sample(round(position / tile) * tile);
}Color Transitions (iOS 18+)
iOS 18 introduces MeshGradient for grid‑based gradients. By animating the gradient’s points according to the transition progress, smooth color‑shift effects can be created.
struct MeshColorTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
let progress = Float(abs(phase.value) / 2.0)
return content.foregroundStyle(
MeshGradient(
width: 4,
height: 3,
points: [
[0.0, 0.0],
[0.5 - progress, 0.0],
[0.5 + progress, 0.0],
[1.0, 0.0],
[0.0, 0.5],
[0.5 - progress, 0.5],
[0.5 + progress, 0.5],
[1.0, 0.5],
[0.0, 1.0],
[0.5 - progress, 1.0],
[0.5 + progress, 1.0],
[1.0, 1.0]
],
colors: [.red, .white, .white, .blue,
.red, .white, .white, .blue,
.red, .white, .white, .blue]
)
)
}
}Text Transitions (iOS 18+)
The TextRenderer protocol allows custom drawing of text. Implement
func draw(layout: Text.Layout, in context: inout GraphicsContext)to access runs and run slices.
Example of a typing effect:
struct TextKeyboardTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content.textRenderer(KeyboardEffectRenderer(value: phase.value))
}
}
struct KeyboardEffectRenderer: TextRenderer, Animatable {
var value: Double
init(value: Double) { self.value = value }
var animatableData: Double {
get { value }
set { value = newValue }
}
func draw(layout: Text.Layout, in context: inout GraphicsContext) {
let progress = value > 0 ? 1 - value : 1 + value
let count = layout.flattenedRunSlices.count
let currentIndex = Int(round(progress * Double(count)))
let cursor = Text("|").foregroundStyle(.green).font(.system(.title, design: .rounded, weight: .semibold))
for (index, slice) in layout.flattenedRunSlices.enumerated() {
var copy = context
if index < currentIndex {
copy.draw(slice)
} else {
copy.draw(cursor, in: slice.typographicBounds.rect)
break
}
}
}
}Scroll‑Based Transitions (iOS 17+)
The .scrollTransition(_:axis:transition:) modifier animates views as they enter or leave a scroll view’s visible area. Example of a horizontal parallax effect:
ScrollView(.horizontal) {
LazyHStack(spacing: 16) {
ForEach(photos) { photo in
VStack {
ZStack {
Card(photo)
.scrollTransition(axis: .horizontal) { content, phase in
content.offset(x: phase.isIdentity ? 0 : phase.value * -200)
}
}
.containerRelativeFrame(.horizontal)
.clipShape(RoundedRectangle(cornerRadius: 36))
}
}
}
}
.contentMargins(32)
.scrollTargetBehavior(.paging)Navigation Transitions (iOS 24+)
iOS 24 introduces the NavigationTransition protocol. Its .zoom(sourceID:in:) method creates App Store‑style zoom transitions.
NavigationLink {
PhotoCardDetail(photo: photo)
.navigationBarBackButtonHidden()
.navigationTransition(.zoom(sourceID: photo.id, in: namespace))
} label: {
PhotoCard(photo: photo)
}
.matchedTransitionSource(id: photo.id, in: namespace)Conclusion
This article explored SwiftUI transition techniques from the foundational AnyTransition and the newer Transition protocol to advanced visual, shader, color, text, scroll, and navigation effects. By combining built‑in modifiers, custom view modifiers, and Metal shaders, developers can craft rich, dynamic animations that elevate user experience.
References
WWDC 2024 Session 10151: https://developer.apple.com/videos/play/wwdc2024/10151/
PixelsMeasure app: https://apps.apple.com/cn/app/pixelsmeasure/id1638740542
iOS Development Advanced Course: https://t2.lagounews.com/lR59RGRBct5E3
SwiftUI Animation Article: https://xiaozhuanlan.com/topic/3165078924
saveSize helper (Stack Overflow): https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftui
Metal Shading Language Specification: https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf
UnitCurve documentation: https://developer.apple.com/documentation/swiftui/unitcurve
SwiftUI Animation Article (GitHub): https://github.com/SwiftOldDriver/WWDC23/blob/main/sessions/session_10156/README.md
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
