iOS Concurrency with GCD & Operations

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

Part 3: Operations & OperationQueues

20. Dependencies

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: 19. Challenge: Download Images in OperationQueue Next episode: 21. Challenge: Implement a Dependency

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: 20. Dependencies

In Part 1, you set up a simple dependency to update the UI after a background task finishes, using the notify method of DispatchWorkItem.

backgroundWorkItem.notify(
  queue: DispatchQueue.global(),
  execute: updateUIWorkItem)
userQueue.async(execute: backgroundWorkItem)

The syntax isn’t very intuitive.

Operation queues let you easily set up dependencies between operations.

let downloadOp = NetworkImageOperation(url: urls[indexPath.row])
let tiltShiftOp = TiltShiftOperation()
tiltShiftOp.addDependency(downloadOp)

You just create your operations, then add dependencies. In this case, tiltShiftOp depends on downloadOp: The operation queue doesn’t set tiltShiftOp’s state to ready until downloadOp is finished.

Operation dependencies is one place where you could create a deadlock. You must be careful not to create a dependency cycle that prevents the operations from finishing. The problem can be harder to find if you add dependencies to operations on other operation queues, which is a perfectly valid thing to do.

Suppose we have these operations on this green operation queue.

And there are dependencies: The first operation must finish before the second operation can start, and so on.

And here’s an orange operation queue, with operations.

Suppose an operation on the first queue depends on an operation on the second queue: That’s fine.

But if this orange operation depends on that last green operation, that’s deadlock! The middle green operation needs the result of the middle orange operation, but that orange operation needs the result of the last green operation, which needs the result of the middle green operation! Deadlock!

There’s no silver bullet solution. You just have to be vigilant and draw diagrams of your dependency graph, if it’s more complicated than a chain.

Open the FilterImages project in the starter folder.

This project downloads images into a scroll view. ImageStore uses NetworkImageOperation, which is a subclass of AsyncOperation. The project also has a TiltShiftOperation that should run on each downloaded image. You need to set up a dependency between these two operations so an operation queue can run them in the correct order.

First, you need a way to pass the downloaded image to the tilt-shift operation. This general-purpose ImageDataProvider protocol enables you to pass image data from NetworkImageOperation to TiltShiftOperation.

import UIKit

protocol ImageDataProvider {
  var image: UIImage? { get }
}

This protocol renames any UIImage from a provider operation as image so the consumer operation doesn’t have to worry about the provider operation’s internal name for its output image.

NetworkImageOperation conforms to ImageDataProvider:

extension NetworkImageOperation: ImageDataProvider {}

It doesn’t have to do anything special because its output image is already named image.

TiltShiftOperation also conforms to ImageDataProvider:

extension TiltShiftOperation: ImageDataProvider {
  var image: UIImage? { return outputImage }
}

When it’s producing an output image, TiltShiftOperation needs to define the protocol’s image property because its output image isn’t named image. When it might be consuming another operation’s output image, it needs to check if there’s one in its dependency chain.

So in TiltShiftOperation, in main(), change guard let inputImage... to this:

let dependencyImage = dependencies
  .compactMap { ($0 as? ImageDataProvider)?.image }
  .first
guard let inputImage = inputImage ?? dependencyImage 
else { return }

You try to unwrap either the input image directly provided to the operation or search the dependency chain for something that provides an image, making sure you get a non-nil image. If neither of those work, simply return without performing any work.

One last thing: Change init to this:

init(image: UIImage? = nil) {

Because you’re now checking the dependency chain for an image, you need a way to initialize a TiltShiftOperation without providing an input image. The simplest way to handle no input is to set a default nil value for image in init.

In the next video, you’ll update ImageStore with an operation queue dependency to download the image, filter it, then append it to the published images array.