Sitemap
Mok5ha G

Mok5ha G

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)")
}
}

--

--