SwiftUI Notes 2: Gestures
01/06/2026
Gestures are how SwiftUI detects what the user's fingers are doing. You attach one to any view, SwiftUI gives you data about the interaction, and you decide what to do with it. Every gesture follows the same three-part pattern: a @State variable, a visual modifier, and a .gesture() that ties them together.
I. The Universal Pattern
Every gesture example in SwiftUI follows the same recipe. Once you see it, new gestures become predictable — the only things that change are which gesture, which state type, and which modifier.
- `@State` variable — stores the gesture data (offset, scale, angle, toggle).
- Visual modifier — reads from that variable (
.offset,.scaleEffect,.rotationEffect). - `.gesture()` — writes to the variable on
.onChangedand resets on.onEnded.
The lifecycle
Two events drive every gesture:
- `.onChanged` — fires repeatedly while the gesture is in progress. Tells you what's happening right now (position, distance, scale).
- `.onEnded` — fires once when the finger lifts. Time to clean up or animate back.
Simple gestures like TapGesture only have .onEnded — there's nothing meaningful to track mid-tap.
II. The Five Gesture Types
| Gesture | Detects | Data you get |
|---|---|---|
TapGesture | Quick tap (single, double, triple) | Just the event |
LongPressGesture | Press and hold | Whether threshold was met |
DragGesture | Finger sliding across screen | translation, location, startLocation |
MagnifyGesture | Pinch to zoom | Scale factor (2.0 = doubled) |
RotateGesture | Two-finger twist | Rotation angle |
III. Code Examples
1. TapGesture
Detect a tap and react. Toggle a color, show a sheet, trigger any action. No .onChanged — a tap is instant.
@State var tapped = false
Circle()
.fill(tapped ? Color.green : Color.red)
.frame(width: 100, height: 100)
.gesture(
TapGesture()
.onEnded {
tapped.toggle()
}
)Tap the circle and it flips between red and green.
2. LongPressGesture
Fires after the user holds for a set time. Use minimumDuration to control how long they need to hold.
@State var isPressed = false
RoundedRectangle(cornerRadius: 16)
.fill(isPressed ? Color.orange : Color.blue)
.frame(width: 150, height: 60)
.scaleEffect(isPressed ? 1.2 : 1.0)
.gesture(
LongPressGesture(minimumDuration: 0.8)
.onEnded { _ in
withAnimation(.spring()) {
isPressed.toggle()
}
}
)Hold for 0.8 seconds — the rectangle scales up and changes color with a spring animation.
3. DragGesture
Tracks finger movement in real time. Gives you a CGSize translation — how far the finger moved from the starting point.
@State var dragOffset: CGSize = .zero
RoundedRectangle(cornerRadius: 24)
.fill(Color.indigo)
.frame(width: 100, height: 100)
.offset(x: dragOffset.width, y: dragOffset.height)
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation
}
.onEnded { _ in
withAnimation(.spring()) {
dragOffset = .zero
}
}
).onChanged fires every frame while dragging — updates position in real time. .onEnded resets to .zero — the square springs back home.
4. MagnifyGesture
Detects a pinch. Gives you a scale multiplier — 1.0 is normal, 2.0 is double, 0.5 is half.
@State var scale: CGFloat = 1.0
Image(systemName: "star.fill")
.font(.system(size: 80))
.foregroundColor(.yellow)
.scaleEffect(scale)
.gesture(
MagnifyGesture()
.onChanged { value in
scale = value.magnification
}
.onEnded { _ in
withAnimation { scale = 1.0 }
}
)Pinch to grow or shrink the star. Let go and it bounces back to normal.
5. RotateGesture
Detects two fingers twisting. Returns a rotation Angle.
@State var angle: Angle = .zero
Rectangle()
.fill(Color.mint)
.frame(width: 120, height: 120)
.rotationEffect(angle)
.gesture(
RotateGesture()
.onChanged { value in
angle = value.rotation
}
.onEnded { _ in
withAnimation { angle = .zero }
}
)Twist two fingers on the square. Release to spring back to original rotation.
IV. Key Concepts
CGSize
A Core Graphics struct holding two numbers: width and height. In drag gestures, it stores how far the finger moved — width for horizontal, height for vertical. CGSize.zero (or just .zero) means no movement, and is used both as the starting value and the reset value.
withAnimation
Wrapping a state change in withAnimation makes the transition smooth. Without it, the view snaps instantly. With it, SwiftUI interpolates between the old and new values.
.spring()for bouncy.easeInOutfor smooth.linearfor constant speed
Gestures don't move views
Gestures only report data — they don't change anything visual on their own. You take the data and apply it through modifiers like .offset(), .scaleEffect(), or .rotationEffect(). The gesture is the sensor; the modifier is the actuator.