Skip to content

The task View Modifier (SwiftUI)

A SwiftUI guide for coding agents. Also covers async work on view appear, task id cancellation, debounce search requests, cooperative cancellation view lifecycle, task priority modifier.

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

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-the-power-of-task-view-modifier" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems