Debounce & Throttle with Combine

rozeri dilar
iOS App Mastery
Published in
3 min readJul 14, 2023

Both throttle and debounce are operators that can be used for handling rapid changes or updates in values. However, they differ in how they handle the timing of emitting values.

Throttle:

With throttle, the publisher will emit the latest value and then ignore subsequent values for a specified duration. If multiple changes occur within that duration, only the latest value will be emitted. It's useful when you want to limit the frequency of emissions and ensure a minimum time interval between emissions.

Here’s an example scenario: Suppose you have a button that users can tap, and you want to handle the tap events but prevent accidental multiple taps in quick succession. You can use throttle to ensure that the button tap event is processed only once every 500 milliseconds, regardless of how many times the button is tapped.

import Combine

class ButtonTapViewModel {
private var cancellables = Set<AnyCancellable>()
func configureButtonTapHandling() {
let buttonTapPublisher = PassthroughSubject<Void, Never>()
buttonTapPublisher
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
self?.handleButtonTap()
}
.store(in: &cancellables)
simulateButtonTapEvents(buttonTapPublisher)
}
private func handleButtonTap() {
// Perform the desired action here
}
private func simulateButtonTapEvents(_ publisher: PassthroughSubject<Void, Never>) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
publisher.send(())
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
publisher.send(())
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
publisher.send(())
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
publisher.send(())
}
}
}
let viewModel = ButtonTapViewModel()
viewModel.configureButtonTapHandling()

In this example, the ButtonTapViewModel sets up a publisher buttonTapPublisher of type PassthroughSubject<Void, Never>, representing the button tap events. The throttle operator is applied to the publisher, specifying a throttle duration of 0.5 seconds and using the main DispatchQueue for scheduling. The latest parameter is set to true, meaning only the latest value will be emitted if multiple taps occur within the throttle window.

The simulateButtonTapEvents method demonstrates how the button tap events can be simulated. In this case, it sends button tap events to the publisher at different time intervals (0.2, 0.4, 0.6, and 0.8 seconds). Since the throttle duration is set to 500 milliseconds, only the latest tap event within that duration will trigger the handleButtonTap method.

By using the throttle operator, you can ensure that the button tap event is processed only once every 500 milliseconds, regardless of how many times the button is tapped in quick succession.

Debounce:

With debounce, the publisher will wait for a specified duration of inactivity (no value emissions) before emitting the latest value. If a new value arrives before the duration of inactivity, the previous value is discarded, and the waiting period restarts. It's useful when you want to wait for a "quiet period" before processing the latest value, such as in search functionality.

Here’s an example scenario: Suppose you have a search bar where users can enter search queries, and you want to make API calls to fetch search results. Instead of making an API call for each keystroke, you can use debounce to wait for a short period (e.g., 500 milliseconds) of inactivity after the user finishes typing before triggering the API call. If the user continues typing within that period, the waiting period restarts, ensuring that the API call is only made after a short pause in typing.

import Combine
import Foundation

class SearchViewModel {
private var searchCancellable: AnyCancellable?
func performSearch(query: String) {
searchCancellable?.cancel()
// Debounce the search query with a 0.5-second delay
searchCancellable = Just(query)
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { searchQuery in
self.search(query: searchQuery)
}
}
private func search(query: String) {
// Perform the actual search operation here
}
}
let viewModel = SearchViewModel()
viewModel.performSearch(query: "apple")
viewModel.performSearch(query: "banana")
viewModel.performSearch(query: "cherry")
// Output:
// Performing search for query: cherry

In this example, the SearchViewModel class has a performSearch method that takes a search query as input. The method debounces the search query using the debounce operator to introduce a delay of 0.5 seconds between user input and the actual search request. The debounce operator ensures that the search request is triggered only when there is a pause in user input within the specified delay.

The debounced search query is then passed to the search method, which represents the actual search operation.

When you run the code, you’ll see that only the last search query (“cherry”) triggers the search operation. The previous search queries (“apple” and “banana”) are ignored due to the debounce delay, and the search operation is performed only after the user pauses or stops typing.

References

--

--