'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 1:[1]
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)")
}
}
}
}
Solution 2:[2]
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
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |