SwiftUI - Close Keyboard on Scroll

Viewed 6199

I have a simple search list:


struct ContentView: View {
    @State var text:String = ""
    var items = 1...100
    var body: some View {
        VStack {
            List {
                TextField("Search", text: $text)
                Section{
                    ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
                       Text("option \(i)")
                    }
                }
            }
        }
    }
}

iOS simulator screenshot

How can I make the keyboard close when scrolling for more than 2 cells/few points?

6 Answers

If you are using a ScrollView (probably also with a List but I haven't confirmed it), you could use the UIScrollView appearance, this will affect all ScrollViews though.

UIScrollView.appearance().keyboardDismissMode = .onDrag

A thorough discussion on how to resign the keyboard with various answers can be found for this question.

One solution to resign the keyboard on a drag gesture in the list is using a method on UIApplication window as shown below. For easier handling I created an extension on UIApplication and view modifier for this extension and finally an extension to View:

extension UIApplication {
    func endEditing(_ force: Bool) {
        self.windows
            .filter{$0.isKeyWindow}
            .first?
            .endEditing(force)
    }
}

struct ResignKeyboardOnDragGesture: ViewModifier {
    var gesture = DragGesture().onChanged{_ in
        UIApplication.shared.endEditing(true)
    }
    func body(content: Content) -> some View {
        content.gesture(gesture)
    }
}

extension View {
    func resignKeyboardOnDragGesture() -> some View {
        return modifier(ResignKeyboardOnDragGesture())
    }
}

So the final modifier for resigning the keyboard is just one modifier that has to be placed on the list like this:

List {
    ForEach(...) {
        //...
    }
}
.resignKeyboardOnDragGesture()

I have also implemented a pure swiftUI version of a search bar that might be interesting for you. You can find it in this answer.

Form {
    ...
}.gesture(DragGesture().onChanged { _ in
    UIApplication.shared.windows.forEach { $0.endEditing(false) }
})
struct EndEditingKeyboardOnDragGesture: ViewModifier {
    func body(content: Content) -> some View {
        content.highPriorityGesture (
            DragGesture().onChanged { _ in 
                UIApplication.shared.endEditing()
            }
        )
    }
}

extension View {
    func endEditingKeyboardOnDragGesture() -> some View {
        return modifier(EndEditingKeyboardOnDragGesture())
    }
}

@FocusState wrapper along with .focused() TextField modifier can be useful.

struct ContentView: View {
    @FocusState private var focusedSearchField: Bool
    @State var text:String = ""
    var items = 1...100
    var body: some View {
        VStack {
            List {
                TextField("Search", text: $text)
                    .focused($focusedSearchField)
                Section{
                    ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
                        Text("option \(i)")
                    }
                }
            } // to also allow swipes on items (theoretically)
            .simultaneousGesture(DragGesture().onChanged({ _ in
                focusedSearchField = false
            }))
            .onTapGesture { // dissmis on tap as well
                focusedSearchField = false
            }
        }
    }
}

As for now, since iOS 16 beta we have a new modifier scrollDismissesKeyboard() that allows to do exactly what you need.

In your example it should look like

struct ContentView: View {
    @State var text: String = ""
    var items = 1...100
    var body: some View {
        List {
            TextField("Search", text: $text)
            Section {
                ForEach(items.filter({"\($0)".contains(text)}), id: \.self) { (i) in
                    Text("option \(i)")
                }
            }
        }
        .scrollDismissesKeyboard(.interactively) // <<-- Put this line
    }
}

The scrollDismissesKeyboard() modifier has a parameter that determine the dismiss rules. Here are the possible values:

  • .automatic: Dismissing based on the context of the scroll.
  • .immediately: The keyboard will be dismissed as soon as any scroll happens.
  • .interactively: The keyboard will move/disappear inline with the user’s gesture.
  • .never: The keyboard will never dismissed when user is scrolling.
Related