Why this happens
UIKit gives first responder status to exactly one view at a time. When a UITextField or UITextView becomes first responder, the system raises the keyboard and keeps it up until that view resigns. Nothing in UIKit dismisses the keyboard automatically when the user taps an empty area of the screen — the tap lands on the view controller’s root view, which has no idea a text field is active.
So the keyboard stays. On a short form it covers the fields below, and the user has to hunt for a Return key or a Done button that may not even exist for a multi-line UITextView.
The fix is to make the root view notice the tap and then tell the whole view hierarchy to stop editing. UIView.endEditing(_:) does that in one call: it asks the current first responder — or any embedded text field — to resign, and the keyboard slides away.
How to fix it
1. Add a tap gesture that calls endEditing
This is the shortest working solution. Create a UITapGestureRecognizer in viewDidLoad, point it at a method that ends editing, and attach it to the controller’s view.
import UIKit
class ItemViewController: UIViewController {
@IBOutlet weak var nameField: UITextField!
@IBOutlet weak var notesField: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self,
action: #selector(dismissKeyboard))
view.addGestureRecognizer(tap)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
}
Two details matter here:
- The action method must be marked
@objc, because#selectorneeds an Objective-C entry point. endEditing(true)forces the resignation even if the field is mid-edit. Passingfalseasks the delegate first and can be refused.
2. Stop the gesture from swallowing other taps
By default a gesture recognizer sets cancelsTouchesInView = true. On a screen with buttons, table view cells or collection view items, the tap gesture can win the race and the underlying control never sees the touch. That shows up as “my buttons stopped working after I added the keyboard dismissal”.
Set the property to false before adding the recognizer:
let tap = UITapGestureRecognizer(target: self,
action: #selector(dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
Now the tap both dismisses the keyboard and reaches the control underneath, so a button press still fires its action.
3. Make it reusable with a UIViewController extension
Copying the same six lines into every screen gets old fast. Put the behaviour in an extension once and call it from each controller that needs it.
import UIKit
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap = UITapGestureRecognizer(target: self,
action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
}
Usage becomes a single line:
override func viewDidLoad() {
super.viewDidLoad()
hideKeyboardWhenTappedAround()
}
Because the selector targets UIViewController.dismissKeyboard rather than self, subclasses that override the method don’t break the recognizer’s target-action pairing.
If you only want the keyboard gone when the user taps outside a text field, keep the gesture but check the touch location, or simply rely on cancelsTouchesInView = false and let the text field handle its own taps normally.
4. Use touchesBegan instead of a gesture recognizer
If you would rather not add a recognizer at all, override the touch handler on the view controller. This fires for taps that reach the controller’s view and nothing else.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
super.touchesBegan(touches, with: event)
}
This is fine for simple screens, but it does not fire when the tap is consumed by a subview such as a UITableView or a UIScrollView, so the gesture approach is more reliable on real screens.
5. Also handle the Return key
Tapping the background is a convenience, not a replacement for the keyboard’s own dismissal path. Conform to UITextFieldDelegate and resign on Return as well:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
Remember to set delegate = self on each field, otherwise the method is never called.
6. The SwiftUI equivalent
SwiftUI has no endEditing. Track focus with @FocusState and clear it on a background tap.
import SwiftUI
struct NoteEditor: View {
@State private var text = ""
@FocusState private var isFocused: Bool
var body: some View {
ZStack {
Color.clear
.contentShape(Rectangle())
.onTapGesture { isFocused = false }
TextField("Note", text: $text)
.focused($isFocused)
.textFieldStyle(.roundedBorder)
.padding()
}
}
}
The contentShape(Rectangle()) is what makes the transparent area tappable; without it, the empty space has no hit-testable shape. For forms inside a ScrollView, .scrollDismissesKeyboard(.interactively) gives the drag-to-dismiss behaviour users expect on iOS 16 and later.
How to prevent it
- Set
cancelsTouchesInView = falseon any dismissal gesture you add to a screen with buttons, cells or links. - Add the gesture once, in
viewDidLoad, not inviewWillAppear— repeated calls stack recognizers and fire the action multiple times. - Prefer
view.endEditing(true)over callingresignFirstResponder()on each field individually; it covers text views and any field you add later. - Keep the Return-key delegate path as well, so hardware keyboards and external input still work.
- In SwiftUI, always pair
.onTapGestureon a background with.contentShape(Rectangle()), or the tap will only register on drawn pixels.
FAQ
Does view.endEditing(true) dismiss the keyboard for a UITextView too?
Yes. endEditing(_:) walks the view hierarchy and resigns whichever descendant is currently first responder, so it works for UITextField, UITextView and any custom view that adopts UIResponder.
Why did my table view stop responding after adding the tap gesture?
Because the recognizer’s cancelsTouchesInView defaults to true, so it cancels the touch before the cell sees it. Set it to false when you create the recognizer and cell selection works again.
Can I dismiss the keyboard only when tapping outside a text field?
Yes. Implement gestureRecognizer(_:shouldReceive:), return false when touch.view is a UITextField or UITextView, and true otherwise. That keeps taps on the field itself from triggering the dismissal.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
