SwiftUI
SwiftUI rotation animation
rotationEffect turns a view around a point. It takes an
Angle and an optional anchor, and it is a drawing
transform — the layout system still sees the view's original frame.
Rectangle()
.rotationEffect(.degrees(45)) // around the centre
.rotationEffect(.degrees(45), anchor: .topLeading)
.rotationEffect(.radians(.pi / 4)) // radians work tooSpinning forever
A continuous spin is one state flip plus a repeating linear animation.
import SwiftUI
struct SquareSpinner: View {
@State private var turning = false
var body: some View {
ZStack {
ForEach(0..<3, id: \.self) { i in
RoundedRectangle(cornerRadius: 9)
.stroke(.orange, lineWidth: 3)
.frame(width: 44, height: 44)
.rotationEffect(.degrees(turning ? 180 : 0))
.scaleEffect(turning ? 0.55 : 1)
.opacity(1 - Double(i) * 0.3)
.animation(
.easeInOut(duration: 2.2)
.repeatForever()
.delay(Double(i) * 0.73),
value: turning
)
}
}
.onAppear { turning = true }
}
}Two mistakes
1. autoreverses defaults to true
.repeatForever() winds back. For a spinner that means it turns
clockwise, then unwinds anticlockwise, forever — which reads as broken rather than
as loading. For continuous rotation you want:
.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: turning)2. Order changes the picture
Modifiers wrap, so they apply outward-in, and rotation composes with position:
// Pushed out to a radius, then swung around the centre.
Capsule().offset(y: -17).rotationEffect(.degrees(30))
// Rotated in place, then pushed out. Twelve of these stack on top of each other.
Capsule().rotationEffect(.degrees(30)).offset(y: -17)The first is how a twelve-dash spinner is built. The second is the bug report that says "all my dashes are in one spot".
Anchors
The anchor is a UnitPoint in the view's own space:
.center by default, but .topLeading,
.bottom and friends all work. A clock hand rotates around
.bottom, not its centre — that one substitution is the difference
between a hand that sweeps and a hand that tumbles.
Rotation that follows the clock
If the angle should be a function of real time rather than a transition between
two values, compute it in a TimelineView instead of animating a
state:
TimelineView(.animation) { context in
let t = context.date.timeIntervalSinceReferenceDate
Rectangle()
.frame(width: 4, height: 22)
.rotationEffect(.degrees(t * 90), anchor: .bottom)
}No state, no onAppear, and it can never drift out of sync — the
angle is derived from the clock every frame.
Run it
Paste SquareSpinner into Swoop and swap the
offset/rotationEffect order, or move the anchor to
.topLeading. Both mistakes above are much easier to remember once you
have watched them happen.
Keep reading
SwiftUI animation examples
Eight components, complete source for each, all running as you read.
2026-09-08SwiftUI 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-08