Demystifying Debounce and Throttle in Combine Framework

Manikanta Sirumalla
5 min readOct 15, 2023

--

Combine, Apple’s framework for handling asynchronous and event-driven code, provides a range of operators to manipulate and control the flow of data within a publisher. Two commonly used operators for managing the rate of value emissions are `debounce` and `throttle`. In this article, we’ll dive into the differences between these operators and provide examples in Swift to illustrate their distinct behaviors.

What are Debounce and Throttle?

Both `debounce` and `throttle` operators serve the purpose of controlling how often values are emitted by a publisher, but they accomplish this in slightly different ways. Understanding these differences is crucial for making the right choice in your Combine code.

Debounce:

Debounce is primarily used to filter out rapidly changing values and emit the most recent value only after a specified “quiet” period. It waits for a pause in value emissions before emitting the latest value.

    .debounce(for: SchedulerTimeIntervalConvertible & Comparable & SignedNumeric, scheduler: Scheduler)
  • for: The time interval to wait for a pause in values before emitting the latest value.
  • scheduler: The scheduler (like DispatchQueue) on which the debouncing occurs.

Behavior:
- When a new value is received, the debounce operator starts a timer.
- If another value is received before the timer expires, the timer is reset.
- The operator only emits the latest value after the timer has completed without new input.

Use Cases:
- Debounce is particularly useful when you want to react to user input or data changes but don’t want to process every intermediate value.
- Common use cases include search bars, text input fields, or auto-suggestions, where you want to wait for the user to pause typing before initiating a search.

Throttle:

Throttle is used to limit the rate of value emissions from a publisher by allowing only one value through within a specified time interval. It ensures a constant rate of emission by effectively discarding intermediate values.

.throttle(for: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool)
  • for: The time interval to enforce a rate limit on value emissions.
  • scheduler: The scheduler on which the throttle operates.
  • latest: A boolean flag that controls whether the latest value within the time interval is emitted (if true) or if the first value is emitted (if false).

Behavior:
- When a new value is received, the throttle operator starts a timer and allows the value to pass through.
- Any subsequent values received during the timer’s duration are ignored.
- After the timer expires, the process repeats, allowing the next value through and starting a new timer.

Use Cases:
- Throttle is useful when you want to enforce a consistent rate of updates or when you want to prevent overloading a downstream system with excessive data.
- It’s commonly employed in scenarios like scrolling events or handling user interactions in UI components.

Examples in Swift

Let’s look at some Swift code examples to illustrate the differences between `debounce` and `throttle`.

Debounce Example

Suppose you have a search bar in your app, and you want to initiate a search request only after the user has paused typing for 0.8 seconds:

import Foundation
import Combine

public final class SearchTextDebounce: ObservableObject {
@Published var text: String = ""
@Published var debouncedText: String = ""
private var bag = Set<AnyCancellable>()

public init(dueTime: TimeInterval = 0.8) {
$text
.removeDuplicates()
.debounce(for: .seconds(dueTime),
scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.debouncedText = value
})
.store(in: &bag)
}
}

//in the search View
struct SearchView: View {

@StateObject var searchVM = SearchViewModel()
@StateObject var debouncedText = SearchTextDebounce()

var body: some View {
NavigationView {
ZStack{
if !searchVM.isLoading {
LogoView()
}
else {
ActivityIndicatorView(isLoading: $searchVM.isLoading)
}
PhotoCollectionView(selectedPhoto: $selectedPhoto, photos: searchVM.photos)
}
.searchable(text: $debouncedText.text, prompt: prompt)
.onChange(of: debouncedText.debouncedText) { searchTerm in
if !searchTerm.isEmpty {
searchVM.performPhotoSearch(with: searchTerm)
} else {
searchVM.clearSearch()
}
}
}
}
}

In this example, the `debounce` operator ensures that the search operation is only triggered after a 0.8-second pause in typing. If the user keeps typing, the timer is reset, and the search is delayed until the pause occurs.

the debounceText property will be updated with the most recent value of text, but with a debounce effect applied. It ensures that debounceText reflects the most recent value from text, but only after a specified quiet period (0.8 seconds by default). If new values are received during this period, the timer is reset, and the debounceText property will only be updated once the pause in updates exceeds the specified interval.

Throttle Example

Let’s say you have a button in your app, and you want to prevent rapid button presses from causing multiple actions. You want the button to respond to the first press and then ignore subsequent presses for 0.8 second:

import Foundation
import Combine

public final class SearchTextThrottle: ObservableObject {
@Published var text: String = ""
@Published var throttleText: String = ""
private var bag = Set<AnyCancellable>()

public init(dueTime: TimeInterval = 0.8) {
$text
.removeDuplicates()
.throttle(for: .seconds(dueTime),
scheduler: DispatchQueue.main, latest: false)
.sink(receiveValue: { [weak self] value in
self?.throttleText = value
})
.store(in: &bag)
}
}

//in the search View
struct SearchView: View {

@StateObject var searchVM = SearchViewModel()
@StateObject var throttledText = SearchTextThrottle()

var body: some View {
NavigationView {
ZStack{
if !searchVM.isLoading {
LogoView()
}
else {
ActivityIndicatorView(isLoading: $searchVM.isLoading)
}
PhotoCollectionView(selectedPhoto: $selectedPhoto, photos: searchVM.photos)
}
.searchable(text: $throttledText.text, prompt: prompt)
.onChange(of: throttledText.throttleText) { searchTerm in
if !searchTerm.isEmpty {
searchVM.performPhotoSearch(with: searchTerm)
} else {
searchVM.clearSearch()
}
}
}
}
}

In this example, the `throttle` operator ensures that only the first button press is processed, and subsequent presses within a 0.8-second window are ignored. You can control whether the latest value within the window is emitted or not by adjusting the `latest` parameter.

throttleText property will be updated with the first value that arrives within the defined time interval (0.8 seconds by default) and will ignore any intermediate values during that interval.

Conclusion

Understanding the differences between `debounce` and `throttle` in Combine is crucial for effective event handling and data flow control. Debounce is ideal for situations where you want to wait for a pause in value emissions, such as user input scenarios, while throttle is useful for enforcing a constant rate of value emissions, preventing rapid changes from overloading your system.

By applying these operators correctly, you can optimize the performance and responsiveness of your Combine-based applications.

Thank you! for reading the article!

--

--

Manikanta Sirumalla

iOS developer with a passion for crafting innovative mobile experiences. With expertise in Swift, SwiftUI. I thrive on creating seamless, user-centric apps.