Home > OS >  SwiftUI Keyboard Dismissing Issues
SwiftUI Keyboard Dismissing Issues

Time:10-05

The goal is to have the ability to dismiss the keyboard when tapping on the anywhere on the screen. I have now tried two approaches, each with presenting different issues.

Approach One

Take a given screen and just wrap it with a tap gesture:

struct ContentView: View {
    
    @State private var favoriteColor = 0
    @State private var text1:String = ""
    @State private var text2:String = ""
    
    var body: some View {
        
        VStack {
            
            TextField("Type Things", text: $text1)
            
            TextField("Type More Things", text: $text2)
            
            Picker("What is your favorite color?", selection: $favoriteColor) {
                Text("Red").tag(0)
                Text("Green").tag(1)
            }
            .pickerStyle(.segmented)

        }
        .frame(
              minWidth: 0,
              maxWidth: .infinity,
              minHeight: 0,
              maxHeight: .infinity,
              alignment: .center
        )
        .background(Color.blue.opacity(0.4))
        .onTapGesture {
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
        
    }
}

Issue: The tap gestures work on all controls expect segmentation control. When you press a given segmentation option the segmentations tap gesture doesn't seem to move the control to the selected option. In my opinion this seems like a SwiftUI bug. I have tried varying permutations of .simultaneousGesture(TapGesture().onEnded { })

Picker("What is your favorite color?", selection: $favoriteColor) {
    Text("Red").tag(0)
    Text("Green").tag(1)
}
.pickerStyle(.segmented)
.simultaneousGesture(TapGesture().onEnded { })

and .highPriorityGesture(TapGesture().onEnded { })

Picker("What is your favorite color?", selection: $favoriteColor) {
    Text("Red").tag(0)
    Text("Green").tag(1)
}
.pickerStyle(.segmented)
.highPriorityGesture(TapGesture().onEnded { })

In a final hack I did try the following. It kind of works. It just doesn't behave like a proper segmentation control:

Picker("What is your favorite color?", selection: $favoriteColor) {
    Text("Red").tag(0)
    Text("Green").tag(1)
}
.pickerStyle(.segmented)
.onTapGesture {
    if self.favoriteColor == 0 {
        self.favoriteColor = 1
    } else {
        self.favoriteColor = 0
    }
}

Approach two

Given the issues with the seg, I then went with option two and applied a UIKit tap gesture to the window, like so:

struct ContentView: View {

    @State private var favoriteColor = 0
    @State private var text1:String = ""
    @State private var text2:String = ""
    
    var body: some View {
        
        VStack {
            
            TextField("Type Things", text: $text1)
            
            TextField("Type More Things", text: $text2)
            
            Picker("What is your favorite color?", selection: $favoriteColor) {
                Text("Red").tag(0)
                Text("Green").tag(1)
            }
            .pickerStyle(.segmented)

        }
        .frame(
              minWidth: 0,
              maxWidth: .infinity,
              minHeight: 0,
              maxHeight: .infinity,
              alignment: .center
        )
        .background(Color.blue.opacity(0.4))

        
    }
}

extension UIApplication {
    func dismissKeyboardGestureRecognizer() {
        guard let window = windows.first else { return }
        let gesture = UITapGestureRecognizer(target: window, action: nil)
        gesture.requiresExclusiveTouchType = false
        gesture.cancelsTouchesInView = false
        gesture.delegate = self
        window.addGestureRecognizer(gesture)
    }
}

extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        return true
    }

    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

@main
struct SeggyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    UIApplication.shared.dismissKeyboardGestureRecognizer()
                }
        }
    }
}

Issue: With this approach the segmentation control works. However if you tap from one textfield to the other, you find that the keyboard bounces up and down. This is because the window gesture detects the tap and tries to dismiss the keyboard, then the other textfield registers as first responder and then brings the keyboard up and hence the bouncing.

Alas this bouncing isn't desired. With approach one this doesn't happen. I'm kind of stuck between a rock and a hard place. Anyone know how you can get the either the segmentation control to respond with approach 1 or stop the bouncing keyboard in approach 2?

Personally I think approach one should work and the the segmentation control has a bug and should be reported to Apple.

CodePudding user response:

Just apply the tap gesture to the background. Now you can dismiss anything by tapping the blue background:

struct ContentView: View {
    @State private var favoriteColor = 0
    @State private var text1: String = ""
    @State private var text2: String = ""

    var body: some View {
        VStack {
            TextField("Type Things", text: $text1)

            TextField("Type More Things", text: $text2)

            Picker("What is your favorite color?", selection: $favoriteColor) {
                Text("Red").tag(0)
                Text("Green").tag(1)
            }
            .pickerStyle(.segmented)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(
            Color.blue
                .opacity(0.4)
                .onTapGesture {
                    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                }
        )
    }
}

Result:

Result

  • Related