All posts
Flutter vs Native7 min read

Flutter gesture system vs UIGestureRecognizer. Why I miss UIKit sometimes.

GestureDetector, the gesture arena, and the things UIGestureRecognizer does without thinking. A deep dive with Swift and Dart side by side.

I once spent three days trying to make a Flutter widget recognize a horizontal swipe and a long press at the same time. The UIKit equivalent in Swift was eight lines and a delegate method. The Flutter version was a custom gesture recognizer subclass, an arena participant, and a bug report I never closed. I love Flutter. I miss UIKit's gesture system specifically.

This post is the technical version of that frustration. I will explain how Flutter's gesture arena works, what GestureDetector actually does, where it falls short of UIGestureRecognizer, and where Flutter's design is genuinely better. With code from both worlds running side by side.

Context: two different philosophies

UIKit's gesture system is a per-recognizer state machine plus a delegate that adjudicates conflicts. Each UIGestureRecognizer lives independently and reports its state. A delegate method, gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:), lets you say "yes, both of these can fire at once."

Flutter's gesture system is an arena. When a pointer goes down, all the GestureRecognizers whose hit-test areas contain that point enter an arena. As the pointer moves, each recognizer either claims victory (the others are dismissed) or rejects itself. If the pointer lifts before anyone claims victory, the arena holds an "election" and the longest-running participant wins by default.

The result: in Flutter, two gesture recognizers cannot fire on the same touch unless one explicitly defers to the other. Simultaneous recognition is the exception, not a configurable default.

The simple case

Both work. Behind the scenes, Flutter constructs both recognizers and lets the arena decide based on timing. UIKit does the same. The simple case is identical.

The hard case: simultaneous recognition

In UIKit, you can have a pan and a pinch on a map work at the same time:

In Flutter, the equivalent requires RawGestureDetector and a custom GestureRecognizer because GestureDetector arena rules dismiss losing recognizers:

This works. It is also a lot more code than the UIKit version. The reason is philosophical: Flutter wants disputes resolved deterministically, not by a delegate's opinion. That is good for predictability and bad for ergonomics when you genuinely want both gestures.

Why the arena exists

The arena prevents an entire class of bugs that UIKit lets through. If a child view recognizes a tap and a parent recognizes a swipe, UIKit can fire both unless you write code to prevent it. Flutter's default is one winner, no surprises.

The arena also handles ambiguity well. Drag, swipe, and pan are similar; the arena waits long enough to tell them apart. UIKit handles this with requireGestureRecognizerToFail, which is more configuration than Flutter usually needs:

For the everyday case, Flutter is less code. For the edge case, UIKit is less code. That is the trade.

The arena, drawn

Caption: the lifecycle of a gesture in Flutter's arena. Compare to UIKit, where each recognizer tracks its own state independently and conflicts are resolved by delegate methods.

UIKit gesture priority diagram

Caption: UIKit gesture recognition has more knobs and more ways to combine recognizers. The default behaviour requires explicit delegate code to enable simultaneous recognition.

What Flutter genuinely does better

The gesture arena makes nested-scrollable behaviour more predictable. A list inside a list inside a sheet works in Flutter without writing one line of disambiguation code. UIKit needs UIScrollViewDelegate work and sometimes a gestureRecognizer:shouldRequireFailureOf: override.

Flutter also makes hit-testing more controllable. HitTestBehavior.translucent, opaque, and deferToChild give you precise control over whether a widget participates in the arena at all. UIKit's equivalent is isUserInteractionEnabled and overriding hitTest(_:with:), which is more powerful but more dangerous.

What I miss from UIKit

  • Per-recognizer state inspection. In UIKit I can ask recognizer.state at any time. In Flutter I track that myself if I need it.
  • The requireGestureRecognizerToFail chain. It is more explicit than the arena's "wait and see" approach when I need a specific ordering.
  • Simultaneous gestures by default for pan + pinch + rotate. Maps and image editors are the canonical case.
  • The recognizer's relationship with the run loop. In UIKit, gesture timing is tied to the same loop as scroll updates and feels coherent.

Where the gap shows up most

Custom interactive widgets are the place. A draggable card with a swipe-to-dismiss behaviour and a long-press-to-edit affordance is straightforward in UIKit and fiddly in Flutter. You usually end up with RawGestureDetector and either custom recognizers or one big custom widget that owns all the gestures.

The Flutter Dismissible widget is excellent for the simple case. Once you need to add anything beyond what its API exposes — a long press, a custom resistance curve, a haptic at a specific point — you are writing your own gesture logic.

What I would do differently

  • I would have read lib/src/gestures/ in the Flutter source before fighting the arena. An hour there explained a week of confusion.
  • I would have used Listener for low-level pointer cases instead of trying to bend GestureDetector. Listener gives you raw PointerEvents and bypasses the arena.
  • I would not have tried to build a card-stack drag with nested GestureDetectors. One RawGestureDetector at the top is cleaner.
  • I would have written widget tests with WidgetTester.drag and tester.timedDrag from the start. Gestures are easy to break and hard to notice.
  • I would not have shipped a GestureDetector wrapping another GestureDetector with overlapping gesture types. The arena's behaviour there is correct but surprising.

Closing opinion

Flutter's arena is the right design for most of what most apps need. UIKit's recognizer system is the right design for advanced interactions where simultaneous gestures matter. If you are building maps, image editors, or anything pinch-and-pan-heavy, Flutter will cost you a week you did not budget. For the iOS feel story more broadly, see Flutter on iOS still does not feel native. For where this fits with the back-gesture story on Android, see Android back gesture handling in Flutter vs native.

Written by the author of Flutterstacks

A developer who shipped production apps in Swift, Kotlin, and Dart — with a genuine native reference point that most Flutter writers simply don't have.

More articles →

Continue reading

You may also enjoy

Flutter vs Native

I have shipped apps in Swift, Kotlin, and Flutter. Here is my unfiltered verdict after 18 months.

After eighteen months of shipping the same product surface in Swift, Kotlin, and Flutter, here is what actually held up in production and what did not.

Read article
Flutter vs Native

Flutter on iOS still does not feel native. Here is exactly why.

A line-by-line breakdown of where Flutter on iOS diverges from UIKit feel: scroll physics, text selection, keyboard, and font rendering. With code to close the gap.

Read article