.task { } is a SwiftUI modifier introduced in iOS 15 that launches an asynchronous operation when a View appears and automatically cancels it when the View disappears. Unlike .onAppear, which executes synchronous code without the ability to cancel, .task works with the async/await context and respects the View lifecycle: when the View disappears, SwiftUI calls cancel() on the created Task. This prevents memory leaks and execution of operations after the View no longer needs updating. According to Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024), .task is the preferred way to load data in SwiftUI because it safely works with Structured Concurrency and automatically manages the lifetime of the asynchronous operation.
Key Takeaways
.task { } is a View modifier that creates a Task in an async context when the View appears on screen. SwiftUI runs the provided closure on a background thread, leaving the main thread free for UI operations. When the View disappears, SwiftUI automatically cancels the Task through the Structured Concurrency mechanism — this ensures that the asynchronous operation does not continue executing after its result is no longer needed.
According to Apple — Swift Programming Language (2025), .task uses the concept of Structured Concurrency introduced in Swift 5.5. Each .task creates a child task within the parent View’s task. If the parent task is cancelled (the View disappears), all child tasks are also cancelled automatically. This radically simplifies managing the lifecycle of asynchronous operations compared to manually storing references to DispatchWorkItem or AnyCancellable.
Unlike the traditional approach with @State + manual calls in .onAppear, .task does not require storing a reference to the Task for subsequent cancellation. SwiftUI does this automatically, reducing boilerplate code and eliminating the risk of forgetting to cancel a task.
struct ArticlesView: View {
@State var articles: [Article] = []
@State var error: Error?
var body: some View {
List(articles) { article in
Text(article.title)
}
.task {
do {
articles = await APIClient().fetchArticles()
} catch {
self.error = error
}
}
}
}
Many developers are used to loading data in .onAppear, but with the arrival of async/await and .task, this approach has become outdated. .onAppear executes code synchronously — for async operations inside .onAppear, you need to wrap the call in Task { } and manually keep a reference to it for possible cancellation. .task does this automatically.
| Characteristic | .task { } | .onAppear |
|---|---|---|
| Async context | Built-in async/await | Requires Task { } wrapper |
| Auto-cancellation | Yes, when View disappears | No, must be implemented manually |
| Structured Concurrency | Supports | Does not support |
| Re-triggering | Only when id changes | Every time the View appears |
| Apple recommendation | Preferred approach | For synchronous operations |
.onAppear is still useful for synchronous operations — for example, logging or initial UI setup. But for asynchronous data loading, network requests, database or file system operations, use .task. It is safer and cleaner from an architectural standpoint.
// ❌ Legacy approach: Task in .onAppear without cancellation
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ Modern approach: .task manages cancellation
func body() { var body: some View { Text("") }
.task { await loadData() }
The .task(id:) modifier accepts an additional parameter — an identifier. When the identifier value changes, SwiftUI cancels the current task and starts a new one with the new identifier. This is ideal for screens where data depends on a selected parameter — for example, a list of articles by category or product details by ID.
struct CategoryView: View {
let categoryId: Int
@State var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task(id: categoryId) {
await loadItems(for: categoryId)
}
}
func loadItems(for id: Int) async {
do {
items = await APIClient().fetchItems(categoryId: id)
} catch {
// handle error
}
}
}
When categoryId changes, SwiftUI cancels the previous request and starts a new one. This is especially important for rapid category switching — old requests will not compete with new ones for state updates. Without .task(id:), you would have to manually track changes through .onChange and manage the Task manually.
Although .task automatically cancels the task when the View disappears, the asynchronous operation itself must cooperatively check for cancellation. Swift uses a cooperative cancellation model — Task.cancel() does not forcibly stop execution, but only sets the isCancelled flag. Code inside the task should periodically check this flag.
struct LoadingView: View {
@State var progress: Double = 0
var body: some View {
ProgressView(value: progress)
.task {
for i in 0..<100 {
// Check cancellation
try Task.checkCancellation()
await Task.sleep(nanoseconds: 50_000_000)
progress = Double(i + 1) / 100.0
}
}
}
}
Task.checkCancellation() throws a CancellationError if the task was cancelled. This is the simplest way to check — it works in any async context. An alternative is to check Task.isCancelled manually before expensive operations. For URLSession, network requests are automatically cancelled when the task is cancelled, since URLSession supports Structured Concurrency out of the box.
.task is suitable for many scenarios: from simple JSON loading to complex parallel operations with TaskGroup. Let us look at three typical use cases.
struct ProfileView: View {
@State var profile: Profile?
@State var isLoading = true
var body: some View {
Group {
if isLoading {
ProgressView()
} else if let profile {
Text(profile.name)
} else {
Text("Failed to load")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// profile stays nil
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("Dashboard")
.task {
stats = await Task {
await withThrowingTaskGroup { group in
group.addTask { await API().fetchUsers() }
group.addTask { await API().fetchOrders() }
group.addTask { await API().fetchRevenue() }
return DashboardStats(
users: try await group.next(),
orders: try await group.next(),
revenue: try await group.next()
)
}
}.value
}
}
}
The most common mistake is mutating UI properties inside .task without switching to the main thread. Although SwiftUI automatically returns updates to the main thread when modifying @State in an async context, direct manipulations of UIKit elements inside .task can cause a crash.
// ❌ Error: no error handling
.task {
let data = await fetchData() // crashes on error!
items = data
}
// ✅ Correct: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
Frequently Asked Questions
.task automatically cancels the task when the View disappears and supports Structured Concurrency. Task { } in .onAppear requires manually keeping a reference to the task and calling cancel() in .onDisappear. .task is also easier to read — it explicitly indicates that data loading is part of the View lifecycle.
Yes, to subscribe to an AsyncSequence or AsyncStream, use for await value in publisher.values inside .task. This works with both async/await and Combine via the Publisher.values extension. When the View disappears, the iteration will automatically end and the subscription will be cancelled.
Yes, if the View inside TabView is recreated on each switch. Starting from iOS 18, TabView can keep Views in memory — in this case .task does not restart. Use .task(id:) with a tab identifier if you need to reload data on every tab switch.
SwiftUI will cancel the task when the View disappears. If the URLSession request was inside the task, it will also be cancelled. If the task does not support cooperative cancellation (for example, it does not check isCancelled), it will continue running, but its result will not be applied to state because the View no longer exists.
Yes, you can add multiple .task modifiers to a single View. Each creates an independent task. This is useful for separating different data sources: one .task for loading a profile, another for subscribing to a WebSocket, a third for monitoring geolocation.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also