~/lxcidfollow

← writing/

Why UITapGestureRecognizer Has No Touch-Down State

By Stan Chang · · Updated · 4 min read · #ios-development

Why UITapGestureRecognizer Has No Touch-Down State

Discrete recognition, continuous feedback, and the boundary with UIButton

In 2015, I used a long-press recognizer to make an arbitrary view behave like a button. The state-model observation was sound. The abstraction was not.

The important distinction is not tap versus long press. It is discrete versus continuous recognition.

  • A discrete recognizer moves from possible to recognized or failed. It does not have a began phase.
  • A continuous recognizer can fail before it begins, or move through began and changed before it reaches ended or cancelled.

That difference explains why a UITapGestureRecognizer cannot highlight a view as soon as the finger touches down: it recognizes the tap only after the touch sequence succeeds.

The gesture-recognizer state machine

State machines for discrete and continuous gesture recognizers

The discrete and continuous state paths described by Apple's current UIGestureRecognizer.state documentation.

Apple’s state documentation defines the paths precisely:

Kind State path Example
Discrete possiblerecognized or failed tap, swipe
Continuous possiblefailed, or possiblebegan → zero or more changedended or cancelled pan, pinch, long press

A discrete recognizer sends one action only when it reaches recognized. A continuous recognizer sends actions at began and each later changed, ended, or cancelled transition. A transition from possible to failed sends no action, so a target-action handler never receives .failed.

When a continuous recognizer is appropriate

UILongPressGestureRecognizer is continuous. It enters began after minimumPressDuration, emits changed as the finger moves, and finishes with ended when the finger lifts. For a custom press surface, setting the duration to zero provides state updates from the start of the press:

let press = UILongPressGestureRecognizer(
    target: self,
    action: #selector(handlePress(_:))
)
press.minimumPressDuration = 0
customView.addGestureRecognizer(press)

@objc private func handlePress(_ gesture: UILongPressGestureRecognizer) {
    guard let view = gesture.view else { return }

    let location = gesture.location(in: view)
    let isInside = view.bounds.contains(location)

    switch gesture.state {
    case .began, .changed:
        view.alpha = isInside ? 0.6 : 1
    case .ended:
        view.alpha = 1
        if isInside {
            activateCustomInteraction()
        }
    case .cancelled:
        view.alpha = 1
    default:
        break
    }
}

This deliberately turns a long-press recognizer into a general press tracker. It is useful only when the interaction genuinely needs gesture states. It can also compete with scrolling or other recognizers, so coordinate recognizer dependencies when the view sits inside a larger gesture system.

If it acts like a button, use a button

The original version of this article used a custom recognizer to imitate UIControl events. That is unnecessary for a normal tap action and makes it easier to miss accessibility, focus, pointer, keyboard, cancellation, and disabled-state behavior.

Use UIButton or another UIControl when the surface activates an action. A custom button can still own any visual treatment:

final class HighlightingButton: UIButton {
    override var isHighlighted: Bool {
        didSet {
            alpha = isHighlighted ? 0.6 : 1
        }
    }
}

What the original experiment taught me

The long-press technique came from inspecting how UISwitch handled press feedback. The useful observation was the natural mapping between continuous gesture states and control touch phases: began can start feedback, changed can track whether the finger remains inside, and ended or cancelled can clear it.

The first version of this article took that experiment further and built LXTouchGestureRecognizer to imitate UIControl events. It was a useful way to understand the state machine, but it also rebuilt behavior UIKit already provides. The current recommendation keeps the diagnostic insight and retires the unnecessary abstraction.

The original Objective-C implementation remains available in my gist. Sumolari was kind enough to turn the experiment into a repository and publish it as the LXTouchGestureRecognizer CocoaPod. Thank you, Sumolari, for doing the work to make it installable for others.

Historical code: I no longer maintain LXTouchGestureRecognizer and do not endorse it for new projects. The gist, repository, and pod are preserved here for provenance and for anyone maintaining legacy code.

The reusable rule is simple: choose a recognizer for gesture semantics and a control for control semantics. The state machine tells you why a tap recognizer cannot provide touch-down feedback; it does not require rebuilding a button.

Apple documentation