Cancelling Background Tasks

Running an asynchronous task in iOS is a relatively straight-forward exercise, especially with the new GCD syntax introduced in Swift 3. There are many reasons you might want to do this: processing information on a background queue, delaying operations until some time in the future, or perhaps ensuring that a UI operation is performed on the main thread (as all UI operations should be).

DispatchQueue(label: "Image Processing Queue").async {
  // Process image
  DispatchQueue.main.async {
    // Display image using the UI thread

Since these operations aren’t usually performed at the time they are declared, it is quite reasonable to assume that by the time the operation is run, it is possible that you will no longer need to run the operation. For example, processing large quantities of received data might not make sense if the user has just logged out. Or, maybe the user of a photo editing application decides that the image they’re currently editing isn’t the image they want after all, and they back out of the editing operation. Any background image editing work performed on the image after this point is moot, and so it would make sense to cancel any in progress image processing tasks.

A Manual Approach

DispatchQueue(label: "Data Refresh Queue").async(after: .now() + 2) {
  guard shouldContinueDataRefresh() else { return }
  // Refresh data

When first approaching this problem, a simple solution presents itself as a simple boolean check, performed before running the operation. This works fine, assuming the task does not capture and retain a large amount of memory. You might find yourself frequently doing this boolean check, and so defining an extension on DispatchQueue to make this task even easier might seem like a good idea. Don’t rush ahead and do this though - there’s a better way.

Cancelling Dispatch Tasks

While the above approach works fine, it’s not perfect. The task does exit instead of running, however the closure and any variables captured by it are held by the dispatch queue until the task is called and exits. In a contrived (although not unreasonable) example, a GCD closure may capture a large image for processing after a long network call. If the task is cancelled, the image will still exist in memory until the network call returns, and the boolean check runs. This isn’t ideal, especially if another similar operation is started in the meantime - we are then in danger of requiring too much memory, which increases the chances of a memory related crash occurring.

You could extend the before-mentioned solution so that cancellable task is stored as an optional closure, allowing the task to be nilled at cancel time. This way, objects can be deallocated in a more timely manner. There is no need to do this however - GCD provides a similar solution in the form of DispatchWorkItem. Dispatch tasks can be created with a work item instead of a closure, and a work item can be cancelled at any time, performing all the hard work for us. Nice, huh?

let workItem = DispatchWorkItem { doExcitingThings() }
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: workItem)

Suspending Dispatch Tasks

While we’re on the topic, we can take a small step back from cancelling an operation completely. DispatchQueue provides suspend() and resume() functions, which as they indicate, pause and resume all pending tasks declared on that queue.

let queue = DispatchQueue(label: "Network Queue")

If queued tasks would have executed during the suspension period, they will run as soon as the task is resumed. Also, it’s not a good idea to suspend a global queue (as other processes or tasks may be relying on it).

On a final note - once a dispatch task has started running, neither cancelling or suspending the task/queue/work item will stop it. Cancelling and suspending operations only effect tasks that haven’t yet been called. If you must do this, you will need to manually check the cancelled state at appropriate moments during the task’s execution, and exit the task if needed.