Logo for tanaschita.com

Cancelling async tasks in SwiftUI

Learn how to cancel async work in SwiftUI with automatic and manual approaches.

22 Sep 2025 · 5 min read

Sometimes we want to cancel async work when certain events happen. A typical example is when a user navigates away from a screen while a network request is still loading. If the result is no longer relevant, we might want the request to stop.

SwiftUI helps us here by tying tasks to the lifecycle of views, which means cancellation often happens automatically. In other cases, we may still want explicit control or even prevent SwiftUI from cancelling the task.

Let's look at both.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

A common way to start async work in SwiftUI is with the task modifier. It runs as soon as the view appears and is automatically cancelled as soon as the view disappears:

struct ContentView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.title)
}
.task {
items = await loadItems()
}
}
}

In this example, if the user leaves the screen before the data arrives, SwiftUI cancels the work and any awaited functions respect that cancellation.

Cancellation does not "unsend" a network request that has already left the device, the server will still process it. But it prevents any further code inside the task from continuing once the cancellation flag is set. For example, if loadItems() checks for cancellation or performs multiple awaits, it stops earlier.

Sometimes, we want to repeat the work whenever a certain input changes. SwiftUI offers a variant of the same modifier for that: task(id:). It runs once when the view appears, and again every time the id value changes. The old task is cancelled automatically before a new one starts.

struct SearchView: View {
@State private var results: [Result] = []
@State private var query = ""
var body: some View {
VStack {
TextField("Search", text: $query)
List(results) { result in
Text(result.title)
}
}
.task(id: query) {
results = try await search(for: query)
}
}
}

This allows us, for example, to handle search or filtering scenarios. The task is cancelled when the user types the next letter, and a new one starts with the updated query. An important note is that each keystroke still triggers a network request. In a real app we would normally add a debounce or delay to avoid flooding the server.

Because Swift concurrency is structured, cancellation automatically propagates down into all awaited calls. If we are running long-running loops or multiple awaits, it can be useful to explicitly check for cancellation using try Task.checkCancellation() or to handle CancellationError in our error handling. For example:

func loadItems() async throws -> [Item] {
var all: [Item] = []
for page in 1...10 {
try Task.checkCancellation()
let batch = try await fetchPage(page)
all.append(contentsOf: batch)
}
return all
}

Sometimes we want a task to continue even after the view disappears. For example, when loading data into a shared cache or database that other parts of the app will use.

In that case, we can start a task in a long-lived type, like this:

class DataService {
func load() {
Task {
let data = await api.load()
await cache.save(data)
}
}
}

Because this task isn't tied to a view, SwiftUI will not cancel it when a screen disappears.

If, however, we also want to manually cancel this work (for example, if the user logs out or explicitly cancels a download), we can keep a reference to the task so we can call cancel():

class DataService {
private var task: Task<Void, Never>?
func startWork() {
task = Task {
await doSomeWork()
}
}
func cancelWork() {
task?.cancel()
}
}

This way we can integrate manual cancellation into workflows that go beyond the lifetime of a single view. For even finer control, Swift provides withTaskCancellationHandler which lets us run cleanup code when cancellation happens.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

swiftui

swift

concurrency

async/await

ios

Building responsive and reliable search in SwiftUI

Learn how to debounce search input, cancel obsolete requests and prevent stale results using Swift concurrency.

14 Sep 2026 · 6 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum