SwiftUI
SwiftUI onAppear animation
The single most common SwiftUI animation question is a version of this: the
animation is written correctly, repeatForever is there, and nothing
moves.
The reason is that SwiftUI animates changes. An animation modifier does not start anything — it describes how a change should be drawn if one happens. If the state never changes, there is no change to draw, and the view sits there.
onAppear is what supplies that first change.
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 }
}
}tall flips exactly once, on appear. Every bar's height depends on it,
so every bar animates — and repeatForever keeps each one going.
Staggering
The stagger is one line: .delay(Double(i) * step). Each item runs the
identical animation, just started later. Multiply the index by a fraction of the
period and the row becomes a wave rather than four bars moving as one.
| Step | Reads as |
|---|---|
0 | One block moving |
period / count | A travelling wave |
period / 2 | Alternating, like a metronome |
The bug where it jumps instead of animating
If the state flip happens before the view has a frame, there is nothing to
interpolate from and the change lands instantly. It shows up most often when
onAppear sets several things at once, or when the view is inside a
container that lays out late.
The fix is to let the layout settle first:
.onAppear {
// One runloop turn is enough — the view has a frame by the time this runs.
DispatchQueue.main.async { tall = true }
}Note. Swoop's player runs a subset of SwiftUI and does not
include DispatchQueue. The plain .onAppear { tall = true }
form above plays on the phone; this variant is for Xcode, when you hit the timing
problem in a real screen.
onAppear is not viewDidAppear
Two habits from UIKit that cause trouble:
- It can fire more than once. A view that is removed and re-added
gets another
onAppear. If you are starting something expensive, guard it. - It fires before the view is on screen, not after. That is precisely why the animation sometimes has no frame to animate from.
The alternative: don't use state at all
If the motion is continuous, onAppear plus
repeatForever is a workaround for not having a clock.
TimelineView(.animation) gives you the clock, and then there is no
state to flip and no first change to arrange —
see the animation examples.
Run it
Paste Equalizer into Swoop and delete the
.onAppear line. Watching it stop dead is the fastest way to internalise
what animation modifiers do and don't do.
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