Animations are what separate a good iOS app from a great one. SwiftUI has evolved dramatically since its introduction, and in 2026 it offers one of the most powerful and expressive animation systems on any platform. Whether you are just getting started or want to master advanced techniques, this guide covers everything you need to know.
1. Implicit Animations with .animation()
Implicit animations are the easiest way to add motion to your SwiftUI views. You attach the .animation() modifier, and SwiftUI automatically animates any changes to animatable properties.
struct ImplicitAnimationDemo: View {
@State private var scale: CGFloat = 1.0
@State private var opacity: Double = 1.0
var body: some View {
VStack(spacing: 30) {
Circle()
.fill(.blue.gradient)
.frame(width: 100, height: 100)
.scaleEffect(scale)
.opacity(opacity)
.animation(.easeInOut(duration: 0.5), value: scale)
Button("Animate") {
scale = scale == 1.0 ? 1.5 : 1.0
opacity = opacity == 1.0 ? 0.5 : 1.0
}
.buttonStyle(.borderedProminent)
}
}
}
The key rule: always pass a value parameter to .animation(). This tells SwiftUI exactly which state change should trigger the animation. Without it, you get unpredictable behavior and a deprecation warning.
2. Explicit Animations with withAnimation
When you need more control over what gets animated, use withAnimation {}. This wraps a state change in an animation context, giving you precise control.
struct ExplicitAnimationDemo: View {
@State private var isExpanded = false
@State private var rotation: Double = 0
var body: some View {
VStack(spacing: 20) {
RoundedRectangle(cornerRadius: isExpanded ? 20 : 50)
.fill(.purple.gradient)
.frame(
width: isExpanded ? 300 : 100,
height: isExpanded ? 200 : 100
)
.rotationEffect(.degrees(rotation))
Button("Toggle") {
withAnimation(.easeInOut(duration: 0.6)) {
isExpanded.toggle()
}
// This change is NOT animated
rotation += 45
}
}
}
}
Use withAnimation when you want some state changes animated and others not. Only the state mutations inside the closure get animated. Here the shape expands smoothly but the rotation snaps instantly.
3. Spring Animations (iOS 17+)
Spring animations create natural, physics-based motion. iOS 17 introduced simplified spring presets that make it trivial to add bouncy, organic movement.
struct SpringAnimationDemo: View {
@State private var offset: CGFloat = 0
var body: some View {
VStack(spacing: 40) {
// Bouncy spring -- high bounce, fun feel
Circle()
.fill(.orange)
.frame(width: 60, height: 60)
.offset(y: offset)
.animation(.bouncy(duration: 0.6, extraBounce: 0.3), value: offset)
// Snappy spring -- quick and responsive
Circle()
.fill(.green)
.frame(width: 60, height: 60)
.offset(y: offset)
.animation(.snappy(duration: 0.4), value: offset)
// Custom spring with full control
Circle()
.fill(.blue)
.frame(width: 60, height: 60)
.offset(y: offset)
.animation(
.spring(duration: 0.8, bounce: 0.5, blendDuration: 0.2),
value: offset
)
Button("Drop") {
offset = offset == 0 ? 150 : 0
}
.buttonStyle(.borderedProminent)
}
}
}
The three main spring presets: .bouncy for playful interfaces, .snappy for responsive UI interactions, and .spring() with custom parameters for full control. Spring animations feel more natural than ease curves because they simulate real-world physics.
4. Transition Animations
Transitions control how views appear and disappear. They work with if statements and other conditional rendering in SwiftUI.
struct TransitionDemo: View {
@State private var showDetail = false
var body: some View {
VStack(spacing: 20) {
Button("Toggle Detail") {
withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
showDetail.toggle()
}
}
if showDetail {
DetailCard()
.transition(
.asymmetric(
insertion: .move(edge: .trailing)
.combined(with: .opacity),
removal: .move(edge: .leading)
.combined(with: .opacity)
)
)
}
}
}
}
struct DetailCard: View {
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(.teal.gradient)
.frame(height: 200)
.overlay(
Text("Detail View")
.font(.title2.bold())
.foregroundColor(.white)
)
.padding()
}
}
The .asymmetric transition lets you define different animations for insertion and removal. You can combine multiple transitions with .combined(with:) for richer effects -- for example, sliding in from the right while fading in.
5. Keyframe Animations (iOS 17+)
KeyframeAnimator lets you define complex, multi-property animations with precise timing control. Think of it as CSS keyframes but for SwiftUI.
struct KeyframeDemo: View {
@State private var animating = false
var body: some View {
VStack {
Text("Hello!")
.font(.largeTitle.bold())
.keyframeAnimator(
initialValue: AnimationValues(),
trigger: animating
) { content, value in
content
.scaleEffect(value.scale)
.rotationEffect(.degrees(value.rotation))
.offset(y: value.yOffset)
} keyframes: { _ in
KeyframeTrack(\.scale) {
SpringKeyframe(1.5, duration: 0.3)
SpringKeyframe(0.8, duration: 0.2)
SpringKeyframe(1.0, duration: 0.3)
}
KeyframeTrack(\.rotation) {
LinearKeyframe(0, duration: 0.1)
SpringKeyframe(15, duration: 0.15)
SpringKeyframe(-15, duration: 0.15)
SpringKeyframe(0, duration: 0.2)
}
KeyframeTrack(\.yOffset) {
SpringKeyframe(-30, duration: 0.3)
SpringKeyframe(0, duration: 0.4)
}
}
Button("Animate") { animating.toggle() }
.buttonStyle(.borderedProminent)
.padding(.top, 40)
}
}
}
struct AnimationValues {
var scale: CGFloat = 1.0
var rotation: Double = 0
var yOffset: CGFloat = 0
}
KeyframeAnimator lets you animate multiple properties independently with different timings and curves. Each KeyframeTrack controls one property, and you can mix spring, linear, and cubic keyframes within the same animation.
6. Phase Animations
PhaseAnimator cycles through a sequence of states automatically, creating repeating multi-step animations without manual state management.
enum BouncePhase: CaseIterable {
case initial, moveUp, moveDown, settle
var yOffset: CGFloat {
switch self {
case .initial: return 0
case .moveUp: return -60
case .moveDown: return 20
case .settle: return 0
}
}
var scale: CGFloat {
switch self {
case .initial: return 1.0
case .moveUp: return 1.2
case .moveDown: return 0.9
case .settle: return 1.0
}
}
}
struct PhaseAnimationDemo: View {
@State private var trigger = false
var body: some View {
VStack {
PhaseAnimator(BouncePhase.allCases, trigger: trigger) { phase in
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundStyle(.red.gradient)
.offset(y: phase.yOffset)
.scaleEffect(phase.scale)
} animation: { phase in
switch phase {
case .initial: .spring(duration: 0.3)
case .moveUp: .spring(duration: 0.25, bounce: 0.4)
case .moveDown: .spring(duration: 0.2)
case .settle: .spring(duration: 0.35)
}
}
Button("Bounce") { trigger.toggle() }
.buttonStyle(.borderedProminent)
.padding(.top, 40)
}
}
}
PhaseAnimator is perfect for loading indicators, attention-grabbing effects, or any animation that goes through distinct stages. Each phase gets its own animation curve, giving you fine-grained control.
7. matchedGeometryEffect for Hero Transitions
matchedGeometryEffect creates smooth hero-style transitions between views by linking elements across different layout states.
struct HeroTransitionDemo: View {
@State private var showDetail = false
@Namespace private var animation
var body: some View {
ZStack {
if !showDetail {
// Thumbnail state
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(.indigo.gradient)
.matchedGeometryEffect(id: "card", in: animation)
.frame(width: 150, height: 150)
Text("SwiftUI")
.font(.headline)
.matchedGeometryEffect(id: "title", in: animation)
}
.onTapGesture {
withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
showDetail = true
}
}
} else {
// Expanded state
VStack(alignment: .leading, spacing: 16) {
RoundedRectangle(cornerRadius: 24)
.fill(.indigo.gradient)
.matchedGeometryEffect(id: "card", in: animation)
.frame(height: 300)
Text("SwiftUI")
.font(.largeTitle.bold())
.matchedGeometryEffect(id: "title", in: animation)
Text("SwiftUI is Apple's declarative framework for building user interfaces across all Apple platforms.")
.font(.body)
.foregroundColor(.secondary)
}
.padding()
.onTapGesture {
withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
showDetail = false
}
}
}
}
}
}
The @Namespace property wrapper creates a shared coordinate space. Elements with the same id and namespace smoothly interpolate between their two positions and sizes. This is how apps like the App Store create those beautiful expanding card transitions.
8. Performance Tips
Animations can hurt performance if you are not careful. Here are the key optimizations.
struct PerformanceView: View {
@State private var phase: CGFloat = 0
var body: some View {
// Tip 1: Use drawingGroup() for complex view hierarchies
// Flattens the view into a single Metal layer
ComplexGradientView()
.drawingGroup()
// Tip 2: Prefer Animation over Transaction for simple cases
// Good -- declarative and clear
Circle()
.animation(.spring, value: phase)
// Tip 3: Use .geometryGroup() in iOS 17+
// to isolate animation effects in subviews
OuterView()
.geometryGroup()
// Tip 4: Avoid animating views that trigger layout recalculation
// Bad -- animating frame causes layout pass every frame
// Rectangle().frame(width: animatedWidth)
// Good -- scaleEffect uses GPU, no layout recalculation
// Rectangle().scaleEffect(x: scaleFactor)
}
}
Key takeaways:
- drawingGroup() -- Renders complex view subtrees into a single Metal texture. Use it when you have many overlapping layers, gradients, or blend modes.
-
GPU-friendly properties -- Prefer
.scaleEffect,.rotationEffect,.offset, and.opacityover.frameor.paddingfor animations. The first group uses GPU compositing; the second triggers CPU layout passes. - geometryGroup() (iOS 17+) -- Isolates a subtree so that parent animations do not cause child views to recalculate geometry independently.
-
Transaction vs Animation -- Use
.animation()modifier for view-level animation. UsewithAnimationfor precise state-level control. Avoid the oldTransactionAPI unless you need to override animations in a specific subview.
Wrapping Up
SwiftUI's animation system in 2026 is incredibly powerful. Start with implicit animations for simple state changes, graduate to explicit animations when you need control, and reach for KeyframeAnimator or PhaseAnimator when building complex sequences. For view transitions, matchedGeometryEffect creates polished hero animations with minimal code.
The best way to learn is to experiment. Take each example above, paste it into an Xcode project, and tweak the parameters. You will quickly develop an intuition for what works.
Want 38 pre-built SwiftUI components with smooth animations? Check out my UI Components Pack: https://pease163.github.io/digital-products/
What animation techniques do you use most in your SwiftUI projects? Let me know in the comments!
Top comments (0)