Debouncing UI Events: DispatchWorkItem vs. Task. sleep
2 min readOct 24, 2026
When a user types into a search bar, you don’t want to send an API request for every single keystroke. Debouncing delays execution until the user stops typing for a specific duration (e.g., 300ms). If a new keystroke arrives before the timer finishes, the previous request is cancelled.
Press enter or click to view image in full size![]()
Get Mok5ha G’s stories in your inbox
Join Medium for free to get updates from this writer.
Approach 1: Grand Central Dispatch (DispatchWorkItem.cancel())
import Foundation
final class SearchDebouncerGCD {
private var pendingWorkItem: DispatchWorkItem?
private let delay: TimeInterval
init(delay: TimeInterval = 0.3) {
self.delay = delay
}
func debounce(action: @escaping () -> Void) {
// Cancel previous pending task if user types again
pendingWorkItem?.cancel()
let workItem = DispatchWorkItem(block: action)
pendingWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}Approach 2: Swift Concurrency (Task.cancel() + Task.sleep)
import Foundation
@MainActor
final class SearchDebouncerAsync {
private var searchTask: Task<Void, Never>?
private let delayNanoseconds: UInt64
init(delaySeconds: Double = 0.3) {
self.delayNanoseconds = UInt64(delaySeconds * 1_000_000_000)
}
func debounce(action: @escaping () async -> Void) {
// Cancel previous task
searchTask?.cancel()
searchTask = Task {
// Sleep for specified delay
try? await Task.sleep(nanoseconds: delayNanoseconds)
// Check cancellation before firing
guard !Task.isCancelled else { return }
await action()
}
}
}SwiftUI Usage Example
import SwiftUI
struct SearchBarView: View {
@State private var query = ""
@StateObject private var viewModel = SearchViewModel()
var body: some View {
VStack {
TextField("Search...", text: $query)
.textFieldStyle(.roundedBorder)
.onChange(of: query) { newValue in
viewModel.onSearchQueryChanged(newValue)
}
}
.padding()
}
}
@MainActor
final class SearchViewModel: ObservableObject {
private let debouncer = SearchDebouncerAsync(delaySeconds: 0.3)
func onSearchQueryChanged(_ newQuery: String) {
guard !newQuery.isEmpty else { return }
debouncer.debounce { [weak self] in
await self?.performSearch(query: newQuery)
}
}
private func performSearch(query: String) async {
print("Executing API Search for query: \(query)")
}
}







