How to hide keyboard when using SwiftUI?
You can force the first responder to resign by sending an action to the shared application:
extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }}
Now you can use this method to close the keyboard whenever you desire:
struct ContentView : View { @State private var name: String = "" var body: some View { VStack { Text("Hello \(name)") TextField("Name...", text: self.$name) { // Called when the user tap the return button // see `onCommit` on TextField initializer. UIApplication.shared.endEditing() } } }}
If you want to close the keyboard with a tap out, you can create a full screen white view with a tap action, that will trigger the endEditing(_:)
:
struct Background<Content: View>: View { private var content: Content init(@ViewBuilder content: @escaping () -> Content) { self.content = content() } var body: some View { Color.white .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) .overlay(content) }}struct ContentView : View { @State private var name: String = "" var body: some View { Background { VStack { Text("Hello \(self.name)") TextField("Name...", text: self.$name) { self.endEditing() } } }.onTapGesture { self.endEditing() } } private func endEditing() { UIApplication.shared.endEditing() }}
After a lot of attempts I found a solution that (currently) doesn't block any controls - adding gesture recognizer to UIWindow
.
- If you want to close keyboard only on Tap outside (without handling drags) - then it's enough to use just
UITapGestureRecognizer
and just copy step 3: Create custom gesture recognizer class that works with any touches:
class AnyGestureRecognizer: UIGestureRecognizer { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { if let touchedView = touches.first?.view, touchedView is UIControl { state = .cancelled } else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable { state = .cancelled } else { state = .began } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { state = .ended } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) { state = .cancelled }}
In
SceneDelegate.swift
in thefunc scene
, add next code:let tapGesture = AnyGestureRecognizer(target: window, action:#selector(UIView.endEditing))tapGesture.requiresExclusiveTouchType = falsetapGesture.cancelsTouchesInView = falsetapGesture.delegate = self //I don't use window as delegate to minimize possible side effectswindow?.addGestureRecognizer(tapGesture)
Implement
UIGestureRecognizerDelegate
to allow simultaneous touches.extension SceneDelegate: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true }}
Now any keyboard on any view will be closed on touch or drag outside.
P.S. If you want to close only specific TextFields - then add and remove gesture recognizer to the window whenever called callback of TextField onEditingChanged
SwiftUI 3 (iOS 15+)
(Done button above the keyboard)
Starting with iOS 15 we can now use @FocusState
to control which field should be focused (see this answer to see more examples).
We can also add ToolbarItem
s directly above the keyboard.
When combined together, we can add a Done
button right above the keyboard. Here is a simple demo:
struct ContentView: View { private enum Field: Int, CaseIterable { case username, password } @State private var username: String = "" @State private var password: String = "" @FocusState private var focusedField: Field? var body: some View { NavigationView { Form { TextField("Username", text: $username) .focused($focusedField, equals: .username) SecureField("Password", text: $password) .focused($focusedField, equals: .password) } .toolbar { ToolbarItem(placement: .keyboard) { Button("Done") { focusedField = nil } } } } }}
SwiftUI 2 (iOS 14+)
(Tap anywhere to hide the keyboard)
Here is an updated solution for SwiftUI 2 / iOS 14 (originally proposed here by Mikhail).
It doesn't use the AppDelegate
nor the SceneDelegate
which are missing if you use the SwiftUI lifecycle:
@mainstruct TestApp: App { var body: some Scene { WindowGroup { ContentView() .onAppear(perform: UIApplication.shared.addTapGestureRecognizer) } }}extension UIApplication { func addTapGestureRecognizer() { guard let window = windows.first else { return } let tapGesture = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing)) tapGesture.requiresExclusiveTouchType = false tapGesture.cancelsTouchesInView = false tapGesture.delegate = self window.addGestureRecognizer(tapGesture) }}extension UIApplication: UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true // set to `false` if you don't want to detect tap during other gestures }}
If you want to detect other gestures (not only tap gestures) you can use AnyGestureRecognizer
as in Mikhail's answer:
let tapGesture = AnyGestureRecognizer(target: window, action: #selector(UIView.endEditing))
Here is an example how to detect simultaneous gestures except Long Press gestures:
extension UIApplication: UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) }}