iOS Concurrency with GCD & Operations

Sep 12 2023 · Swift 5.8, macOS 13, iOS 16, Xcode 14.3

Part 3: Operations & OperationQueues

22. Cancel Operations

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 21. Challenge: Implement a Dependency Next episode: 23. Conclusion

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 22. Cancel Operations

You want to be able to cancel unnecessary operations to save your users’ data bandwidth, processing time and battery life. As soon as you add an operation to an operation queue, the only way to cancel it is to call its cancel method. But this only flips the values of its state properties. In particular, isCancelled becomes true. Your job is to code your operations so they check their isCancelled value, then do whatever is necessary: cancel tasks, notify servers, rollback database changes, throw exceptions, or generally clean up.

It’s easy to cancel an operation before it starts. Operation’s start method checks isCancelled and exits immediately if its value is true. When you subclassed Operation to create AsyncOperation, you overrode start to set state to .finished.

You can cancelAllOperations in an operation queue.

Continue with your final project from the previous video or open the starter project. Look at NetworkImageOperation. Its main method just creates a URLSession dataTask. If the app cancels this operation before the network request finishes, its cancel method should cancel the dataTask. To cancel a task, it needs a name, so add a new property at the top of NetworkImageOperation:

private var task: URLSessionDataTask?

And back to main() to store the dataTask in this task:

task = URLSession.shared.dataTask(with: url) { [weak self] ...   // just add task = at the start of line

And fix the error flag on .resume() by calling resume on task, on a new line:

task = URLSession.shared.dataTask(with: url) { [weak self]
...
}
task?.resume()  // this is the new line 

Now override cancel for this operation type:

override func cancel() {
  super.cancel()
  task?.cancel()
}

The app could cancel this operation after the dataTask finishes. Then the completion handler is the only place where you can check isCancelled. Back in main(), after the defer statement, add this line:

guard !self.isCancelled else { return }

So if the app cancels this operation after the download finishes, you don’t run the operation’s completion handler or save the downloaded data into the output image. Is this really the right thing to do? The app has already spent time and bandwidth to download the image. Maybe you should store this image in case the user scrolls back to its cell a few seconds later. There’s no right or wrong answer. This is an architectural decision that you’ll have to make based on the requirements of the project.

You also need to allow for canceling a tilt shift operation. In TiltShiftOperation, in main, add this line before setting fromRect:

guard !isCancelled else { return }

Once you’ve applied the tilt shift and stored its output image, you check to see if you should proceed with creating the CGImage. And check again, just before setting outputImage:

guard !isCancelled else { return }

You have a CGImage at this point, but there’s no value in converting it to a UIImage if there’s nowhere to display it. There’s no task to stop, so you don’t need to override cancel, like you did with NetworkImageOperation.

You’ve implemented cancel in your operations, but what about canceling operations for an Image that scrolls offscreen? In ImageStore, add this property:

private var operations: [[Operation]] = [[]]

This is a 2-dimensional array that will hold the download and tilt shift operations for a specific image. 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. Just before the closing brace of createImagesArray(), initialize it with the correct number of empty arrays:

operations = Array<[Operation]>(repeating: [], count: urls.count)

You’ll need to cancel operations from ContentView as well as downloadImageOp, so add this helper method:

func cancelOperations(index: Int) {
  guard index < operations.count else { return }
  for operation in operations[index] {
    operation.cancel()
  }
}

To store the operations, add the following lines at the end of downloadImageOp, :

cancelOperations(index: index)
guard index < operations.count else { return }
operations[index] = [tiltShiftOp, downloadOp]

If an operation for this index already exists, cancel it, then store the new operations for that index. Now, in ContentView, add the following modifier to Image:

.onDisappear {
  store.cancelOperations(index: image.id)
}

The system calls onDisappear when a view goes offscreen. At that point, you cancel the operations for that Image, so only visible images use the phone’s resources.

To see when this actually happens, add a breakpoint on this line and edit it: Add an action to log @image.id@ and check the continue option.

Build and run the app. You might not notice a big difference, but now when you quickly scroll down, the app won’t load and filter an image that quickly went off the screen. You can see the list of image IDs that should be cancelled. It cancels the downloads for images that go offscreen, saving the user’s network traffic and battery life, and making your app run faster.