.task starts an async task tied to the view's lifecycle: SwiftUI cancels it through cooperative cancellation when the view disappears. The id: variant restarts it on data changes, which makes debounced search a three-line pattern.
Lifecycle-Bound Async Work
Attach .task and the work starts when the view appears and cancels when it leaves.
List(store.products) { product in
NavigationLink(product.id.uuidString) { Text(product.id.uuidString) }
}
.task {
await store.fetchProducts()
}
Cancellation is cooperative: Task.sleep throws CancellationError when the task cancels mid-sleep, and long loops should check Task.isCancelled. The default priority is .userInitiated; pass a lower one for background work.
.task(priority: .utility) {
await store.fetchProducts()
}
task(id:) Restarts on Data Changes
.task(id:) observes an Equatable value. On every change SwiftUI cancels the running task and starts a fresh one with the new value.
.searchable(text: $query)
.task(id: query) {
await store.search(matching: query)
}
Debounce With Sleep Plus Cancellation
Sleep before the request. If the user types again inside the window, SwiftUI cancels the sleeping task, so no request fires for stale queries.
.task(id: query) {
do {
try await Task.sleep(nanoseconds: 300_000_000)
await store.search(matching: query)
} catch {
// cancelled before the request fired
}
}
A Reusable Debouncing Modifier
Wrap the pattern once and call it as .task(id:nanoseconds:).
struct DebouncingTaskViewModifier<ID: Equatable>: ViewModifier {
let id: ID
let priority: TaskPriority
let nanoseconds: UInt64
let task: @Sendable () async -> Void
func body(content: Content) -> some View {
content.task(id: id, priority: priority) {
do {
try await Task.sleep(nanoseconds: nanoseconds)
await task()
} catch {
// ignore cancellation
}
}
}
}
extension View {
func task<ID: Equatable>(
id: ID,
priority: TaskPriority = .userInitiated,
nanoseconds: UInt64 = 0,
task: @Sendable @escaping () async -> Void
) -> some View {
modifier(DebouncingTaskViewModifier(id: id, priority: priority, nanoseconds: nanoseconds, task: task))
}
}
// call site
.task(id: query, nanoseconds: 300_000_000) {
await store.search(matching: query)
}