SwiftUI’s animation system is powerful but often misunderstood. Even Xcode, when you start typing a keyword, shows a load of almost identical options. What about when, in a tech interview, you are asked about the difference?
Two concepts that frequently come up when building polished, animated interfaces are Transitions and Transactions. They sound similar (and they’re both related to motion and state changes) but they serve very different purposes.
A Transition in SwiftUI defines how a view appears and disappears when it is inserted into or removed from the view hierarchy. It’s about enter and exit animation — the visual effect used when a view changes state and is added or removed.
Transitions don’t control timing or easing. They describe the type of animation that happens. The actual animation timing comes from a surrounding animation context.
SwiftUI provides several built-in transitions:
Text("Hello SwiftUI!")
.transition(.opacity)
You can also combine transitions:
.transition(.scale.combined(with: .opacity))
Or define asymmetric transitions, where insertion and removal behave differently:
.transition(.asymmetric(
insertion: .slide,
removal: .scale
))
Or create a custom Transition relying on AnyTransition class. It’s pretty straightforward. How about blur and scale transition?
Need to make a transition modifier (yeah):
struct BlurAndScaleTransition: ViewModifier {
let progress: CGFloat
func body(content: Content) -> some View {
content
.scaleEffect(progress)
.blur(radius: (1 - progress) * 10)
.opacity(progress)
}
}
Then wrap in AnyTransition:
extension AnyTransition {
static var blurAndScale: AnyTransition {
.modifier(
active: BlurAndScaleTransition(progress: 0),
identity: BlurAndScaleTransition(progress: 1)
)
}
}
// and use it! A plain example is shown in the next section
.transition(.blurAndScale)
Transitions take effect only when views are inserted into or removed from the view hierarchy. This usually happens inside conditional statements such as if blocks or when modifying collections in List or ForEach.
SwiftUI compares the previous and current view tree, detects insertions and removals, and applies the transition accordingly.
Importantly, a transition alone does not animate. It must be wrapped in an animation context using withAnimation or .animation(_:).
struct MyView: View {
@State private var show = false
var body: some View {
VStack {
if show {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(height: 100)
.transition(.move(edge: .top))
}
Button("Toggle") {
withAnimation(.easeInOut) {
show.toggle()
}
}
}
}
}
Here:
-
The rectangle is inserted and removed from the hierarchy
-
The transition defines the movement
-
The animation defines timing and easing
A Transaction in SwiftUI represents the context of a state change. It carries information about how SwiftUI should process that change, including animation behavior.
Every time SwiftUI processes a state update, it creates a Transaction and propagates it through the view hierarchy.
A transaction can include:
-
The animation associated with the update
-
Whether animations are disabled
-
Metadata used internally by SwiftUI during rendering
Transitions define what happens visually. Transactions define how that visual change is executed.
If you use withAnimation, SwiftUI attaches the animation to the transaction. That transaction then flows down to child views unless explicitly modified.
This mechanism explains why animations sometimes affect views you didn’t expect — they are all responding to the same transaction.
Let’s track what the transaction does in our example:
struct SwiftBitsTransactionView: View {
@State private var show = false
var body: some View {
VStack {
if show {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(height: 200)
.transition(.move(edge: .top))
//Tracking transaction for animation
.transaction { thx in
print(thx as Any)
}
}
Button("Toggle Rect") {
withAnimation(.easeInOut) {
show.toggle()
}
}
}
}
}
The console will reveal this:
Transaction(plist: [TransactionPropertyKey = Optional(AnyAnimator(SwiftUI.BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: 0.52, bx: -0.78, cx: 1.26, ay: -2.0, by: 3.0, cy: 0.0))))])
And if we change it to Linear:
Transaction(plist: [TransactionPropertyKey = Optional(AnyAnimator(SwiftUI.BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: -2.0, bx: 3.0, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0))))])
It really matches the result on screen. Nice!
SwiftUI provides the .transaction(_:) modifier:
Text("No animation")
.transaction { thx in
thx.animation = nil
}
this removes animation for that view and all of its children, even if the state change was wrapped in withAnimation.
Or, it can be used to override the original animation behavior:
struct SwiftBitsTransactionView: View {
@State private var show = false
var body: some View {
VStack {
if show {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(height: 200)
.transition(.move(edge: .top))
.transaction { thx in
thx.animation = thx
.animation?
.delay(2.0)
.speed(2)
}
}
Button("Toggle Rect") {
withAnimation(.linear) {
show.toggle()
}
}
}
}
}
-
animation: The animation applied to this update, if any -
disablesAnimations: A Boolean that disables animations for this subtree -
addAnimationCompletion: Completion closure to run when the animations created with this transaction are all complete
Transactions allow very fine-grained control over animation behavior and are especially useful in complex view hierarchies.
Transitions are declarative and visual. Transactions are contextual and behavioral.
-
A view is conditionally shown or hidden
-
A view is inserted or removed from a collection
-
You want a clear visual effect for appearance or disappearance
Examples include modals, banners, expanding panels, or list rows.
-
You need to override or suppress animations
-
You want different animation behavior in specific subtrees
-
You need programmatic control over animation propagation
-
Default animation behavior is too broad or unpredictable
Transactions are more advanced and typically used when standard .animation and .transition modifiers are not enough.
Happy coding!
