Tools
SwiftUI Gesture System Internals
2025-12-24
0 views
admin
π§ The Core Gesture Model ## π 1. Gesture Types in SwiftUI ## π§© 2. Gestures Are Attached to Views β Not the Screen ## βοΈ 3. Gesture Competition: Who Wins? ## π₯ 4. highPriorityGesture ## π€ 5. simultaneousGesture ## π 6. Composing Gestures ## Sequence (one after another) ## Simultaneous ## Exclusive ## π 7. Gesture State vs View State ## π 8. Gesture Lifecycle ## π§± 9. Gesture Propagation & View Identity ## π 10. ScrollView vs Gestures ## β οΈ 11. Common Gesture Bugs (And Fixes) ## π§ Mental Model Cheat Sheet ## π Final Thoughts SwiftUI gestures look simple on the surface: But under the hood, SwiftUI has a powerful, layered gesture system that decides: 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. SwiftUI gestures follow this pipeline: Multiple gestures can observe the same touch β but not all will win. SwiftUI provides several primitive gestures: These are value types, composed into the view tree. 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. to define a tappable area. When multiple gestures detect the same touch, SwiftUI resolves them in this order: The deepest child gesture wins. Overrides child gesture precedence. Be careful β this can break expected UX. Allows gestures to fire together. This does not block other gestures. SwiftUI lets you combine gestures explicitly. This gives full control over gesture flow. Use @GestureState for temporary gesture values. π Use @GestureState for motion, @State for results. Every gesture has phases: This is why gestures sometimes feel βjumpyβ when identity changes. If a view is recreated: π Stable identity = stable gesture behavior. ScrollView owns a high-priority drag gesture. β Gesture cancels randomly Understanding the system fixes all of these. SwiftUI gestures are not magic β theyβre a deterministic system. β¦without fighting SwiftUI. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
.onTapGesture { } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
.onTapGesture { } CODE_BLOCK:
.onTapGesture { } CODE_BLOCK:
Touch input β
Hit-testing β
Gesture recognition β
Gesture competition β
Resolution (win / fail / simultaneous) β
State updates Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
Touch input β
Hit-testing β
Gesture recognition β
Gesture competition β
Resolution (win / fail / simultaneous) β
State updates CODE_BLOCK:
Touch input β
Hit-testing β
Gesture recognition β
Gesture competition β
Resolution (win / fail / simultaneous) β
State updates CODE_BLOCK:
Text("Hello") .onTapGesture { print("Tapped") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
Text("Hello") .onTapGesture { print("Tapped") } CODE_BLOCK:
Text("Hello") .onTapGesture { print("Tapped") } CODE_BLOCK:
.contentShape(Rectangle()) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
.contentShape(Rectangle()) CODE_BLOCK:
.contentShape(Rectangle()) CODE_BLOCK:
.view .highPriorityGesture( TapGesture().onEnded { print("Parent tap") } ) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
.view .highPriorityGesture( TapGesture().onEnded { print("Parent tap") } ) CODE_BLOCK:
.view .highPriorityGesture( TapGesture().onEnded { print("Parent tap") } ) CODE_BLOCK:
.view .simultaneousGesture( TapGesture().onEnded { print("Also tapped") } ) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
.view .simultaneousGesture( TapGesture().onEnded { print("Also tapped") } ) CODE_BLOCK:
.view .simultaneousGesture( TapGesture().onEnded { print("Also tapped") } ) CODE_BLOCK:
LongPressGesture() .sequenced(before: DragGesture()) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
LongPressGesture() .sequenced(before: DragGesture()) CODE_BLOCK:
LongPressGesture() .sequenced(before: DragGesture()) CODE_BLOCK:
TapGesture() .simultaneously(with: LongPressGesture()) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
TapGesture() .simultaneously(with: LongPressGesture()) CODE_BLOCK:
TapGesture() .simultaneously(with: LongPressGesture()) CODE_BLOCK:
TapGesture() .exclusively(before: DragGesture()) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
TapGesture() .exclusively(before: DragGesture()) CODE_BLOCK:
TapGesture() .exclusively(before: DragGesture()) CODE_BLOCK:
@GestureState private var dragOffset = CGSize.zero Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
@GestureState private var dragOffset = CGSize.zero CODE_BLOCK:
@GestureState private var dragOffset = CGSize.zero CODE_BLOCK:
DragGesture() .updating($dragOffset) { value, state, _ in state = value.translation } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
DragGesture() .updating($dragOffset) { value, state, _ in state = value.translation } CODE_BLOCK:
DragGesture() .updating($dragOffset) { value, state, _ in state = value.translation } CODE_BLOCK:
.gesture(drag, including: .subviews) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
.gesture(drag, including: .subviews) CODE_BLOCK:
.gesture(drag, including: .subviews) - which gesture wins
- which gesture fails
- which gesture runs simultaneously
- when gestures cancel each other
- how gestures propagate through the view tree - LongPressGesture
- DragGesture
- MagnificationGesture
- RotationGesture - Exclusive gestures (default)
- High-priority gestures
- Simultaneous gestures
- Parent gestures - parent must intercept touches
- child interactions must not block parent logic - secondary effects
- logging interactions - resets automatically
- does not trigger permanent state updates
- perfect for animations - gestures can fail
- gestures can cancel
- gestures can restart - gestures are recreated
- gesture state resets
- in-progress gestures cancel - changing id()
- conditional views
- list identity issues
- parent invalidations - child drag gestures sometimes donβt fire
- custom swipe gestures feel broken - use simultaneousGesture
- attach gesture to overlay
- disable scrolling temporarily
- use gesture(_:including:) - View has zero size
- Missing contentShape
- Hit-testing disabled - View identity changed
- Parent re-rendered
- Navigation transition - Competing drag gestures
- Wrong priority - 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 - competition
- propagation - swipe actions
- custom sliders
- drag-to-dismiss
- multi-touch interactions
- advanced animations
how-totutorialguidedev.toaiml