Reputation: 13537
How to hide keyboard
using SwiftUI
for below cases?
Case 1
I have TextField
and I need to hide the keyboard
when the user clicks the return
button.
Case 2
I have TextField
and I need to hide the keyboard
when the user taps outside.
How I can do this using SwiftUI
?
Note:
I have not asked a question regarding UITextField
. I want to do it by using SwifUI.TextField
.
Upvotes: 204
Views: 141087
Reputation: 119108
In addition to all answers about tapping outside of the textField, you may want to dismiss the keyboard from anywhere outside of the Field:
define this global function:
func resignFirstResponder() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
Return
KeyYou can call this method in the submit action which will be called when the user tap the return key on the keyboard:
TextField("title", text: $text, onCommit: {
resignFirstResponder()
})
You can use the .onSubmit
modifier instead of onCommit
closure for newer iOS versions
Upvotes: 7
Reputation: 1251
Expanding the answer by josefdolezal above, you can hide keyboard when user taps anywhere outside the textfield like below:
struct SwiftUIView: View {
@State private var textFieldId: String = UUID().uuidString // To hidekeyboard when tapped outside textFields
@State var fieldValue = ""
var body: some View {
VStack {
TextField("placeholder", text: $fieldValue)
.id(textFieldId)
.onTapGesture {} // So that outer tap gesture has no effect on field
// any more views
}
.onTapGesture { // whenever tapped within VStack
textFieldId = UUID().uuidString
//^ this will remake the textfields hence loosing keyboard focus!
}
}
}
Explanation: It creates new text field when we tap outside, it creates new id, causing create new text field with same state. The old text field is gone so the keyboard hides.
Warning: Don't use it with List or any other similar view which recreates views depending upon view ids.
Upvotes: 9
Reputation: 2978
.onSubmit
and @FocusState
on + iOS 15Using .onSubmit
and @FocusState
you can dismiss the keyboard on any press of return, or you can decide another TextField
to then receive focus:
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)
}
.onSubmit {
focusedField = nil
}
}
}
}
Or if you want to use .onSubmit
to bring focus to a different TextField
:
.onSubmit {
if focusedField == .email {
focusedField = .password
} else if focusedField == .password {
focusedField = nil
}
}
Upvotes: 6
Reputation: 2567
You can simply do this,
Form {
}
.scrollDismissesKeyboard(.interactively)
Upvotes: 4
Reputation: 1171
First add the extension on your code
extension UIApplication {
func dismissKeyboard() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
} }
Use it as a modifer to close the keyboard on pressing
Done
button.
.toolbar{
ToolbarItemGroup(placement: .keyboard){
Spacer()
Button("Done"){
UIApplication.shared.dismissKeyboard()
}
}
}
Upvotes: 7
Reputation: 27
extension UIView{
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Upvotes: 1
Reputation: 515
I'd like to point out that .onTapGesture might consume events meant for navigation links. You can alternatively use a drag gesture, this should not clash with the most common elements.
.gesture(
DragGesture().onEnded { value in
self.dismissKeyboard()
})
However, this can prevent swipeActions. I avoided this by adding the event at the background view inside my CoporateIdentity View:
struct CIView: View {
var displayView: AnyView
var body: some View {
ZStack{
Color("Background").edgesIgnoringSafeArea(.all)
.gesture(
DragGesture().onEnded { value in
self.dismissKeyboard()
})
displayView
}
.foregroundColor(Color("Foreground"))
}
private func dismissKeyboard() {
UIApplication.shared.dismissKeyboard()
}
}
This view can be used like this:
CIView(displayView: AnyView(YourView()))
Upvotes: 0
Reputation: 3342
iOS 13+
One simple hack for iOS13+ is to set a state variable for "disabled" for every single textfield. Obviously not ideal, but might get the job done in some cases.
Once you set disabled = True
, then all linked responders automatically resign.
@State var isEditing: Bool
@State var text: String
....
TextField("Text", text: self.$text).disabled(!self.isEditing)
Upvotes: 1
Reputation: 2918
Updated the answer, working with Swift 5.7:
extension UIApplication {
func dismissKeyboard() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
and then using it where needed like for example as button action:
Button(action: {
// do stuff
UIApplication.shared.dismissKeyboard()
}, label: { Text("MyButton") })
Upvotes: 11
Reputation: 54426
(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
}
}
}
}
}
}
(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:
@main
struct 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)
}
}
Upvotes: 157
Reputation: 222
Answer from @Mikhail worked very well; it just has the issue that it cannot support dragging to select text within TextView - keyboard will close on tapping on the selected text. I extended his solution for AnyGesture below to provide better text editing user experience. (Answer from How to check for a UITextRangeView?)
Any recommendations to optimise the while loop?
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 {
// Check if it is a subview of editable UITextView
if var touchedView = touches.first?.view {
while let superview = touchedView.superview {
if let view = superview as? UITextView, view.isEditable {
state = .cancelled
return
} else {
touchedView = superview
}
}
}
state = .began
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
}
Upvotes: 0
Reputation: 976
Since iOS 15, you can use @FocusState
struct ContentView: View {
@Binding var text: String
private enum Field: Int {
case yourTextEdit
}
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextEditor(text: $speech.text.bound)
.padding(Edge.Set.horizontal, 18)
.focused($focusedField, equals: .yourTextEdit)
}.onTapGesture {
if (focusedField != nil) {
focusedField = nil
}
}
}
}
Upvotes: 20
Reputation: 2805
I am trying to hide keyboard while single tap & Picker should also work with single tap in SwiftUIForms.
I searched a lot to find a proper solution but didn't get any which works for me. So I make my own extension which works very well.
Use in your SwiftUI Form View:
var body: some View {
.onAppear { KeyboardManager.shared.setCurrentView(UIApplication.topViewController()?.view)
}
}
KeyboardManager Utility:
enum KeyboardNotificationType {
case show
case hide
}
typealias KeyBoardSizeBlock = ((CGSize?, UIView?, KeyboardNotificationType) -> Void)
class KeyboardManager: NSObject {
static let shared = KeyboardManager()
private weak var view: UIView?
var didReceiveKeyboardEvent: KeyBoardSizeBlock?
@objc public var shouldResignOnTouchOutside = true {
didSet {
resignFirstResponderGesture.isEnabled = shouldResignOnTouchOutside
}
}
@objc lazy public var resignFirstResponderGesture: UITapGestureRecognizer = {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissCurrentKeyboard))
tap.cancelsTouchesInView = false
tap.delegate = self
return tap
}()
private override init() {
super.init()
self.setup()
}
func setCurrentView(_ view: UIView?) {
self.view = view
resignFirstResponderGesture.isEnabled = true
if let view = self.view {
view.addGestureRecognizer(resignFirstResponderGesture)
}
}
private func setup() {
registerForKeyboardWillShowNotification()
registerForKeyboardWillHideNotification()
}
private func topViewHasCurrenView() -> Bool {
if view == nil { return false }
let currentView = UIApplication.topViewController()?.view
if currentView == view { return true }
for subview in UIApplication.topViewController()?.view.subviews ?? [] where subview == view {
return true
}
return false
}
@objc func dismissCurrentKeyboard() {
view?.endEditing(true)
}
func removeKeyboardObserver(_ observer: Any) {
NotificationCenter.default.removeObserver(observer)
}
private func findFirstResponderInViewHierarchy(_ view: UIView) -> UIView? {
for subView in view.subviews {
if subView.isFirstResponder {
return subView
} else {
let result = findFirstResponderInViewHierarchy(subView)
if result != nil {
return result
}
}
}
return nil
}
deinit {
removeKeyboardObserver(self)
}
}
// MARK: - Keyboard Notifications
extension KeyboardManager {
private func registerForKeyboardWillShowNotification() {
_ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
guard let `self` = self else { return }
guard let userInfo = notification.userInfo else { return }
guard var kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
kbRect.size.height -= self.view?.safeAreaInsets.bottom ?? 0.0
var mainResponder: UIView?
guard self.topViewHasCurrenView() else { return }
if let scrollView = self.view as? UIScrollView {
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: kbRect.size.height, right: 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
guard let firstResponder = self.findFirstResponderInViewHierarchy(scrollView) else {
return
}
mainResponder = firstResponder
var aRect = scrollView.frame
aRect.size.height -= kbRect.size.height
if (!aRect.contains(firstResponder.frame.origin) ) {
scrollView.scrollRectToVisible(firstResponder.frame, animated: true)
}
} else if let tableView = self.view as? UITableView {
guard let firstResponder = self.findFirstResponderInViewHierarchy(tableView),
let pointInTable = firstResponder.superview?.convert(firstResponder.frame.origin, to: tableView) else {
return
}
mainResponder = firstResponder
var contentOffset = tableView.contentOffset
contentOffset.y = (pointInTable.y - (firstResponder.inputAccessoryView?.frame.size.height ?? 0)) - 10
tableView.setContentOffset(contentOffset, animated: true)
} else if let view = self.view {
guard let firstResponder = self.findFirstResponderInViewHierarchy(view) else {
return
}
mainResponder = firstResponder
var aRect = view.frame
aRect.size.height -= kbRect.size.height
if (!aRect.contains(firstResponder.frame.origin) ) {
UIView.animate(withDuration: 0.1) {
view.transform = CGAffineTransform(translationX: 0, y: -kbRect.size.height)
}
}
}
if let block = self.didReceiveKeyboardEvent {
block(kbRect.size, mainResponder, .show)
}
})
}
private func registerForKeyboardWillHideNotification() {
_ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
guard let `self` = self else { return }
guard let userInfo = notification.userInfo else { return }
guard let kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
let contentInsets = UIEdgeInsets.zero
guard self.topViewHasCurrenView() else { return }
if let scrollView = self.view as? UIScrollView {
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
} else if let tableView = self.view as? UITableView {
tableView.contentInset = contentInsets
tableView.scrollIndicatorInsets = contentInsets
tableView.contentOffset = CGPoint(x: 0, y: 0)
} else if let view = self.view {
view.transform = CGAffineTransform(translationX: 0, y: 0)
}
if let block = self.didReceiveKeyboardEvent {
block(kbRect.size, nil, .hide)
}
})
}
}
//MARK: - UIGestureRecognizerDelegate
extension KeyboardManager: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view is UIControl ||
touch.view is UINavigationBar { return false }
return true
}
}
Upvotes: 0
Reputation: 10857
In iOS15 this is working flawlessly.
VStack {
// Some content
}
.onTapGesture {
// Hide Keyboard
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded({ gesture in
// Hide keyboard on swipe down
if gesture.translation.height > 0 {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}))
Nothing else is required on your TextField and both swipe down along with tap will work to hide it. The way I use this is that on my master NavigationView
I add this code and then everything below it will work. The only exception would be that any Sheet
would need to have this appended to it as that is acting on a different state.
Upvotes: 16
Reputation: 41
Well, the easiest solution for me is to simply use the library here.
SwiftUI support is somewhat limited, I use it by placing this code in the @main struct:
import IQKeyboardManagerSwift
@main
struct MyApp: App {
init(){
IQKeyboardManager.shared.enable = true
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
}
...
}
Upvotes: 4
Reputation: 493
SwiftUI in iOS 15 (Xcode 13) gained native support for programmatic focus of TextField
using new @FocusState
property wrapper.
To dismiss the keyboard, simply set view's focusedField
to nil
. The return key will dismiss keyboard automatically (since iOS 14).
Docs: https://developer.apple.com/documentation/swiftui/focusstate/
struct MyView: View {
enum Field: Hashable {
case myField
}
@State private var text: String = ""
@FocusState private var focusedField: Field?
var body: some View {
TextField("Type here", text: $text)
.focused($focusedField, equals: .myField)
Button("Dismiss") {
focusedField = nil
}
}
}
You can completely avoid interaction with UIKit and implement it in pure SwiftUI. Just add an .id(<your id>)
modifier to your TextField
and change its value whenever you want to dismiss keyboard (on swipe, view tap, button action, ..).
Sample implementation:
struct MyView: View {
@State private var text: String = ""
@State private var textFieldId: String = UUID().uuidString
var body: some View {
VStack {
TextField("Type here", text: $text)
.id(textFieldId)
Spacer()
Button("Dismiss", action: { textFieldId = UUID().uuidString })
}
}
}
Note that I only tested it in latest Xcode 12 beta, but it should work with older versions (even Xcode 11) without any issue.
Upvotes: 24
Reputation: 1
True SwiftUI Solution
@State var dismissKeyboardToggle = false
var body: some View {
if dismissKeyboardToggle {
textfield
} else {
textfield
}
Button("Hide Keyboard") {
dismissKeyboardToggle.toggle()
}
}
this will work flawlessly
Upvotes: 0
Reputation: 1392
A cleaner SwiftUI-native way to dismiss the keyboard via tap without blocking any complicated forms or whatnot... credit to @user3441734 for flagging GestureMask as a clean approach.
Monitor UIWindow.keyboardWillShowNotification / willHide
Pass the current keyboard state via an EnvironmentKey set at the/a root view
Tested for iOS 14.5.
Form { }
.dismissKeyboardOnTap()
// Root view
.environment(\.keyboardIsShown, keyboardIsShown)
.onDisappear { dismantleKeyboarMonitors() }
.onAppear { setupKeyboardMonitors() }
// Monitors
@State private var keyboardIsShown = false
@State private var keyboardHideMonitor: AnyCancellable? = nil
@State private var keyboardShownMonitor: AnyCancellable? = nil
func setupKeyboardMonitors() {
keyboardShownMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillShowNotification)
.sink { _ in if !keyboardIsShown { keyboardIsShown = true } }
keyboardHideMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillHideNotification)
.sink { _ in if keyboardIsShown { keyboardIsShown = false } }
}
func dismantleKeyboarMonitors() {
keyboardHideMonitor?.cancel()
keyboardShownMonitor?.cancel()
}
struct HideKeyboardGestureModifier: ViewModifier {
@Environment(\.keyboardIsShown) var keyboardIsShown
func body(content: Content) -> some View {
content
.gesture(TapGesture().onEnded {
UIApplication.shared.resignCurrentResponder()
}, including: keyboardIsShown ? .all : .none)
}
}
extension UIApplication {
func resignCurrentResponder() {
sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
extension View {
/// Assigns a tap gesture that dismisses the first responder only when the keyboard is visible to the KeyboardIsShown EnvironmentKey
func dismissKeyboardOnTap() -> some View {
modifier(HideKeyboardGestureModifier())
}
/// Shortcut to close in a function call
func resignCurrentResponder() {
UIApplication.shared.resignCurrentResponder()
}
}
extension EnvironmentValues {
var keyboardIsShown: Bool {
get { return self[KeyboardIsShownEVK] }
set { self[KeyboardIsShownEVK] = newValue }
}
}
private struct KeyboardIsShownEVK: EnvironmentKey {
static let defaultValue: Bool = false
}
Upvotes: 2
Reputation: 550
Simple solution for clicking "outside" that worked for me:
First provide a ZStack before all views. In it, put a background (with the color of your choosing) and supply a tap Gesture. In the gesture call, invoke the 'sendAction' we've seen above:
import SwiftUI
struct MyView: View {
private var myBackgroundColor = Color.red
@State var text = "text..."
var body: some View {
ZStack {
self.myBackgroundColor.edgesIgnoringSafeArea(.all)
.onTapGesture(count: 1) {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
TextField("", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Upvotes: 3
Reputation: 3209
Something I found that works very nice is
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Then add to the view struct:
private func endEditing() {
UIApplication.shared.endEditing()
}
Then
struct YourView: View {
var body: some View {
ParentView {
//...
}.contentShape(Rectangle()) //<---- This is key!
.onTapGesture {endEditing()}
}
}
Upvotes: 6
Reputation: 2378
So far above options did not work for me, because I have Form and inside buttons, links, picker ...
I create below code that is working, with help from above examples.
import Combine
import SwiftUI
private class KeyboardListener: ObservableObject {
@Published var keyabordIsShowing: Bool = false
var cancellable = Set<AnyCancellable>()
init() {
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.sink { [weak self ] _ in
self?.keyabordIsShowing = true
}
.store(in: &cancellable)
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.sink { [weak self ] _ in
self?.keyabordIsShowing = false
}
.store(in: &cancellable)
}
}
private struct DismissingKeyboard: ViewModifier {
@ObservedObject var keyboardListener = KeyboardListener()
fileprivate func body(content: Content) -> some View {
ZStack {
content
Rectangle()
.background(Color.clear)
.opacity(keyboardListener.keyabordIsShowing ? 0.01 : 0)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({ $0.activationState == .foregroundActive })
.map({ $0 as? UIWindowScene })
.compactMap({ $0 })
.first?.windows
.filter({ $0.isKeyWindow }).first
keyWindow?.endEditing(true)
}
}
}
}
extension View {
func dismissingKeyboard() -> some View {
ModifiedContent(content: self, modifier: DismissingKeyboard())
}
}
Usage:
var body: some View {
NavigationView {
Form {
picker
button
textfield
text
}
.dismissingKeyboard()
Upvotes: 2
Reputation: 47
SwiftUI released on June/2020 with Xcode 12 and iOS 14 adds hideKeyboardOnTap() modifier. This should solve your case number 2. The solution for your case number 1 comes for free with Xcode 12 and iOS 14: the default keyboard for TextField hides automatically when the Return button is pressed.
Upvotes: -2
Reputation: 4311
After a lot of attempts I found a solution that (currently) doesn't block any controls - adding gesture recognizer to UIWindow
.
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 the func scene
, add next code:
let tapGesture = AnyGestureRecognizer(target: window, action:#selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self //I don't use window as delegate to minimize possible side effects
window?.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
Upvotes: 85
Reputation: 861
Based on @Sajjon's answer, here is a solution allowing you to dismiss keyboard on tap, long press, drag, magnification and rotation gestures according to your choice.
This solution is working in XCode 11.4
struct MyView: View {
@State var myText = ""
var body: some View {
VStack {
DismissingKeyboardSpacer()
HStack {
TextField("My Text", text: $myText)
Button("Return", action: {})
.dismissKeyboard(on: [.longPress])
}
DismissingKeyboardSpacer()
}
}
}
struct DismissingKeyboardSpacer: View {
var body: some View {
ZStack {
Color.black.opacity(0.0001)
Spacer()
}
.dismissKeyboard(on: Gestures.allCases)
}
}
enum All {
static let gestures = all(of: Gestures.self)
private static func all<CI>(of _: CI.Type) -> CI.AllCases where CI: CaseIterable {
return CI.allCases
}
}
enum Gestures: Hashable, CaseIterable {
case tap, longPress, drag, magnification, rotation
}
protocol ValueGesture: Gesture where Value: Equatable {
func onChanged(_ action: @escaping (Value) -> Void) -> _ChangedGesture<Self>
}
extension LongPressGesture: ValueGesture {}
extension DragGesture: ValueGesture {}
extension MagnificationGesture: ValueGesture {}
extension RotationGesture: ValueGesture {}
extension Gestures {
@discardableResult
func apply<V>(to view: V, perform voidAction: @escaping () -> Void) -> AnyView where V: View {
func highPrio<G>(gesture: G) -> AnyView where G: ValueGesture {
AnyView(view.highPriorityGesture(
gesture.onChanged { _ in
voidAction()
}
))
}
switch self {
case .tap:
return AnyView(view.gesture(TapGesture().onEnded(voidAction)))
case .longPress:
return highPrio(gesture: LongPressGesture())
case .drag:
return highPrio(gesture: DragGesture())
case .magnification:
return highPrio(gesture: MagnificationGesture())
case .rotation:
return highPrio(gesture: RotationGesture())
}
}
}
struct DismissingKeyboard: ViewModifier {
var gestures: [Gestures] = Gestures.allCases
dynamic func body(content: Content) -> some View {
let action = {
let forcing = true
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(forcing)
}
return gestures.reduce(AnyView(content)) { $1.apply(to: $0, perform: action) }
}
}
extension View {
dynamic func dismissKeyboard(on gestures: [Gestures] = Gestures.allCases) -> some View {
return ModifiedContent(content: self, modifier: DismissingKeyboard(gestures: gestures))
}
}
Upvotes: 3
Reputation: 1459
I prefer using the .onLongPressGesture(minimumDuration: 0)
, which does not cause the keyboard to blink when another TextView
is activated (side effect of .onTapGesture
). The hide keyboard code can be a reusable function.
.onTapGesture(count: 2){} // UI is unresponsive without this line. Why?
.onLongPressGesture(minimumDuration: 0, maximumDistance: 0, pressing: nil, perform: hide_keyboard)
func hide_keyboard()
{
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
Upvotes: 12
Reputation: 3964
My solution how to hide software keyboard when users tap outside.
You need to use contentShape
with onLongPressGesture
to detect the entire View container. onTapGesture
required to avoid blocking focus on TextField
. You can use onTapGesture
instead of onLongPressGesture
but NavigationBar items won't work.
extension View {
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct KeyboardAvoiderDemo: View {
@State var text = ""
var body: some View {
VStack {
TextField("Demo", text: self.$text)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture {}
.onLongPressGesture(
pressing: { isPressed in if isPressed { self.endEditing() } },
perform: {})
}
}
Upvotes: 15
Reputation: 401
I experienced this while using a TextField inside a NavigationView. This is my solution for that. It will dismiss the keyboard when you start scrolling.
NavigationView {
Form {
Section {
TextField("Receipt amount", text: $receiptAmount)
.keyboardType(.decimalPad)
}
}
}
.gesture(DragGesture().onChanged{_ in UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)})
Upvotes: 40
Reputation: 722
Please check https://github.com/michaelhenry/KeyboardAvoider
Just include KeyboardAvoider {}
on top of your main view and that's all.
KeyboardAvoider {
VStack {
TextField()
TextField()
TextField()
TextField()
}
}
Upvotes: 5
Reputation: 9072
This method allows you to hide the keyboard on spacers!
First add this function (Credit Given To: Casper Zandbergen, from SwiftUI can't tap in Spacer of HStack)
extension Spacer {
public func onTapGesture(count: Int = 1, perform action: @escaping () -> Void) -> some View {
ZStack {
Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
self
}
}
}
Next add the following 2 functions (Credit Given To: rraphael, from this question)
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
The function below would be added to your View class, just refer to the top answer here from rraphael for more details.
private func endEditing() {
UIApplication.shared.endEditing()
}
Finally, you can now simply call...
Spacer().onTapGesture {
self.endEditing()
}
This will make any spacer area close the keyboard now. No need for a big white background view anymore!
You could hypothetically apply this technique of extension
to any controls you need to support TapGestures that do not currently do so and call the onTapGesture
function in combination with self.endEditing()
to close the keyboard in any situation you desire.
Upvotes: 3
Reputation: 1169
@RyanTCB's answer is good; here are a couple of refinements that make it simpler to use and avoid a potential crash:
struct DismissingKeyboard: ViewModifier {
func body(content: Content) -> some View {
content
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(true)
}
}
}
The 'bug fix' is simply that keyWindow!.endEditing(true)
properly should be keyWindow?.endEditing(true)
(yes, you might argue it can't happen.)
More interesting is how you can use it. For example, suppose you have a form with multiple editable fields in it. Just wrap it like this:
Form {
.
.
.
}
.modifier(DismissingKeyboard())
Now, tapping on any control that itself doesn't present a keyboard will do the appropriate dismiss.
(Tested with beta 7)
Upvotes: 36