Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - dismissing keyboard on tapping anywhere in the view - issues with other interactive elements

Tags:

ios

swift

swiftui

I have a TextField and some actionable elements like Button, Picker inside a view. I want to dismiss the keyboard when the use taps outside the TextField. Using the answers in this question, I achieved it. However the problem comes with other actionable items.

When I tap a Button, the action takes place but the keyboard is not dismissed. Same with a Toggle switch. When I tap on one section of a SegmentedStyle Picker, the keyboard is dimissed but the picker selection doesn't change.

Here is my code.


struct SampleView: View {

    @State var selected = 0
    @State var textFieldValue = ""

    var body: some View {
        VStack(spacing: 16) {
            TextField("Enter your name", text: $textFieldValue)
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                .background(Color(UIColor.secondarySystemFill))
                .cornerRadius(4)


            Picker(selection: $selected, label: Text(""), content: {
                Text("Word").tag(0)
                Text("Phrase").tag(1)
                Text("Sentence").tag(2)
            }).pickerStyle(SegmentedPickerStyle())            

            Button(action: {
                self.textFieldValue = "button tapped"
            }, label: {
                Text("Tap to change text")
            })

        }.padding()
        .onTapGesture(perform: UIApplication.dismissKeyboard)
//        .gesture(TapGesture().onEnded { _ in UIApplication.dismissKeyboard()})
    }
}

public extension UIApplication {

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

As you can see in the code, I tried both options to get the tap gesture and nothing worked.

like image 699
Imthath Avatar asked Feb 22 '20 04:02

Imthath


3 Answers

You can create an extension on View like so

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

and use it for the Views you want to dismiss the keyboard.

.onTapGesture {

      self.endTextEditing()
} 

I have just seen this solution in a recent raywenderlich tutorial so I assume it's currently the best solution.

like image 152
Marc T. Avatar answered Nov 15 '22 08:11

Marc T.


Dismiss the keyboard by tapping anywhere (like others suggested) could lead to very hard to find bug (or unwanted behavior).

  1. you loose default build-in TextField behaviors, like partial text selection, copy, share etc.
  2. onCommit is not called

I suggest you to think about gesture masking based on the editing state of your fields

/// Attaches `gesture` to `self` such that it has lower precedence
    /// than gestures defined by `self`.
    public func gesture<T>(_ gesture: T, including mask: GestureMask = .all) -> some View where T : Gesture

this help us to write

.gesture(TapGesture().onEnded({
            UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
        }), including: (editingFlag) ? .all : .none)

Tap on the modified View will dismiss the keyboard, but only if editingFlag == true. Don't apply it on TextField! Otherwise we are on the beginning of the story again :-)

This modifier will help us to solve the trouble with Picker but not with the Button. That is easy to solve while dismiss the keyboard from its own action handler. We don't have any other controls, so we almost done

Finally we have to find the solution for rest of the View, so tap anywhere (excluding our TextFields) dismiss the keyboard. Using ZStack filled with some transparent View is probably the easiest solution.

Let see all this in action (copy - paste - run in your Xcode simulator)

import SwiftUI
struct ContentView: View {

    @State var selected = 0

    @State var textFieldValue0 = ""
    @State var textFieldValue1 = ""

    @State var editingFlag = false

    @State var message = ""

    var body: some View {
        ZStack {
            // TODO: make it Color.clear istead yellow
            Color.yellow.opacity(0.1).onTapGesture {
                UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
            }
            VStack {

                TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
                    self.editingFlag = editing
                }, onCommit: {
                    self.onCommit(txt: "salutation commit")
                })
                    .padding()
                    .background(Color(UIColor.secondarySystemFill))
                    .cornerRadius(4)

                TextField("Welcome message", text: $textFieldValue1, onEditingChanged: { editing in
                    self.editingFlag = editing
                }, onCommit: {
                    self.onCommit(txt: "message commit")
                })
                    .padding()
                    .background(Color(UIColor.secondarySystemFill))
                    .cornerRadius(4)

                Picker(selection: $selected, label: Text(""), content: {
                    Text("Word").tag(0)
                    Text("Phrase").tag(1)
                    Text("Sentence").tag(2)
                })
                    .pickerStyle(SegmentedPickerStyle())
                    .gesture(TapGesture().onEnded({
                        UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
                    }), including: (editingFlag) ? .all : .none)


                Button(action: {
                    self.textFieldValue0 = "Hi"
                    print("button pressed")
                    UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
                }, label: {
                    Text("Tap to change salutation")
                        .padding()
                        .background(Color.yellow)
                        .cornerRadius(10)
                })

                Text(textFieldValue0)
                Text(textFieldValue1)
                Text(message).font(.largeTitle).foregroundColor(Color.red)

            }

        }
    }

    func onCommit(txt: String) {
        print(txt)
        self.message = [self.textFieldValue0, self.textFieldValue1].joined(separator: ", ").appending("!")
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here

If you miss onCommit (it is not called while tap outside TextField), just add it to your TextField onEditingChanged (it mimics typing Return on keyboard)

TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
    self.editingFlag = editing
    if !editing {
        self.onCommit(txt: "salutation")
    }
 }, onCommit: {
     self.onCommit(txt: "salutation commit")
 })
     .padding()
     .background(Color(UIColor.secondarySystemFill))
     .cornerRadius(4)
like image 36
user3441734 Avatar answered Nov 15 '22 09:11

user3441734


I'd like to take Mark T.s Answer even further and add the entire function to an extension for View:

extension View {
    func hideKeyboardWhenTappedAround() -> some View  {
        return self.onTapGesture {
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), 
                  to: nil, from: nil, for: nil)
        }
    }
}

Can then be called like:

var body: some View {
    MyView()
      // ...
      .hideKeyboardWhenTappedAround()
      // ...
}
like image 45
emmics Avatar answered Nov 15 '22 09:11

emmics