SwiftUI gestures look simple on the surface:
.onTapGesture { }
But under the hood, SwiftUI has a powerful, layered gesture system that decides:
- which gesture wins
- which gesture fails
- which gesture runs simultaneously
- when gestures cancel each other
- how gestures propagate through the view tree
Most gesture bugs happen because developers donβt understand gesture precedence and resolution.
This post breaks down how SwiftUI gestures actually work, from the engine level β so you can build reliable, predictable, complex interactions.
π§ The Core Gesture Model
SwiftUI gestures follow this pipeline:
Touch input
β
Hit-testing
β
Gesture recognition
β
Gesture competition
β
Resolution (win / fail / simultaneous)
β
State updates
Multiple gestures can observe the same touch β but not all will win.
π 1. Gesture Types in SwiftUI
SwiftUI provides several primitive gestures:
TapGestureLongPressGestureDragGestureMagnificationGestureRotationGesture
These are value types, composed into the view tree.
π§© 2. Gestures Are Attached to Views β Not the Screen
Text("Hello")
.onTapGesture { print("Tapped") }
This gesture exists only where the view is hit-tested.
Key rule:
π If a view has no size or is transparent to hit-testing, its gesture wonβt fire.
Use:
.contentShape(Rectangle())
to define a tappable area.
βοΈ 3. Gesture Competition: Who Wins?
When multiple gestures detect the same touch, SwiftUI resolves them in this order:
- Exclusive gestures (default)
- High-priority gestures
- Simultaneous gestures
- Parent gestures
By default:
The deepest child gesture wins.
π₯ 4. highPriorityGesture
Overrides child gesture precedence.
.view
.highPriorityGesture(
TapGesture().onEnded { print("Parent tap") }
)
Use when:
- parent must intercept touches
- child interactions must not block parent logic
Be careful β this can break expected UX.
π€ 5. simultaneousGesture
Allows gestures to fire together.
.view
.simultaneousGesture(
TapGesture().onEnded { print("Also tapped") }
)
Use for:
- analytics
- haptics
- secondary effects
- logging interactions
This does not block other gestures.
π 6. Composing Gestures
SwiftUI lets you combine gestures explicitly.
Sequence (one after another)
LongPressGesture()
.sequenced(before: DragGesture())
Simultaneous
TapGesture()
.simultaneously(with: LongPressGesture())
Exclusive
TapGesture()
.exclusively(before: DragGesture())
This gives full control over gesture flow.
π 7. Gesture State vs View State
Use @GestureState for temporary gesture values.
@GestureState private var dragOffset = CGSize.zero
Key properties:
- resets automatically
- does not trigger permanent state updates
- perfect for animations
Example:
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
π Use @GestureState for motion, @State for results.
π 8. Gesture Lifecycle
Every gesture has phases:
.onChanged.onEnded.updating
Internally:
- gestures can fail
- gestures can cancel
- gestures can restart
This is why gestures sometimes feel βjumpyβ when identity changes.
π§± 9. Gesture Propagation & View Identity
If a view is recreated:
- gestures are recreated
- gesture state resets
- in-progress gestures cancel
Common causes:
- changing
id() - conditional views
- list identity issues
- parent invalidations
π Stable identity = stable gesture behavior.
π 10. ScrollView vs Gestures
ScrollView owns a high-priority drag gesture.
This is why:
- child drag gestures sometimes donβt fire
- custom swipe gestures feel broken
Solutions:
- use
simultaneousGesture - attach gesture to overlay
- disable scrolling temporarily
- use
gesture(_:including:)
Example:
.gesture(drag, including: .subviews)
β οΈ 11. Common Gesture Bugs (And Fixes)
β Gesture not firing
- View has zero size
- Missing
contentShape - Hit-testing disabled
β Gesture cancels randomly
- View identity changed
- Parent re-rendered
- Navigation transition
β Scroll conflicts
- Competing drag gestures
- Wrong priority
Understanding the system fixes all of these.
π§ Mental Model Cheat Sheet
- Gestures live on views
- Identity matters
- Children win by default
- Priority changes behavior
- Simultaneous gestures donβt block
- GestureState is ephemeral
- ScrollView is aggressive
- Layout affects hit-testing
π Final Thoughts
SwiftUI gestures are not magic β theyβre a deterministic system.
Once you understand:
- competition
- priority
- identity
- propagation
You can build:
- swipe actions
- custom sliders
- drag-to-dismiss
- multi-touch interactions
- advanced animations
β¦without fighting SwiftUI.
Top comments (0)