swiftswiftuitextfielddispatch-queue

SwiftUI: How to only run code when the user stops typing in a TextField?


so I'm trying to make a search bar that doesn't run the code that displays the results until the user stops typing for 2 seconds (AKA it should reset a sort of timer when the user enters a new character). I tried using .onChange() and an AsyncAfter DispatchQueue and it's not working (I think I understand why the current implementation isn't working, but I'm not sure I'm even attack this problem the right way)...

struct SearchBarView: View {
    @State var text: String = ""
    @State var justUpdatedSuggestions: Bool = false
    var body: some View {
        ZStack {
            TextField("Search", text: self.$text).onChange(of: self.text, perform: { newText in
                appState.justUpdatedSuggestions = true
                DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
                    appState.justUpdatedSuggestions = false
                })
                if justUpdatedSuggestions == false {
                    //update suggestions
                }
            })
        }
    }
}

Solution

  • The possible approach is to use debounce from Combine framework. To use that it is better to create separated view model with published property for search text.

    Here is a demo. Prepared & tested with Xcode 12.4 / iOS 14.4.

    import Combine
    
    class SearchBarViewModel: ObservableObject {
        @Published var text: String = ""
    }
    
    struct SearchBarView: View {
        @StateObject private var vm = SearchBarViewModel()
        var body: some View {
            ZStack {
                TextField("Search", text: $vm.text)
                    .onReceive(
                        vm.$text
                            .debounce(for: .seconds(2), scheduler: DispatchQueue.main)
                    ) {
                        guard !$0.isEmpty else { return }
                        print(">> searching for: \($0)")
                    }
            }
        }
    }