10.
Canceling Operations
Written by Scott Grosch
With an operation, you have the capability of canceling a running operation as long as it’s written properly. This is very useful for long operations that can become irrelevant over time. For instance, the user might leave the screen or scroll away from a cell in a table view. There’s no sense in continuing to load data or make complex calculations if the user isn’t going to see the result.
The Magic of Cancel
Once you schedule an operation into an operation queue, you no longer have any control over it. The queue will schedule and manage the operation from then on. The one and only change you can make, once it’s been added to the queue, is to call the cancel method of Operation.
There’s nothing magical about how canceling an operation works. If you send a request to an operation to stop running, then the isCancelled computed property will return true. Nothing else happens automatically! At first, it may seem strange that iOS doesn’t stop the operation automatically, but it’s really not.
What does canceling an operation mean to the OS?
- Should the operation simply throw an exception?
- Is there cleanup that needs to take place?
- Can a running network call be canceled?
- Is there a message to send server-side to let something else know the task stopped?
- If the operation stops, will data be corrupted?
With just the small list of issues presented in the bullets above, you can see why setting a flag identifying that cancellation has been requested is all that’s possible automatically.
The default start implementation of Operation will first check to see whether the isCancelled flag is true, and exit immediately if it is.
Cancel and cancelAllOperations
The interface to cancel an operation is quite simple. If you just want to cancel a specific Operation, then you can call the cancel method. If, on the other hand, you wish to cancel all operations that are in an operation queue, then you should call the cancelAllOperations method defined on OperationQueue.
Updating AsyncOperation
In this chapter, you’ll update the app you’ve been working on so that an image’s operations are canceled when the user scrolls away from that image.
In Chapter 8, “Asynchronous Operations,” you built the AsyncOperation base class. If you recall, there was a note with that code warning you that the provided implementation wasn’t entirely complete. It‘s time to fix that!
The start method provided was written like so:
override func start() {
main()
state = .executing
}
If you‘re going to allow your operation to be cancelable — which you should always do unless you have a very good reason not to — then you need to check the isCancelled variable at appropriate locations. Open up the starter project from this chapter’s download materials and edit AsyncOperation.swift to update start:
override func start() {
if isCancelled {
state = .finished
return
}
main()
state = .executing
}
After the above changes, it’s now possible for your operation to be canceled before it’s started.
Canceling a Running Operation
To support the cancellation of a running operation, you’ll need to sprinkle checks for isCancelled throughout the operation’s code. Obviously, more complex operations are going to be able to check in more places than something simple like your NetworkImageOperation.
Open up NetworkImageOperation.swift and in main, add a new guard statement right after the defer:
guard !self.isCancelled else { return }
For the network download, there’s really no other location that you’d need to make the check. In fact, it’s questionable whether or not you’d really want to make the check at all.
You’ve already spent the time to download the image from the network. Is it better to cancel and return no image, or let the image get created? There’s no right or wrong answer. It’s simply an architectural decision that you’ll have to make based on the requirements of the project.
Next, add a way to cancel the network request while it’s in progress. First, add a new property to the class:
private var task: URLSessionDataTask?
This will hold the network task while it’s being run. Next, in main, assign the output of the dataTask call to task:
task = URLSession.shared.dataTask(with: url) {
Next, remove the call to resume at the end of that block. Instead, you’re going to call resume on the task by adding the following at the end of main:
task?.resume()
Finally, you need to override cancel to make sure the task is canceled. Add the following method to the class:
override func cancel() {
super.cancel()
task?.cancel()
}
Now, the downloading can be canceled at any time.
It’s time to allow canceling in TiltShiftOperation.swift. You’ll probably want to place two checks in the main method. Just before setting the fromRect variable, make the first check:
guard !isCancelled else { return }
Once you’ve applied the tilt shift and grabbed the output image, that’s a good point to stop before you then create the CGImage.
Next, just before setting outputImage, add the same check again.
guard !isCancelled else { return }
You’ve got a CGImage at this point but there’s no value in converting it to a UIImage if a cancellation was requested. Some would argue for a third check, right after creating the outputImage, but that leads to the same question posed during the network operation: You’ve already done all the work, do you really want to stop now?
Now that you have a way to cancel the operation, it’s time to hook this up to ImageView so that the operations are canceled when the user scrolls away.
Open up ImageView.swift and add the following property to the class:
@State private var operations: [Operation] = []
This is an array that will hold the operations for this specific image (both the downloading and tilt shifting). You need to store the operations because canceling is a method on the actual operation, so you need a way to grab it to cancel it.
In tiltShiftImage, just before adding the operations to the queue, store them:
operations = [downloadOp, tiltShiftOp]
Then, at the end of body, cancel the operations when the view disappears:
.onDisappear {
operations.forEach { $0.cancel() }
}
Build and run the app.
You probably won’t notice a big difference, but now when you quickly scroll through the table view the app won’t load and filter an image for each cell that quickly went past the screen. The downloads for the ones that went offscreen are canceled, saving the user’s network traffic and battery life and making your app run faster.
Where to Go From Here?
Having to cancel an operation doesn’t necessarily mean something negative happened. At times you cancel an operation because it’s simply no longer necessary.