Fix: `UITapGestureRecognizer` Won’t Fire Programmatically

Fix: `UITapGestureRecognizer` Won't Fire Programmatically

Written by

in

Quick answer: A UITapGestureRecognizer is not triggered by calling a UIView event method; UIView has no sendActionForEvent API. Put the tap behavior in a method you can call directly from a unit test, or use an XCTest UI test to tap the view and exercise the recognizer through UIKit.

Why this happens

A UITapGestureRecognizer responds when UIKit recognizes a tap on its attached view. It is not a UIControl, so you cannot make it recognize a tap by calling a control-style event-sending method. UIView.sendActionForEvent is not a UIKit API, and a gesture recognizer does not expose a method for injecting a completed tap into its recognition process.

There are two different test goals that often get mixed together. A unit test can check what your app does after a tap, without involving UIKit’s gesture recognition. A UI test can perform an actual simulated tap on the screen, allowing UIKit to hit-test the view and run the recognizer. Choose the test type that matches what you need to verify.

Also check the recognizer setup itself. The view must have the recognizer attached with addGestureRecognizer(_:), and the target must implement the selector passed to the recognizer. A view such as a label or image view may also need user interaction enabled.

How to fix it

1. Attach the recognizer with a valid target and action

Use addGestureRecognizer(_:) and #selector rather than the old string-based selector syntax. In this example, the view controller is the target, so it implements the action method:

import UIKit

final class ExampleViewController: UIViewController {
    private let tapArea = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()

        tapArea.isUserInteractionEnabled = true
        tapArea.addGestureRecognizer(
            UITapGestureRecognizer(
                target: self,
                action: #selector(handleTap(_:))
            )
        )
    }

    @objc private func handleTap(_ recognizer: UITapGestureRecognizer) {
        performTapAction()
    }

    func performTapAction() {
        // Put the behavior for a tap here.
    }
}

The @objc annotation makes the action available to the Objective-C runtime, which UIKit uses to dispatch target-action callbacks. The selector #selector(handleTap(_:)) must match the method’s name and parameter. The recognizer passes itself to the action, so the parameter should be a UITapGestureRecognizer.

UIView.isUserInteractionEnabled is normally enabled for a plain UIView, but some view types, including UIImageView and UILabel, default to having interaction disabled. Enable it on the view receiving the gesture when necessary. Attach the recognizer to the view that should receive the tap, not just to a view that happens to contain it.

2. Test the tap behavior directly in a unit test

If the test only needs to verify the app’s response, call the behavior method rather than trying to manufacture a gesture event. Keeping the behavior in a separate method makes the test independent of UIKit’s hit-testing and gesture-recognition machinery:

func testTapActionUpdatesState() {
    let controller = ExampleViewController()

    controller.performTapAction()

    // Assert the state or output changed as expected.
}

In a real app, make the assertion against observable behavior, such as a view model property or a callback. Avoid adding a test-only method that pretends a gesture was recognized: calling the behavior method verifies the behavior, but does not verify that the recognizer is configured or attached correctly.

You can call an action method directly if that is specifically what you want to test. For example, a handler declared as @objc func handleTap(_ sender: UITapGestureRecognizer? = nil) can be called with handleTap(). However, this only runs the method. It does not simulate a tap, populate a real recognizer state, or check that UIKit would deliver the action. Separating the tap behavior from the UIKit callback is usually clearer.

3. Use a UI test to exercise a real tap

If you need to verify that a user can tap the view, use an XCTest UI test. Give the target a stable accessibility identifier, then tap the matching element in the app:

let target = app.descendants(matching: .any)
    .matching(identifier: "tapTarget")
    .firstMatch

XCTAssertTrue(target.waitForExistence(timeout: 2))
target.tap()

Set the identifier on the view in the app:

tapArea.accessibilityIdentifier = "tapTarget"

A UI test sends an interaction through the running app, so it can catch problems that a direct method call cannot, such as a view being covered or the gesture being attached to the wrong view. The element must be available to the accessibility hierarchy for XCTest to find it. If your view is not exposed as a distinct element, adjust its accessibility configuration or test a suitable accessible control instead.

4. Use a control for control-like behavior

If the view represents a button or another standard action, prefer a UIButton or another suitable UIControl over a tap recognizer. Controls provide their own target-action events, which can be sent with APIs such as sendActions(for:) in tests. That API belongs to UIControl; it is not a way to activate a UITapGestureRecognizer attached to an arbitrary UIView.

A gesture recognizer is more appropriate when the interaction is genuinely a gesture, such as tapping a non-button canvas or recognizing a double tap. Choosing a control for a button-like action also improves the default accessibility and interaction behavior.

How to prevent it

  • Use addGestureRecognizer(_:); do not use addGesture or sendActionForEvent on a UIView.
  • Keep the selector and handler signature in sync, and use #selector with an @objc action method.
  • Enable isUserInteractionEnabled on views that need it, especially labels and image views.
  • Unit-test the app behavior directly; use UI tests when you need to verify gesture attachment and hit-testing.
  • Use UIControl and its control events for button-like actions.

FAQ

Can I call the gesture recognizer’s action method manually?

You can call a handler method directly if it is accessible, but that only executes its code. It does not make UIKit recognize a tap or prove that the recognizer is connected correctly.

Why does sendActions(for:) work for a button but not a tap recognizer?

sendActions(for:) is a UIControl API for control events. A UITapGestureRecognizer is a different UIKit object; UIKit invokes its target-action callback after recognizing a gesture.

Does a tap recognizer work on a UILabel or UIImageView?

Yes, provided the recognizer is attached to that view and user interaction is enabled. These view types do not enable interaction by default, so set isUserInteractionEnabled = true when needed.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.