Content Swap Transition
A showcase of text value changes using numericText, interpolate and opacity content transitions.

- Transitions
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- content transition · numeric text · counter · rolling digits · interpolate · text animation · opacity · transición de contenido · texto numérico · contador · dígitos · interpolar · animación de texto
Description
Section titled “Description”Text that swaps its value with a content transition: rolling digits, a morph or a fade. Change the text inside an animation.
With Reduce Motion on, numbers fade instead of rolling.
struct ContentSwapTransitionDemo: View { @State private var step = 0
private let amounts: [Double] = [1284, 1317, 2096, 1905] private let greetings = ["Hello", "Hola", "Bonjour", "Ciao"]
private var amount: Double { amounts[step % amounts.count] } private var isHeavy: Bool { step.isMultiple(of: 2) }
var body: some View { VStack(spacing: 16) { card("numericText", systemImage: "number", tint: .blue) { ContentSwapTransition(text: amount.formatted(.currency(code: "EUR")), style: .numeric(amount)) }
card("interpolate", systemImage: "textformat", tint: .pink) { ContentSwapTransition(text: "Weight", style: .interpolate) .fontWeight(isHeavy ? .black : .ultraLight) .foregroundStyle(isHeavy ? .pink : .orange) }
card("opacity", systemImage: "circle.lefthalf.filled", tint: .teal) { ContentSwapTransition(text: greetings[step % greetings.count], style: .opacity) } } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemGroupedBackground).ignoresSafeArea()) .task { while (try? await Task.sleep(for: .seconds(1.2))) != nil { withAnimation(.smooth(duration: 0.6)) { step += 1 } } } }
private func card( _ title: String, systemImage: String, tint: Color, @ViewBuilder content: () -> some View ) -> some View { VStack(alignment: .leading, spacing: 12) { Label(title, systemImage: systemImage) .font(.subheadline.weight(.semibold)) .foregroundStyle(tint)
content() .font(.system(.largeTitle, design: .rounded, weight: .bold)) .monospacedDigit() .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) } .padding(24) .background(Color(.secondarySystemGroupedBackground), in: .rect(cornerRadius: 24)) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
text | String | Required |
style | ContentSwapTransitionStyle | .opacity |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
enum ContentSwapTransitionStyle { case numeric(Double) case interpolate case opacity}
/// Text that swaps its value with a content transition: rolling digits, a morph or a fade./// Change the text inside an animation.////// With Reduce Motion on, numbers fade instead of rolling.struct ContentSwapTransition: View { let text: String var style: ContentSwapTransitionStyle = .opacity
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var transition: ContentTransition { switch style { case .numeric(let value): reduceMotion ? .opacity : .numericText(value: value) case .interpolate: .interpolate case .opacity: .opacity } }
var body: some View { Text(text) .contentTransition(transition) }}
// MARK: - Demo
struct ContentSwapTransitionDemo: View { @State private var step = 0
private let amounts: [Double] = [1284, 1317, 2096, 1905] private let greetings = ["Hello", "Hola", "Bonjour", "Ciao"]
private var amount: Double { amounts[step % amounts.count] } private var isHeavy: Bool { step.isMultiple(of: 2) }
var body: some View { VStack(spacing: 16) { card("numericText", systemImage: "number", tint: .blue) { ContentSwapTransition(text: amount.formatted(.currency(code: "EUR")), style: .numeric(amount)) }
card("interpolate", systemImage: "textformat", tint: .pink) { ContentSwapTransition(text: "Weight", style: .interpolate) .fontWeight(isHeavy ? .black : .ultraLight) .foregroundStyle(isHeavy ? .pink : .orange) }
card("opacity", systemImage: "circle.lefthalf.filled", tint: .teal) { ContentSwapTransition(text: greetings[step % greetings.count], style: .opacity) } } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemGroupedBackground).ignoresSafeArea()) .task { while (try? await Task.sleep(for: .seconds(1.2))) != nil { withAnimation(.smooth(duration: 0.6)) { step += 1 } } } }
private func card( _ title: String, systemImage: String, tint: Color, @ViewBuilder content: () -> some View ) -> some View { VStack(alignment: .leading, spacing: 12) { Label(title, systemImage: systemImage) .font(.subheadline.weight(.semibold)) .foregroundStyle(tint)
content() .font(.system(.largeTitle, design: .rounded, weight: .bold)) .monospacedDigit() .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) } .padding(24) .background(Color(.secondarySystemGroupedBackground), in: .rect(cornerRadius: 24)) }}
#Preview { ContentSwapTransitionDemo()}