Fix: Keyboard Won’t Dismiss on Tap Outside in Swift

Fix: Keyboard Won't Dismiss on Tap Outside in Swift

Written by

in

Quick answer: To dismiss the keyboard when tapping anywhere outside an active text input, attach a UITapGestureRecognizer to your view controller’s root view in viewDidLoad and call view.endEditing(true). Make sure to set cancelsTouchesInView = false so other interactive controls, such as buttons and table cells, still receive their touch events.

Why this happens

By default, UIKit does not automatically dismiss the on-screen keyboard when a user touches outside an active UITextField or UITextView. An input view presents the software keyboard when it becomes the first responder (becomeFirstResponder()), and UIKit retains that keyboard on screen until the input explicitly yields its status by calling resignFirstResponder() or the container view calls endEditing(_:).

Because UIKit views do not listen for background touches to clear responder status, tapping empty view space produces no action. Furthermore, developers who attempt to solve this by attaching an unconfigured tap gesture recognizer often discover that buttons, table view cells, or collection view items stop registering user taps because the gesture interceptor absorbs touch events.

Handling keyboard dismissal cleanly requires signaling the view hierarchy to resign first responder status while preserving interaction across all other subviews.

How to fix it

1. Add a tap gesture recognizer with endEditing

The most robust approach in standard view controllers is to attach a UITapGestureRecognizer to view inside viewDidLoad(). Calling view.endEditing(true) forces whichever text field or text view currently owns focus to resign first responder status, without requiring you to maintain explicit references to individual text inputs.

Crucially, you must set cancelsTouchesInView = false. Without this flag, the tap gesture intercepts touches meant for buttons, segmented controls, or navigation elements.

import UIKit

class LoginViewController: UIViewController {

    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupKeyboardDismissTap()
    }

    private func setupKeyboardDismissTap() {
        let tapGesture = UITapGestureRecognizer(
            target: self,
            action: #selector(handleBackgroundTap)
        )
        tapGesture.cancelsTouchesInView = false
        view.addGestureRecognizer(tapGesture)
    }

    @objc private func handleBackgroundTap() {
        view.endEditing(true)
    }
}

By passing true to endEditing(_:), UIKit queries all subviews in the view hierarchy and forces any active text responder to resign immediately.

2. Override touchesBegan in the UIViewController

If your view hierarchy contains plain background views without complex scroll views or interactive controls, you can override touchesBegan(_:with:) directly on your UIViewController.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
    view.endEditing(true)
}

While concise, this technique has limitations: touchesBegan does not trigger when the user taps inside subviews that intercept touches (such as UIScrollView, UITableView, or custom interactive control surfaces). It works best for simple, flat layout screens.

3. Enable keyboardDismissMode on scroll views and table views

When text fields live inside a UIScrollView, UITableView, or UICollectionView, standard tap gestures can conflict with scroll momentum or row selection. UIKit provides a built-in property on UIScrollView designed specifically for this scenario: keyboardDismissMode.

Configure your scroll view to dismiss the keyboard interactively or on drag:

override func viewDidLoad() {
    super.viewDidLoad()

    // Automatically dismisses when the user drags the scroll view
    tableView.keyboardDismissMode = .onDrag
}

If you prefer dismissing immediately upon scrolling rather than requiring an explicit drag motion, use .interactive, which ties dismissal directly to the finger’s vertical offset relative to the keyboard.

To also handle taps inside non-scrolling areas of a table view, add the gesture recognizer to the tableView.backgroundView rather than the table view itself, ensuring cell selection logic remains uninterrupted.

4. Create a reusable UIViewController extension

To avoid repeating boilerplate across multiple view controllers, encapsulate the gesture configuration in an extension:

import UIKit

extension UIViewController {
    func hideKeyboardWhenTappedAround() {
        let tap = UITapGestureRecognizer(
            target: self,
            action: #selector(UIViewController.dismissKeyboard)
        )
        tap.cancelsTouchesInView = false
        view.addGestureRecognizer(tap)
    }

    @objc private func dismissKeyboard() {
        view.endEditing(true)
    }
}

In any view controller where text input occurs, invoke the helper once:

override func viewDidLoad() {
    super.viewDidLoad()
    hideKeyboardWhenTappedAround()
}

5. Managing focus in SwiftUI

If your project uses SwiftUI or hosts a UIKit view controller in a hybrid hierarchy, use @FocusState to manage keyboard focus declaratively rather than using UIKit gesture recognizers:

import SwiftUI

struct FormView: View {
    enum Field {
        case email
        case password
    }

    @State private var email = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        VStack(spacing: 16) {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)
                .textFieldStyle(.roundedBorder)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
                .textFieldStyle(.roundedBorder)

            Spacer()
        }
        .padding()
        .contentShape(Rectangle())
        .onTapGesture {
            focusedField = nil
        }
    }
}

Setting focusedField = nil causes the active field to lose focus, dismissing the software keyboard automatically.

How to prevent it

  • Configure cancelsTouchesInView = false on every background tap gesture recognizer to prevent dead touches on buttons and controls.
  • Call view.endEditing(true) on the container view rather than maintaining explicit references to individual text fields.
  • Set keyboardDismissMode = .onDrag on any UIScrollView or UITableView that contains editable inputs.
  • Ensure background views have isUserInteractionEnabled = true if you attach custom gesture recognizers directly to secondary container views.

FAQ

Why does my UIButton stop working after adding a tap gesture to dismiss the keyboard?

By default, UITapGestureRecognizer sets cancelsTouchesInView = true, which intercepts touch sequences and prevents them from reaching subviews. Set tapGesture.cancelsTouchesInView = false to allow buttons, switches, and collection cells to process their normal tap events.

Should I call resignFirstResponder() on each field or use endEditing(true)?

Using view.endEditing(true) is preferred. It traverses the entire view hierarchy and commands whichever subview is currently the first responder to resign, saving you from tracking multiple text field instances manually.

Why does touchesBegan fail inside a UIScrollView?

UIScrollView intercepts and delays touches to determine whether the user’s gesture is a tap or a scroll. Because the scroll view absorbs these events, touchesBegan on the parent view controller is never called; use keyboardDismissMode = .onDrag or attach a configured gesture recognizer instead.

Sources & credits

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