I have a SwiftUI app with SwiftUI App life cycle. I’m trying to setup a standard way to add
typing debounce to TextFields. Ideally, I’d like to create my own TextField modifier that
can easily be applied to views that have many textfields to edit. I’ve tried a bunch of
ways to do this but I must be missing something fundamental. Here’s one example. This
does not work:
struct ContentView: View {
@State private var searchText = ""
var body: some View {
VStack {
Text("You entered: (searchText)")
.padding()
TextField("Enter Something", text: $searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
.onChange(of: searchText, perform: { _ in
var subscriptions = Set<AnyCancellable>()
let pub = PassthroughSubject<String, Never>()
pub
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.collect()
.sink(receiveValue: { t in
self.searchText = t.first ?? "nothing"
} )
.store(in: &subscriptions)
})
}
}
}
Any guidance would be appreciated. Xcode 12.4, iOS 14.4
4
Answers
I think you’ll have to keep two variables: one for the text in the field as the user is typing and one for the debounced text. Otherwise, the user wouldn’t see the typing coming in in real-time, which I’m assuming isn’t the behavior you want. I’m guessing this is probably for the more standard use case of, say, performing a data fetch once the user has paused their typing.
I like ObservableObjects and Combine to manage this sort of thing:
I included two examples — the top, where the container view (
ContentView
) owns the ObservableObject and the bottom, where it’s made into a more-reusable component.A little simplified version of text debouncer from @jnpdx
Note that
.assign(to: &$debouncedText)
doesn’t create a reference cycle and manages subscription for you automaticallyIf you are not able to use an
ObservableObject
(ie, if your view is driven by a state machine, or you are passing the input results to a delegate, or are simply publishing the input), there is a way to accomplish the debounce using only view code. This is done by forwarding text changes to a localPublisher
, then debouncing the output of thatPublisher
.Or if broadcasting the changes:
However, if you have the choice, it may be more "correct" to go with the
ObservableObject
approach.Tunous on GitHub added a debounce extension to onChange recently. https://github.com/Tunous/DebouncedOnChange that is super simple to use. Instead of adding .onChange(of: value) {newValue in doThis(with: newValue) } you can add .onChange(of: value, debounceTime: 0.8 /sec/ ) {newValue in doThis(with: newValue) }
He sets up a Task that sleeps for the debounceTime but it is cancelled and reset on every change to value. The view modifier he created uses a State var debounceTask. It occurred to me that this task could be a Binding instead and shared amount multiple onChange view modifiers allowing many textfields to be modified on the same debounce. This way if you programmatically change a bunch of text fields using the same debounceTask only one call to the action is made, which is often what one wants to do. Here is the code with a simple example.
I haven’t tried the shared debounceTask binding using an ObservedObject or StateObject, just a State var as yet. If anyone tries that please post the result.