Conditional gestures in SwiftUI

⇐ Notes archive

(This is an entry in my technical diary. There will likely be typos, mistakes, or wider logical leaps — the intent here is to “[let] others look over my shoulder while I figure things out.”)

Conditionally enabling a gesture in SwiftUI wasn’t as intuitive for me compared to other modifiers. Showing or hiding a view is an .opacity(someCondition ? 1 : 0) (or if-else in a ViewBuilder or .hidden()) away. But for gestures? It felt off to have to .gesture(someCondition ? DragGesture().onChanged { /* … */ }.onEnded { /* … */ } : DragGesture()), where the first branch returns a live gesture and the second, an inert one. The types returned from the ternary need to line up — there’s an AnyGesture eraser in the framework that usually helps in these situations, yet it still begs the question of which instance to erase in the disabled case.

A search online for “SwiftUI disable gesture” tops out with Paul Hudson’s post, “Disabling user interactivity with allowsHitTesting(_:)” and while that modifier works in some situations, it was too coarse for the one I was in. I needed to disable a drag gesture and keep a tap gesture on the same view in tact and allowsHitTesting wholesale disables both.

Poking around the Gesture protocol’s listed conformances had the answer I was looking for — Optional conditionally conforms to Gesture!

And Harlan Haskins helped me tidy the above ternary to .gesture(someCondition ? DragGesture().onChanged { /* … */ }.onEnded { /* … */ } : nil). What’s wicked here is the gesture is of type _EndedGesture<_ChangedGesture<DragGesture>> and Swift is able to promote the nil to an optionally-wrapped version without any added annotations.