Updated for Xcode 14.2
Swift’s tasks use cooperative cancellation, which means that although we can tell a task to stop work, the task itself is free to completely ignore that instruction and carry on for as long as it wants. This is a feature rather than a bug: if cancelling a task made it stop work immediately, the task might leave your program in an inconsistent state.
There are seven things to know when working with task cancellation:
cancel()
method.Task.isCancelled
to determine whether the task has been cancelled or not.Task.checkCancellation()
method, which will throw a CancellationError
if the task has been cancelled or do nothing otherwise.Task.sleep()
to wait for some amount of time to pass, that will not honor cancellation requests – the task will still sleep even when cancelled.task()
modifier, that task will automatically be canceled when the view disappears.We can explore a few of these in code. First, here’s a function that uses a task to fetch some data from a URL, decodes it into an array, then returns the average:
func getAverageTemperature() async {
let fetchTask = Task { () -> Double in
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
let sum = readings.reduce(0, +)
return sum / Double(readings.count)
}
do {
let result = try await fetchTask.value
print("Average temperature: \(result)")
} catch {
print("Failed to get data.")
}
}
await getAverageTemperature()
Download this as an Xcode project
Now, there is no explicit cancellation in there, but there is implicit cancellation because the URLSession.shared.data(from:)
call will check to see whether its task is still active before continuing. If the task has been cancelled, data(from:)
will automatically throw a URLError
and the rest of the task won’t execute.
However, that implicit check happens before the network call, so it’s unlikely to be an actual cancellation point in practice. As most of our users are likely to be using mobile network connections, the network call is likely to take most of the time of this task, particularly if the user has a poor connection.
So, we could upgrade our task to explicitly check for cancellation after the network request, using Task.checkCancellation()
. This is a static function call because it will always apply to whatever task it’s called inside, and it needs to be called using try
so that it can throw a CancellationError
if the task has been cancelled.
Here’s the new function:
func getAverageTemperature() async {
let fetchTask = Task { () -> Double in
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
try Task.checkCancellation()
let readings = try JSONDecoder().decode([Double].self, from: data)
let sum = readings.reduce(0, +)
return sum / Double(readings.count)
}
do {
let result = try await fetchTask.value
print("Average temperature: \(result)")
} catch {
print("Failed to get data.")
}
}
await getAverageTemperature()
Download this as an Xcode project
As you can see, it just takes one call to Task.checkCancellation()
to make sure our task isn’t wasting time calculating data that’s no longer needed.
If you want to handle cancellation yourself – if you need to clean up some resources or perform some other calculations, for example – then instead of calling Task.checkCancellation()
you should check the value of Task.isCancelled
instead. This is a simple Boolean that returns the current cancellation state, which you can then act on however you want.
To demonstrate this, we could rewrite our function a third time so that cancelling the task or failing to fetch data returns an average temperature of 0. This time we’re going to cancel the task ourselves as soon as it’s created, but because we’re always returning a default value we no longer need to handle errors when reading the task’s result:
func getAverageTemperature() async {
let fetchTask = Task { () -> Double in
let url = URL(string: "https://hws.dev/readings.json")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
if Task.isCancelled { return 0 }
let readings = try JSONDecoder().decode([Double].self, from: data)
let sum = readings.reduce(0, +)
return sum / Double(readings.count)
} catch {
return 0
}
}
fetchTask.cancel()
let result = await fetchTask.value
print("Average temperature: \(result)")
}
await getAverageTemperature()
Download this as an Xcode project
Now we have one implicit cancellation point with the data(from:)
call, and an explicit one with the check on Task.isCancelled
. If either one is triggered, the task will return 0 rather than throw an error.
Tip: You can use both Task.checkCancellation()
and Task.isCancelled
from both synchronous and asynchronous functions. Remember, async functions can call synchronous functions freely, so checking for cancellation can be just as important to avoid doing unnecessary work.
SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.