183

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.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
Hitesh Surani
  • 12,733
  • 6
  • 54
  • 65
  • 37
    @DannyBuonocore Read my question carefully again! – Hitesh Surani Jun 07 '19 at 09:44
  • 12
    @DannyBuonocore This is not a duplicate of mentioned question. This question is about SwiftUI, and other is normal UIKit – Johnykutty Jun 07 '19 at 12:15
  • 1
    @DannyBuonocore please have looked to https://developer.apple.com/documentation/swiftui to find the difference between UIKit and SwiftUI. Thanks – Hitesh Surani Jun 07 '19 at 12:31
  • I added my solution [here](https://stackoverflow.com/a/59872410/4067700) I hope it helps you. – Victor Kushnerov Jan 28 '20 at 12:17
  • Most solutions here do not work as desired, as they disable desired reactions on other control taps. A working solution can be found here: https://forums.developer.apple.com/thread/127196 – Hardy Apr 12 '20 at 06:57
  • Any update on this now that iOS 14 and Xcode 12 is out? – Ryan Jun 23 '20 at 00:07
  • @Ryan Unfortunately the keyboard hiding issue still persists in SwiftUI 2 - [here](https://stackoverflow.com/a/63942065/8697793) you can find a working solution for SwiftUI 2. – pawello2222 Sep 17 '20 at 16:14

35 Answers35

151

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()
    }
}
rraphael
  • 10,041
  • 2
  • 25
  • 33
  • 2
    `.keyWindow` is now deprecated. See [Lorenzo Santini's answer](https://stackoverflow.com/a/57639614/3397217). – LinusGeffarth Sep 08 '19 at 21:03
  • 4
    Also, `.tapAction` has been renamed to `.onTapGesture` – LinusGeffarth Sep 08 '19 at 21:08
  • Can the keyboard be dismissed when an alternate control becomes active? https://stackoverflow.com/questions/58643512/swiftui-dismiss-keyboard-when-picker-selected – Yarm Oct 31 '19 at 12:40
  • 1
    Is there a way to do this without the whitebackground, I'm using spacers and I need it to detect a tap gesture on the spacer. Also the white background strategy creates a problem on the newer iPhones where there is extra screen space above now. Any help appreciated! – Joseph Astrahan Dec 19 '19 at 01:16
  • I posted an answer which improves upon your design. Feel free to make edits to your answer if you want I do not care for credit. – Joseph Astrahan Dec 19 '19 at 01:26
  • UIApplication has no endEditing agagin, use 'UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)' – MichaelMao Aug 31 '20 at 09:05
  • 2
    Maybe it's also worth noticing that `UIApplication` is part of UIKit, so one needs to `import UIKit`. – Alienbash Nov 22 '20 at 12:55
  • Use Color(UIColor.systemBackground) instead of Color.white – Lukasz D Nov 08 '21 at 10:17
136

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)
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 8
    This should be on top because keeps in mind the new SwiftUI lifecycle. – carlosobedgomez Oct 07 '20 at 00:51
  • 1
    This works great. However if I double tap in a text field, instead of selecting the text the keyboard now disappears. Any idea how I can allow double tap for selection? – Gary Oct 31 '20 at 22:46
  • 1
    @Gary In the bottom extension you can see the line with the comment *set to false if you don't want to detect tap during other gestures*. Just set it to `return false`. – pawello2222 Nov 01 '20 at 00:04
  • Setting it to false works, but then the keyboard also doesn’t dismiss if someone long presses or drags or scrolls outside the text area. Is there some way to set it to false only for double clicks (preferably double clicks inside the text field, but even just all double clicks would do). – Gary Nov 01 '20 at 02:06
  • 4
    To answer my own question, I set it back to true, and then I set tapGesture= AnyGestureRecognizer(... ) that Mikhail created in his answer rather than tapGesture=UITapGestureRecognizer(...). This allows double taps to select text within the text field while also allowing various gestures to hide the keyboard outside the text field. – Gary Nov 01 '20 at 09:08
  • Best solution for swiftui 2.0, however, there is a crash when double clicking on text fields – ElKePoN Dec 26 '20 at 17:01
  • @Gary I get `AnyGestureRecognizer` can you share the code? – ElKePoN Dec 26 '20 at 17:05
  • Dismissing keyboard this way should be discouraged because it acts on app's global view layout. The correct way should be to only dismiss in areas that should be tap-dismissible. Tapping on navigation bar should not dismiss keyboard. Tapping on scroll view should. It comes down to user experience but in general this global-tap-gesture approach is prone to unwanted side effects – Fero Feb 24 '21 at 12:00
  • @Ferologics But what should be tap-dismissible? IMHO tapping on navigation bar *should* dismiss keyboard. And actually as you can see from other answers, you're more likely to encounter issues when attaching gestures to single controls than when adding a global *simultaneous* gesture. But again it would all be way easier if we had a native approach. – pawello2222 Feb 24 '21 at 17:43
  • 1
    This warning comes up when using this code `'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead`. – Roland Lariotte Jun 14 '21 at 20:30
  • @RolandLariotte The iOS 14 solution *will* continue to work in iOS 15 (as confirmed [here](https://developer.apple.com/forums/thread/682621)). However, iOS 15 gives us new, more native solutions for hiding keyboard - please see the updated answer. – pawello2222 Jun 17 '21 at 20:32
  • 1
    @pawello2222 Unfortunately, the iOS 15 solution do not allow you to tap outside the keyboard to dismiss it. – Roland Lariotte Jun 17 '21 at 21:00
  • 7
    @RolandLariotte Assuming you use iOS, you can do `guard let window = (connectedScenes.first as? UIWindowScene)?.windows.first else { return }` to silence the warning. It will behave exactly the same as the original solution. – pawello2222 Jun 19 '21 at 09:35
  • 1
    By far, the best solution showing all necessary steps both for iOS 14 and iOS 15 – Redar Nov 08 '21 at 17:47
  • You'll note that this adds a bar to all text fields. See here: https://stackoverflow.com/questions/69478735/swiftui-keyboard-toolbar-scope – Jimbo Dec 28 '21 at 18:58
  • The SwiftUI 3 version doesn't work for me. Copied the exact example. Only the Done button shows, and not the actual keyboard. Xcode 13.2.1. – axlrtr May 23 '22 at 03:43
  • what if i have only one text field, should i create an enum for this text field ? sounds unreasonable. – JAHelia Jun 28 '22 at 16:44
  • 2
    @JAHelia The .focused modifer accepts bool as well, so you don’t have to use enum if you only have one text field. – pawello2222 Jun 28 '22 at 17:04
81

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

Mikhail
  • 4,271
  • 3
  • 27
  • 39
  • This is a superior approach to any method SwiftUI offers for handling keyboard resignation. Thank you – Dan Barclay Feb 15 '20 at 17:07
  • 4
    This answer should be at the top. Other answers fail when there are other controls in the view. – imthath Feb 22 '20 at 07:27
  • It works perfectly but dismisses the keyboard and shows it again when going from a TextField to another one. – Roland Lariotte Apr 06 '20 at 14:52
  • 1
    @RolandLariotte updated answer to fix this behaviour, look at new implementation of AnyGestureRecognizer – Mikhail Apr 06 '20 at 20:59
  • Perfect solution. Superior to anything I have tried, yet. All other solutions I tried had side effects, such that I could not move rows any more or normal dragging of the list was compromised. Thank you!! – user3687284 Apr 10 '20 at 16:01
  • 2
    Awesome answer. Works flawlessly. @Mikhail actually interested to know how do you remove the gesture recognizer specifically for some textfields (I built an autocomplete with tags, so everytime I tap an element in the list, I don't want this specific textfield to lose focus) – Pasta May 18 '20 at 06:26
  • @Pasta in this case I would recommend to make an empty protocol like Undismissing, and implement it for any view you want (like UITextField, or your autocompletion view). And update touchesBegan method to check wether touched view confroms to this protocol, instead of checking for only UITextField/UITextView. – Mikhail May 20 '20 at 17:38
  • this works well except it doesn't seem to allow me to increment values via a button after i use this keyboard – Mike Lyons May 22 '20 at 19:12
  • I assume, 2. is missing - class AnyGestureRecognizer: UIGestureRecognizer { - at the top. However, your implementation does not dismiss the keyboard when an UITextView via UIViewRepresentable is currently selected and somewhere outside the UITextView is being clicked. Should it? Can you please clarify where in 3. the code has to be added? – Peanutsmasher Jun 16 '20 at 18:00
  • @randomcontrol haven't you received this error in #4 'Redundant conformance of SceneDelegate to protocol UIGestureRecognizerDelegate'? If not, do you have any idea why this happens ? – Maksym Rohovskoi Jun 22 '20 at 07:17
  • @Maksym Rohovskoi yes, I got that too. That's because when you do step 3 xcode tells you that you have to add the UIGestureRecognizerDelegate protocol to your "class SceneDelegate", which you tell xcode to do. But afterwards you add the extension which again conforms to that -> "extension SceneDelegate: UIGestureRecognizerDelegate" and then you have to remove ": UIGestureRecognizerDelegate" from "class SceneDelegate" again ;-) – randomcontrol Jun 24 '20 at 22:31
  • 1
    @Mikhail Your solution is really good, but it *ends editing* not only for the keyboard input. I have issues when trying to select some text - I cannot change the selection. Every time I try to move a cursor (to expand the selection) the selection disappears. Are you able to modify your `action:#selector(UIView.endEditing)` to only hide keyboard and not interfere with text selection? – pawello2222 Jul 27 '20 at 15:35
  • Amazing all-purpose solution! Other solutions either disable taps on other views, or require you to implement it for each window. This is simple and to the point. --- ps. I moved step 3 to a method in the SceneDelegate extension: `func closeSoftKeyboardOnAnyGesture(window: UIWindow)` which is called in `func scene`, to keep the pieces of code together. – P Kuijpers Aug 04 '20 at 14:51
  • 1
    this solution is actually great, but after using it for like 3 months, unfortunately I've found a bug, directly caused by this kind of hack. please, be aware of same happening to you – glassomoss Aug 09 '20 at 00:03
  • 1
    Fantastic answer! I wonder how this will be implemented with iOS 14 without the scenedelegate? – Dom Sep 15 '20 at 14:29
  • 3
    @DominiqueMiller I adapted this solution for iOS 14 [here](https://stackoverflow.com/a/63942065/8697793). – pawello2222 Sep 18 '20 at 09:29
  • 1
    @pawello2222 Thanks! – Dom Sep 23 '20 at 19:25
  • @pawello2222, did you find a solution using this but fix the issue to select some text? The keyboard closes when trying to select text. Thanks! – user14341201 Mar 22 '22 at 14:14
36

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)})
DubluDe
  • 361
  • 3
  • 4
32

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

Feldur
  • 1,121
  • 9
  • 23
  • 9
    Hmmm - tapping on other controls no longer registers. The event is swallowed. – Yarm Oct 31 '19 at 10:42
  • I can't replicate that - it's still working for me using the latest drops from Apple as of 11/1. Did it work and then just stop working for you, or ?? – Feldur Nov 01 '19 at 19:13
  • If you have a DatePicker in the form, then the DatePicker will not be shown any more – Albert Jan 06 '20 at 21:08
  • @Albert - that's true; to use this approach, you'll have to break down where items are decorated with DismissingKeyboard() to a finer grained level that applies to the elements that should dismiss and avoids the DatePicker. – Feldur Jan 08 '20 at 22:33
  • Usage of this code will reproduce the warning ``Can't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default`` – np2314 Jan 31 '20 at 08:30
  • @np2314 Can you post your code that presents the problem? I checked with my app using this technique, and as of today (2/3) it's working. – Feldur Feb 03 '20 at 20:25
  • This code make `cancelsTouchesInView = true`. You can not tap on button that are present on the View. – Roland Lariotte Jun 14 '21 at 20:27
30

I found another way to dismiss the keyboard that doesn't require accessing the keyWindow property; as a matter of fact the compiler gives back a warning using

UIApplication.shared.keyWindow?.endEditing(true)

'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes

Instead I used this code:

UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
Dharman
  • 30,962
  • 25
  • 85
  • 135
Lorenzo Santini
  • 655
  • 7
  • 13
  • 1
    This is a perfect answer. So clean and easy. Just put this in the `.onTapGesture{}` modifier of the view at the top level of the hierarchy and this solves the problem. Thank you – NSSpeedForce Jun 20 '22 at 01:36
17

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.

josefdolezal
  • 403
  • 1
  • 6
  • 9
16

SwiftUI in 'SceneDelegate.swift' file just add: .onTapGesture { window.endEditing(true)}

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(
                rootView: contentView.onTapGesture { window.endEditing(true)}
            )
            self.window = window
            window.makeKeyAndVisible()
        }
    }

this is enough for each View using keyboard in your app...

Dim Novo
  • 367
  • 3
  • 8
  • 5
    This gives another problem - i have a picker in the Form{} alongside of textfield, it became unresponsive. I didn't find a solution to it using all the answers in this topic. But your answer is a good for dismissing keyboard with a tap elsewhere - if you do not use pickers. – Nalov Oct 11 '19 at 14:29
  • hello. my code ``` var body: some View { NavigationView{ Form{ Section{ TextField("typesomething", text: $c) } Section{ Picker("name", selection: $sel) { ForEach(0..<200){ Text("\(self.array[$0])%") } } } ``` The keyboard is dismissed when tapping elsewhere, but the picker became unresponsive. I didn't find a way to make it work. – Nalov Oct 11 '19 at 18:56
  • 2
    Hi again, at the moment I have two solutions: the first - is to use the native keyboard dismissed on the return button, the second - is to change the tapping handling slightly(aka 'костыль') - window.rootViewController = UIHostingController(rootView: contentView.onTapGesture(count: 2, perform: { window.endEditing(true) }) ) Hope this helps you... – Dim Novo Oct 12 '19 at 10:30
  • Hello. Thank you. The second way solved it. I am using numeric pad, so users can enter only numbers, it does not have return key. Dismissing with tapping was what i was searching. – Nalov Oct 12 '19 at 11:08
  • this will cause list can not be navigate. – Cui Mingda Oct 21 '19 at 06:37
  • Combining with @DimNovo 's answer this works great (may solve the pickers issue didn't try): `window.rootViewController = UIHostingController(rootView: contentView.onTapGesture { UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil) })` – Cinn Nov 28 '19 at 16:12
16

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
            }
        }
    }
}
Ptah
  • 906
  • 1
  • 12
  • 25
15

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: {})
    }
}
Victor Kushnerov
  • 3,706
  • 27
  • 56
  • This worked great, I used it slightly differently and had to be sure it was called on the main thread. – keegan3d Apr 09 '20 at 04:32
12

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.

Joe Scotto
  • 10,936
  • 14
  • 66
  • 136
  • I used the iOS 14 (swiftUI 2) example below using @main with the 2 extensions. Are you saying I have to throw out all that code to implement the same thing in iOS 15? There isn't a simple fix to close the keyboard when tapping anywhere to close the keyboard? – Galen Smith Oct 20 '21 at 21:59
  • @GalenSmith No, I'm saying that I tested the solution I posted in iOS15. But it should work in iOS14, 13, etc with some minor changes to naming. I think specifically `.onTapGesture` is different – Joe Scotto Oct 24 '21 at 15:10
11

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)
}
np2314
  • 645
  • 5
  • 14
George Valkov
  • 1,217
  • 12
  • 10
10

add this modifier to the view you want to detect user taps

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

        }
RyanTCB
  • 7,400
  • 5
  • 42
  • 62
  • The .onTapGesture works in most cases however, when I add an onTapGesture in a view where there are navigation links inside a list these will not be triggered anymore. – Mike Birkhoff Nov 18 '22 at 07:24
10

Because keyWindow is deprecated.

extension View {
    func endEditing(_ force: Bool) {
        UIApplication.shared.windows.forEach { $0.endEditing(force)}
    }
}
msk
  • 8,885
  • 6
  • 41
  • 72
9

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") })
zero3nna
  • 2,770
  • 30
  • 28
8

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!
            }
        }
    }
Hasaan Ali
  • 1,192
  • 16
  • 22
7

Keyboard's Return Key

In addition to all answers about tapping outside of the textField, you may want to dismiss the keyboard when the user taps the return key on the keyboard:

define this global function:

func resignFirstResponder() {
    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}

And add use in onCommit argument it:

TextField("title", text: $text, onCommit:  {
    resignFirstResponder()
})

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

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
6

Expanding on the answer by @Feldur (which was based on @RyanTCB's), here is an even more expressive and powerful solution allowing you to dismiss keyboard on other gestures than onTapGesture, you can specify which you want in the function call.

Usage

// MARK: - View
extension RestoreAccountInputMnemonicScreen: View {
    var body: some View {
        List(viewModel.inputWords) { inputMnemonicWord in
            InputMnemonicCell(mnemonicInput: inputMnemonicWord)
        }
        .dismissKeyboard(on: [.tap, .drag])
    }
}

Or using All.gestures (just sugar for Gestures.allCases )

.dismissKeyboard(on: All.gestures)

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 {
            view.highPriorityGesture(
                gesture.onChanged { value in
                    _ = value
                    voidAction()
                }
            ).eraseToAny()
        }

        switch self {
        case .tap:
            // not `highPriorityGesture` since tapping is a common gesture, e.g. wanna allow users
            // to easily tap on a TextField in another cell in the case of a list of TextFields / Form
            return view.gesture(TapGesture().onEnded(voidAction)).eraseToAny()
        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(content.eraseToAny()) { $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))
    }
}

Word of caution

Please do note that if you use all gestures they might conflict and I did not come up with any neat solution solving that.

Sajjon
  • 8,938
  • 5
  • 60
  • 94
5

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

}
Michael Henry
  • 644
  • 9
  • 12
4

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()} 
     }
 }
    
Sergio Bost
  • 2,591
  • 2
  • 11
  • 29
4

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
        
    }

    ...
}
Ole Pannier
  • 3,208
  • 9
  • 22
  • 33
  • I used to disregard messages recommending IQKeyboardManager because I thought "just another library". After some much struggle with the SwiftUI keyboard I finally came to implement it. – Liviu Jan 07 '22 at 05:13
3

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.

Joseph Astrahan
  • 8,659
  • 12
  • 83
  • 154
  • My question now is how do you trigger a commit on a textfield when you get the keyboard to go away this way? currently the 'commit' only triggers if you hit the return key on the iOS keyboard. – Joseph Astrahan Dec 20 '19 at 01:15
3

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))
    }
}
Nicolas Mandica
  • 803
  • 1
  • 10
  • 16
3

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()
          }
      }
  }
Anubhav Singh
  • 905
  • 6
  • 5
2

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()
zdravko zdravkin
  • 2,090
  • 19
  • 21
2

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

Ethan Halprin
  • 470
  • 3
  • 11
2

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
}
Ryan
  • 1,252
  • 6
  • 15
2

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 {
        fieldInFocus = nil
      }
    }
  }
}

Or if you want to use .onSubmit to bring focus to a different TextField:

  .onSubmit {
      } if fieldInFocus == .email {
        fieldInFocus = .password
      } else if fieldInFocus == .password {
        fieldInFocus = nil
      }
  }
cohen72
  • 2,830
  • 29
  • 44
1
extension UIView{

 override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {  
     UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

  }
}
Shailesh
  • 3,072
  • 3
  • 24
  • 33
  • replace Set to Set – Sergei Volkov Oct 05 '22 at 06:35
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 05 '22 at 10:21
  • Brilliant! This should be the default. Not sure why apple leaves it open. I'll leave it to someone with a higher pay grade while I code away at my new app! – user3413723 Aug 10 '23 at 14:09
0

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

Dan Fu
  • 1
0

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
    }
    
}
Muhammad Waqas Bhati
  • 2,775
  • 20
  • 25
0

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
    }
}
user14341201
  • 212
  • 1
  • 9
0

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)
Joshua Wolff
  • 2,687
  • 1
  • 25
  • 42
0

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()))
Mike Birkhoff
  • 505
  • 5
  • 10
-2

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.