2

How can I dismiss the keyboard after the user clicks outside the TextField using SwiftUI?

I created a TextField using SwiftUI, but I couldn't find any solution for dismissing the keyboard if the user clicks outside the TextField. I took a look at all attributes of TextField and also the SwiftUI TextField documentation and I couldn't find anything related with dismissing keyboard.

This is my view's code:

struct InputView: View {
    @State var inputValue : String = ""
    var body: some View {

        VStack(spacing: 10) {
            TextField("$", text: $inputValue)
                .keyboardType(.decimalPad)
        }
    }
}
Jujuba
  • 341
  • 3
  • 8

6 Answers6

5

This can be done with a view modifier.

Code

public extension View {
    func dismissKeyboardOnTap() -> some View {
        modifier(DismissKeyboardOnTap())
    }
}

public struct DismissKeyboardOnTap: ViewModifier {
    public func body(content: Content) -> some View {
        #if os(macOS)
        return content
        #else
        return content.gesture(tapGesture)
        #endif
    }

    private var tapGesture: some Gesture {
        TapGesture().onEnded(endEditing)
    }

    private func endEditing() {
        UIApplication.shared.connectedScenes
            .filter {$0.activationState == .foregroundActive}
            .map {$0 as? UIWindowScene}
            .compactMap({$0})
            .first?.windows
            .filter {$0.isKeyWindow}
            .first?.endEditing(true)
    }
}

Usage

backgroundView()
   .dismissKeyboardOnTap()

Check out the demo here: https://github.com/youjinp/SwiftUIKit

youjin
  • 2,147
  • 1
  • 12
  • 29
  • 2
    this has an undesired side effect, back button from navigationview stops working if you place that it on NavigationView exactly, but can be fixed by adding it on the child of NavigationView – Cyber Gh Dec 19 '20 at 22:31
  • *as well as buttons and NavLinks (among others) – thisIsTheFoxe Jan 21 '21 at 10:35
  • Does not work. After the keyboard gets dismissed the first time, I can't get the keyboard back when tapping on the textview. It just dismisses automatically. I checked and the UIApplication endEediting does not get called after the first time, so it's not that. – TimBigDev Mar 19 '21 at 02:07
3

here is the solution using DragGesture it's working.

struct ContentView: View {
    @State var text: String = ""
    var body: some View {
        VStack {
            TextField("My Text", text: $text)
                .keyboardType(.decimalPad)
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        .edgesIgnoringSafeArea(.all)
        .gesture(
            TapGesture()
                .onEnded { _ in
                    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
            }
        )
    }
}
Ruchi Makadia
  • 1,005
  • 1
  • 7
  • 20
  • 2
    It is a solution. Unfortunately it only works if the user clicks inside the `VStack`. If the user clicks in any area that is not the stack, it will not work. I found an answer that solves it it in this [question](https://stackoverflow.com/questions/56491386/how-to-hide-keyboard-when-using-swiftui). If we create a specific type of background view and embed all elements of our view in it, it will work for the user clicking anywhere on the view. – Jujuba Feb 03 '20 at 10:01
  • 2
    The mentioned solutions all have a major caveat: they disable taps with a desired action, e.g., on a list item with a `NavigationLink`. One working solution can be found here: https://forums.developer.apple.com/thread/127196 – Hardy Apr 12 '20 at 06:54
1

Add tap gesture to most outer view and call extension method inside tap gesture closure.

struct InputView: View {
    @State var inputValue : String = ""
    var body: some View {
        
        VStack(spacing: 10) {
            TextField("$", text: $inputValue)
                .keyboardType(.decimalPad)
        } .onTapGesture(perform: {
            self.endTextEditing()
        })
    }
}

extension View {
    func endTextEditing() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
                                        to: nil, from: nil, for: nil)
    }
}
Chaitanya
  • 52
  • 4
0
TextField("Phone Number", text: $no)
    .keyboardType(.numbersAndPunctuation)
    .padding()
    .background(Color("4"))
    .clipShape(RoundedRectangle(cornerRadius: 10))
    .offset(y:-self.value).animation(.spring()).onAppear() {
         NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillShowNotification, object: nil, queue: .main){ (notif)in
        let value  = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
            let height = value.height
            self.value = height


        }
        NotificationCenter.default.addObserver(forName:UIResponder.keyboardWillHideNotification, object: nil, queue: .main){ (notification)in
        self.value = 0
    }
}

Tom Slabbaert
  • 21,288
  • 10
  • 30
  • 43
mila kohen
  • 15
  • 10
0

In SwiftUI 3 @FocusState wrapper can be used to remove or switch focus from editable fields. When the focus is removed from field, keyboard dismisses. So in your case it is just a matter of giving space and gesture to the surrounding space of TextView.

struct ContentView: View {
    @State var inputValue : String = ""
    @FocusState private var inputIsFocused: Bool
    var body: some View {
        VStack(spacing: 10) {
            TextField("$", text: $inputValue)
                .keyboardType(.decimalPad)
                .border(Color.green)
                .focused($inputIsFocused)
            
        }
        .frame(maxHeight: .infinity) // If input is supposed to be in the center
        .background(.yellow)
        .onTapGesture {
            inputIsFocused = false
        }
    }
}  

But we can do more interesting things with @FocusState. How about switching from field to field in a form. And if you tap away, keyboard also dismisses.

struct ContentView: View {
    enum Field {
        case firstName
        case lastName
        case emailAddress
    }

    @State private var firstName = ""
    @State private var lastName = ""
    @State private var emailAddress = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        ZStack {
            VStack {
                TextField("Enter first name", text: $firstName)
                    .focused($focusedField, equals: .firstName)
                    .textContentType(.givenName)
                    .submitLabel(.next)
                
                TextField("Enter last name", text: $lastName)
                    .focused($focusedField, equals: .lastName)
                    .textContentType(.familyName)
                    .submitLabel(.next)
                
                TextField("Enter email address", text: $emailAddress)
                    .focused($focusedField, equals: .emailAddress)
                    .textContentType(.emailAddress)
                    .submitLabel(.join)
            }
            .onSubmit {
                switch focusedField {
                case .firstName:
                    focusedField = .lastName
                case .lastName:
                    focusedField = .emailAddress
                default:
                    print("Creating account…")
                }
            }
        }
        .textFieldStyle(.roundedBorder)
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle()) // So ZStack becomes clickable
        .onTapGesture {
            focusedField = nil
        }
    }
}
 
Paul B
  • 3,989
  • 33
  • 46
  • 1
    I have not tried this solution, but I wanted to note the following: "Important: You should not attempt to use the same focus binding for two different form fields." https://www.hackingwithswift.com/quick-start/swiftui/how-to-dismiss-the-keyboard-for-a-textfield – xdeleon Aug 11 '23 at 21:11
  • Definitely worth noting, @xdeleon. The question and the answer are about `VStack`, but forms are legit candidates for the UIs in question. So what works in stacks might unexpectedly fail in forms if one ignores such nuances. – Paul B Aug 13 '23 at 10:08
0

I think that's probably the easy way to do it

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

struct test: View {
    @State private var pass = ""

    var body: some View {
        ZStack {
            Color.white.edgesIgnoringSafeArea(.all)
                .onTapGesture(perform: {
                    self.hideKeyboard()
                })

            VStack {
                TextField("测试", text: $pass)
                    .font(.largeTitle)
                    .foregroundColor(.red)
                    .frame(width: 350, height: 90)
                    .textFieldStyle(.roundedBorder)
                    .buttonBorderShape(.roundedRectangle)
                    .multilineTextAlignment(.center)
            }
        }
    }
}