What is the difference between Task {} and .task in a SwiftUI file?
Both create a Task for running async code, but they differ in lifecycle, attachment, and cancellation semantics.
Task { }
This is the plain Swift Concurrency API — creates and immediately starts an unstructured task. It has no inherent connection to a view’s lifecycle.
Button("Load") {
Task {
await viewModel.loadData()
}
}
Key characteristics:
- You must manage cancellation yourself. The task keeps running even if the view disappears, unless you explicitly cancel it.
- Runs wherever you call it — inside a button action,
onAppear,onChange, init, anywhere. - Fire-and-forget by default. If you want it cancellable, you have to store the
Taskhandle:
struct MyView: View {
@State private var loadTask: Task<Void, Never>?
var body: some View {
SomeView()
.onAppear {
loadTask = Task {
await viewModel.loadData()
}
}
.onDisappear {
loadTask?.cancel()
}
}
}
That boilerplate (store the handle, cancel in onDisappear) is exactly what .task exists to eliminate.
.task { } (view modifier)
This is a SwiftUI-specific modifier introduced in iOS 15 that ties a task’s lifetime directly to the view’s lifecycle.
SomeView()
.task {
await viewModel.loadData()
}
Key characteristics:
- Automatically starts when the view appears, similar to
onAppear. - Automatically cancels when the view disappears. SwiftUI calls
cancel()on the underlying task for you — no manual handle management. - Runs before the view is fully on-screen in terms of task creation, tied to the view’s identity in the render tree — if the view’s identity changes, the task restarts.
- Supports an
id:parameter to restart the task when a value changes:
SomeView()
.task(id: userID) {
await viewModel.loadUser(userID)
}
This is the async equivalent of .onChange(of: userID) combined with cancel-and-restart — very useful for “reload when the selected item changes” patterns, since it cancels the in-flight task for the old userID before starting a new one.
Practical differences at a glance
Task { } |
.task { } |
|
|---|---|---|
| Attachment | None — just a closure call | Bound to a specific view’s lifecycle |
| Auto-cancel on disappear | No — manual | Yes — automatic |
| Where you can use it | Anywhere (button actions, init, closures) | Only as a view modifier |
| Restart on value change | Manual (onChange + cancel + relaunch) |
Built-in via .task(id:) |
| Typical use | Responding to a discrete user action (tap, submit) | Loading data / starting an ongoing async job while a view is visible |
When to use which
- Use
Task { }for anything triggered by a discrete event that should run to completion regardless of what the view does next — e.g., a button tap that submits a form, fires an analytics event, or starts a save that shouldn’t be interrupted just because the user navigates away. - Use
.task { }for anything that should only be “alive” while the view is on screen — the classic case being fetching data for a detail screen: if the user swipes away before the fetch completes, you generally want it cancelled rather than updating state on a view that’s gone.
One subtlety: because .task cancellation is cooperative (Swift Concurrency cancellation, not forced termination), your async code still needs to check Task.isCancelled or use cancellation-aware APIs (like URLSession’s async methods, which throw CancellationError automatically) for the cancellation to actually stop work early — otherwise the task is marked cancelled but keeps running until it hits a checkpoint.