SwiftUI Gestures
Comprehensive guide to SwiftUI gesture recognition with composition patterns, state management, and accessibility integration.
When to Use This Skill
- Implementing tap, drag, long press, magnification, or rotation gestures
- Composing multiple gestures (simultaneously, sequenced, exclusively)
- Managing gesture state with GestureState
- Creating custom gesture recognizers
- Debugging gesture conflicts or unresponsive gestures
- Making gestures accessible with VoiceOver
- Cross-platform gesture handling (iOS, macOS, axiom-visionOS)
Example Prompts
These are real questions developers ask that this skill is designed to answer:
1. "My drag gesture isn't working - the view doesn't move when I drag it. How do I debug this?"
โ The skill covers DragGesture state management patterns and shows how to properly update view offset with @GestureState
2. "I have both a tap gesture and a drag gesture on the same view. The tap works but the drag doesn't. How do I fix this?"
โ The skill demonstrates gesture composition with .simultaneously, .sequenced, and .exclusively to resolve gesture conflicts
3. "I want users to long press before they can drag an item. How do I chain gestures together?"
โ The skill shows the .sequenced pattern for combining LongPressGesture with DragGesture in the correct order
4. "My gesture state isn't resetting when the gesture ends. The view stays in the wrong position."
โ The skill covers @GestureState automatic reset behavior and the updating parameter for proper state management
5. "VoiceOver users can't access features that require gestures. How do I make gestures accessible?"
โ The skill demonstrates .accessibilityAction patterns and providing alternative interactions for VoiceOver users
Choosing the Right Gesture (Decision Tree)
What interaction do you need?
โโ Single tap/click?
โ โโ Use Button (preferred) or TapGesture
โ
โโ Drag/pan movement?
โ โโ Use DragGesture
โ
โโ Hold before action?
โ โโ Use LongPressGesture
โ
โโ Pinch to zoom?
โ โโ Use MagnificationGesture
โ
โโ Two-finger rotation?
โ โโ Use RotationGesture
โ
โโ Multiple gestures together?
โ โโ Both at same time? โ .simultaneously
โ โโ One after another? โ .sequenced
โ โโ One OR the other? โ .exclusively
โ
โโ Complex custom behavior?
โโ Create custom Gesture conforming to Gesture protocol
Pattern 1: Basic Gesture Recognition
TapGesture
โ WRONG (Custom tap on non-semantic view)
Text("Submit")
.onTapGesture {
submitForm()
}
Problems:
- Not announced as button to VoiceOver
- No visual press feedback
- Doesn't respect accessibility settings
โ
CORRECT (Use Button for tap actions)
Button("Submit") {
submitForm()
}
.buttonStyle(.bordered)
When to use TapGesture: Only when you need tap data (location, count) or non-standard tap behavior:
Image("map")
.onTapGesture(count: 2) {
showDetails()
}
.onTapGesture { location in
addPin(at: location)
}
DragGesture
โ WRONG (Direct state mutation in gesture)
@State private var offset = CGSize.zero
var body: some View {
Circle()
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
)
}
Problems:
- View updates on every drag event (60-120 times per second)
- No way to reset to original position
- Loses intermediate state if drag cancelled
โ
CORRECT (Use GestureState for temporary state)
@GestureState private var dragOffset = CGSize.zero
@State private var position = CGSize.zero
var body: some View {
Circle()
.offset(x: position.width + dragOffset.width,
y: position.height + dragOffset.height)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
}
)
}
Why: GestureState automatically resets to initial value when gesture ends, preventing state corruption.
LongPressGesture
@GestureState private var isDetectingLongPress = false
@State private var completedLongPress = false
var body: some View {
Text("Press and hold")
.foregroundStyle(isDetectingLongPress ? .red : .blue)
.gesture(
LongPressGesture(minimumDuration: 1.0)
.updating($isDetectingLongPress) { currentState, gestureState, _ in
gestureState = currentState
}
.onEnded { _ in
completedLongPress = true
}
)
}
Key parameters:
minimumDuration: How long to hold (default 0.5 seconds)
maximumDistance: How far finger can move before cancelling (default 10 points)
MagnificationGesture
@GestureState private var magnificationAmount = 1.0
@State private var currentZoom = 1.0
var body: some View {
Image("photo")
.scaleEffect(currentZoom * magnificationAmount)
.gesture(
MagnificationGesture()
.updating($magnificationAmount) { value, state, _ in
state = value.magnification
}
.onEnded { value in
currentZoom *= value.magnification
}
)
}
Platform notes:
- iOS: Pinch gesture with two fingers
- macOS: Trackpad pinch
- visionOS: Pinch gesture in 3D space
RotationGesture
@GestureState private var rotationAngle = Angle.zero
@State private var currentRotation = Angle.zero
var body: some View {
Rectangle()
.fill(.blue)
.frame(width: 200, height: 200)
.rotationEffect(currentRotation + rotationAngle)
.gesture(
RotationGesture()
.updating($rotationAngle) { value, state, _ in
state = value.rotation
}
.onEnded { value in
currentRotation += value.rotation
}
)
}
Pattern 2: Gesture Composition
Simultaneous Gestures
Use when: Two gestures should work at the same time
@GestureState private var dragOffset = CGSize.zero
@GestureState private var m