8

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
                }
            })
        }
    }
}
nickcoding
  • 305
  • 8
  • 35

2 Answers2

20

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)")
                }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
11

There are usually two most common techniques used when dealing with delaying search query calls: throttling or debouncing.


To implement these concepts in SwiftUI, you can use Combine frameworks throttle/debounce methods.

An example of that would look something like this:

import SwiftUI
import Combine

final class ViewModel: ObservableObject {
    private var disposeBag = Set<AnyCancellable>()

    @Published var text: String = ""

    init() {
        self.debounceTextChanges()
    }

    private func debounceTextChanges() {
        $text
            // 2 second debounce
            .debounce(for: 2, scheduler: RunLoop.main)

            // Called after 2 seconds when text stops updating (stoped typing)
            .sink {
                print("new text value: \($0)")
            }
            .store(in: &disposeBag)
    }
}

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View {
        TextField("Search", text: $viewModel.text)
    }
}

You can read more about Combine and throttle/debounce in official documentation: throttle, debounce

Tomas Jablonskis
  • 4,246
  • 4
  • 22
  • 40