FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

How to cancel a Task

Paul Hudson    @twostraws   

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:

  1. You can explicitly cancel a task by calling its cancel() method.
  2. Any task can check Task.isCancelled to determine whether the task has been cancelled or not.
  3. You can call the Task.checkCancellation() method, which will throw a CancellationError if the task has been cancelled or do nothing otherwise.
  4. Some parts of Foundation automatically check for task cancellation and will throw their own cancellation error even without your input.
  5. If you’re using 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.
  6. If the task is part of a group and any part of the group throws an error, the other tasks will be cancelled and awaited.
  7. If you have started a task using SwiftUI’s 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.")
    }
}

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.")
    }
}

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)")
}

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.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for iOS devs who want to become complete senior developers — from October 18th to 24th. Learn how to apply iOS app architecture patterns through a series of lectures and practical coding sessions.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.