Hitesh Surani
Hitesh Surani

Reputation: 13537

How to hide keyboard when using SwiftUI?

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

Answers (30)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119108

Dismiss from anywhere

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)
}

Keyboard's Return Key

You 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

Benefits

  • You can call it from anywhere
  • It's not dependent on UIKit or SwiftUI (can be used in mac apps)
  • It works even in iOS 13

Demo

demo

Upvotes: 7

Hasaan Ali
Hasaan Ali

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

cohen72
cohen72

Reputation: 2978

Using .onSubmit and @FocusState on + iOS 15

Using .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

Prasad De Zoysa
Prasad De Zoysa

Reputation: 2567

iOS 16

You can simply do this,

Form {

}
.scrollDismissesKeyboard(.interactively)

Upvotes: 4

Anubhav Singh
Anubhav Singh

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

Shubh Sharma
Shubh Sharma

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

Mike Birkhoff
Mike Birkhoff

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

Joshua Wolff
Joshua Wolff

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

zero3nna
zero3nna

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

pawello2222
pawello2222

Reputation: 54426

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 ToolbarItems directly above the keyboard.

When combined together, we can add a Done button right above the keyboard. Here is a simple demo:

enter image description here

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
                    }
                }
            }
        }
    }
}

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:

@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

user14341201
user14341201

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

Ptah
Ptah

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

Muhammad Waqas Bhati
Muhammad Waqas Bhati

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

Joe Scotto
Joe Scotto

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

Sergio Castro
Sergio Castro

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

josefdolezal
josefdolezal

Reputation: 493

Pure SwiftUI (iOS 15)

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
        }
    }
}

Pure SwiftUI (iOS 14 and below)

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

Dan Fu
Dan Fu

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

Ryan
Ryan

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.

  1. Monitor UIWindow.keyboardWillShowNotification / willHide

  2. Pass the current keyboard state via an EnvironmentKey set at the/a root view

Tested for iOS 14.5.

Attach dismiss gesture to the form

Form { }
    .dismissKeyboardOnTap()

Setup monitor in root view

// 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()
    }

SwiftUI Gesture + Sugar


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()
    }
}

EnvironmentKey

extension EnvironmentValues {
    var keyboardIsShown: Bool {
        get { return self[KeyboardIsShownEVK] }
        set { self[KeyboardIsShownEVK] = newValue }
    }
}

private struct KeyboardIsShownEVK: EnvironmentKey {
    static let defaultValue: Bool = false
}

Upvotes: 2

Ethan Halprin
Ethan Halprin

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)
  }
}

sample

Upvotes: 3

Sergio Bost
Sergio Bost

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

zdravko zdravkin
zdravko zdravkin

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

Marcel Mendes Filho
Marcel Mendes Filho

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

Mikhail
Mikhail

Reputation: 4311

After a lot of attempts I found a solution that (currently) doesn't block any controls - adding gesture recognizer to UIWindow.

  1. 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:
  2. 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
        }
    }
    
  3. 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)  
    
  4. 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

Nicolas Mandica
Nicolas Mandica

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

Usage to get the behavior asked by @IMHiteshSurani

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)
    }
}

Code

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

George Valkov
George Valkov

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

Victor Kushnerov
Victor Kushnerov

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

DubluDe
DubluDe

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

Michael Henry
Michael Henry

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

Joseph Astrahan
Joseph Astrahan

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

Feldur
Feldur

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

Related Questions