Using SwiftUI’s .task instead of .onAppear

1 minute read

As I’ve been learning SwiftUI, there are times that I’ve wanted to run some async code when a view appeared. The straightforward translation of that intent is to create a Task in the .onAppear view modifier.

// Naive implementation -- probably not what you want to do
var body: some View {
    Text("Awesome Sauce")
        .onAppear {
            Task {
                // Async task that is NOT cancelled when view disappears.
            }
        }
}

Each time the View appears, a new task will be created. If this is in a NavigationStack where views can be pushed and popped often, you may end up with many tasks running for the same view or doing background work for a view that no longer exists.

SwiftUI supports this common pattern with the .task view modifier. The primary benefit of this over the naive approach is support for cancellation when the view disappears.

// Improved implementation -- supports task cancellation
var body: some View {
    Text("Awesome Sauce")
        .task {
            // Async task that is cancelled when the View disappears.
        }
}

Supporting Task Cancellation

Just because a task is cancelled doesn’t mean that it’s terminated. That would result in undefined behavior. Instead, it is up to the developer to check if a task is cancelled and handle it appropriately for the workload that’s behing performed. From the Swift documentation:

Depending on the work you’re doing, that usually means one of the following:

  • Throwing an error like CancellationError
  • Returning nil or an empty collection
  • Returning the partially completed work

There are two primary ways of checking if the task has been cancelled:

There are static and instance versions of these. If inside the task, use the static versions and it will report on the currently running task. If you have an instance of a task, use the instance version to check on that specific task’s status.