SwiftUI
SwiftUI offset animation
offset shifts where a view is drawn without changing where
the layout system thinks it is. Everything around it stays put. That makes it the
right tool for motion and the wrong tool for layout.
HStack {
Circle().frame(width: 20, height: 20).offset(y: -30)
Circle().frame(width: 20, height: 20)
}
// The HStack is still 20pt tall. The first circle is simply drawn above it.If you want the surrounding layout to react, animate padding,
frame or the stack's alignment instead. If you want a thing to move
across what's already there, offset.
A wave
Five dots, one shared state, and a delay per index. Each dot runs the same easeInOut between two offsets, started a little later than the last.
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 }
}
}A real wave, from sin
The version above approximates a wave with staggered easing. If you want an actual sine — where the vertical position is a genuine function of index and time — compute it:
import SwiftUI
struct SineWave: View {
var count: Int = 7
var amplitude: Double = 10
var period: Double = 1.4
var body: some View {
TimelineView(.animation) { context in
let t = context.date.timeIntervalSinceReferenceDate
HStack(spacing: 7) {
ForEach(0..<count, id: \.self) { i in
Circle()
.fill(.teal)
.frame(width: 9, height: 9)
.offset(y: sin(t * .pi * 2 / period + Double(i) * 0.6) * amplitude)
}
}
}
}
}No state, no delays, no onAppear. The phase offset
Double(i) * 0.6 is what makes it travel along the row, and changing that
one number changes the wavelength.
Bouncing
Offset plus a spring, and the overshoot does the work:
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 }
}
}offset, position or padding
| Modifier | Relative to | Affects layout |
|---|---|---|
offset(x:y:) | Where it would have been | No |
position(x:y:) | The parent's coordinate space | No — and it takes all the space |
padding() | Its own edges | Yes |
position catches people out: it makes the view fill its parent and
then places the content at that point, so a position inside an
HStack will happily eat the whole stack.
Order, again
offset before rotationEffect swings the view around its
parent's centre at a radius. After, it slides the already-rotated view along the
screen's axes. Both are useful; they are not interchangeable. There is more on that
in rotation animation.
Run it
Open SineWave in Swoop and change the phase step from
0.6 to 0.15, then to 2.0. It goes from a gentle ripple to something closer to a
scatter, and the number stops being arbitrary.
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