SwiftUI
SwiftUI animation examples
Every example below is a whole file. No helper types, no package, nothing to import
beyond SwiftUI. Paste one into Xcode and it builds; open it in
Swoop on your phone and it runs.
They are ordered by the SwiftUI idea each one teaches, not by how they look: implicit animation first, then repeating animation, then time-driven animation.
1. A pulse, with repeatForever
The smallest useful animation in SwiftUI: one piece of state, one modifier that
animates on it, and a repeat. onAppear flips the state once and the
repeat does the rest.
import SwiftUI
struct PulseBadge: View {
@State private var wide = false
var body: some View {
ZStack {
Circle()
.fill(.green)
.frame(width: 20, height: 20)
.scaleEffect(wide ? 2.7 : 1)
.opacity(wide ? 0 : 0.55)
Circle()
.fill(.green)
.frame(width: 20, height: 20)
}
.animation(.easeOut(duration: 1.6).repeatForever(autoreverses: false), value: wide)
.onAppear { wide = true }
}
}2. A spinner, with rotationEffect
A trimmed circle is a spinner. trim cuts the stroke short, and
rotating the whole shape forever does the spinning — there is no separate arc type
to learn.
import SwiftUI
struct RingSpinner: View {
@State private var turning = false
var size: Double = 46
var lineWidth: Double = 5
var body: some View {
Circle()
.trim(from: 0, to: 0.72)
.stroke(.orange, lineWidth: lineWidth)
.frame(width: size, height: size)
.rotationEffect(.degrees(turning ? 360 : 0))
.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: turning)
.onAppear { turning = true }
}
}3. A wave, with ForEach and a delay per item
The trick is the delay: index times a fraction of the period. Every dot runs the same animation, just later than the one before it.
import SwiftUI
struct DotWave: View {
@State private var up = false
var count: Int = 5
var period: Double = 1.05
var body: some View {
HStack(spacing: 7) {
ForEach(0..<count, id: \.self) { i in
Circle()
.fill(.cyan)
.frame(width: 9, height: 9)
.offset(y: up ? -9 : 9)
.animation(
.easeInOut(duration: period / 2)
.repeatForever()
.delay(Double(i) * period / 10),
value: up
)
}
}
.onAppear { up = true }
}
}4. An equalizer, animating frame(height:)
Same shape as the wave, but the property being animated is the height of a capsule rather than its offset.
import SwiftUI
struct Equalizer: View {
@State private var tall = false
var bars: Int = 4
var body: some View {
HStack(alignment: .bottom, spacing: 5) {
ForEach(0..<bars, id: \.self) { i in
Capsule()
.fill(.purple)
.frame(width: 7, height: tall ? 42 : 14)
.animation(
.easeInOut(duration: 0.9)
.repeatForever()
.delay(Double(i) * 0.14),
value: tall
)
}
}
.frame(height: 44)
.onAppear { tall = true }
}
}5. A progress arc, animating trim
trim is animatable, so the arc can be driven straight from a
Double. This is the same modifier as the spinner, moving instead of
turning.
import SwiftUI
struct ProgressArc: View {
@State private var value: Double = 0.04
var body: some View {
Circle()
.trim(from: 0, to: value)
.stroke(.blue, lineWidth: 6)
.frame(width: 46, height: 46)
.rotationEffect(.degrees(-90))
.animation(.easeInOut(duration: 1.3).repeatForever(), value: value)
.onAppear { value = 0.92 }
}
}6. A bounce, with a spring
Springs are described by feel, not by curve: response is roughly how
long the move takes and dampingFraction is how much it overshoots.
import SwiftUI
struct BouncingBall: View {
@State private var down = false
var body: some View {
ZStack(alignment: .top) {
Circle()
.fill(.pink)
.frame(width: 18, height: 18)
.offset(y: down ? 28 : 0)
}
.frame(width: 44, height: 48)
.animation(
.spring(response: 0.36, dampingFraction: 0.55).repeatForever(),
value: down
)
.onAppear { down = true }
}
}7. A toggle that overshoots
Any spring with a damping fraction below about 0.7 overshoots. That is the whole difference between a switch that feels mechanical and one that feels alive.
import SwiftUI
struct BouncyToggle: View {
@State private var on = false
var body: some View {
Button {
on.toggle()
} label: {
Capsule()
.fill(on ? Color.green : Color.gray.opacity(0.3))
.frame(width: 50, height: 30)
.overlay(alignment: on ? .trailing : .leading) {
Circle()
.fill(.white)
.frame(width: 24, height: 24)
.padding(3)
}
}
.animation(.spring(response: 0.34, dampingFraction: 0.55), value: on)
}
}8. An orbit, driven by the clock
The others animate between two states. This one has no states: it reads
the current time every frame and computes a position from it. That is what
TimelineView(.animation) is for, and it is the only way to get motion
that depends on continuous time rather than on a transition.
import SwiftUI
struct TwinOrbitLoader: View {
var size: Double = 62
var period: Double = 0.85
var body: some View {
TimelineView(.animation) { context in
let t = context.date.timeIntervalSinceReferenceDate
let a = t * .pi * 2 / period
ZStack {
ball(angle: a, color: .mint)
ball(angle: a + .pi, color: .cyan)
}
}
.frame(width: size, height: size)
}
func ball(angle: Double, color: Color) -> some View {
let depth = sin(angle)
return Circle()
.fill(color.gradient)
.frame(width: size * 0.36, height: size * 0.36)
.scaleEffect(0.78 + 0.22 * ((depth + 1) / 2))
.offset(x: cos(angle) * size * 0.28, y: sin(angle) * size * 0.1)
.zIndex(depth)
}
}Which one should you reach for
| You want | Use |
|---|---|
| Something to change when a value changes | .animation(_:value:) |
| Something to run forever | .repeatForever() plus onAppear |
| Items to move one after another | .delay(Double(i) * step) inside ForEach |
| Motion that depends on the clock | TimelineView(.animation) |
| Overshoot | .spring(dampingFraction:) below 0.7 |
Run them
Every file above is complete. Copy one into Swoop, press play, and
change a number — the fastest way to understand a spring is to watch
dampingFraction go from 0.9 to 0.4 while you are holding the phone.
Keep reading
SwiftUI loading animation
Four spinners, complete files, no package to install.
2026-09-08withAnimation, and the modifier that replaced it
Two ways to animate the same change, and the rule for picking one.
2026-09-08SwiftUI rotation animation
rotationEffect, anchors, and the two mistakes everybody makes once.
2026-09-08