Chapters

Hide chapters

Concurrency by Tutorials

Second Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

9. Operation Dependencies
Written by Scott Grosch

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In this chapter, you’re going to learn about dependencies between operations. Making one operation dependent on another provides two specific benefits for the interactions between operations:

  1. Ensures that the dependent operation does not begin before the prerequisite operation has completed.
  2. Provides a clean way to pass data from the first operation to the second operation automatically.

Enabling dependencies between operations is one of the primary reasons you’ll find yourself choosing to use an Operation over GCD.

Modular design

Consider the tilt shift project you’ve been creating. You now have an operation that will download from the network, as well as an operation that will perform the tilt shift. You could instead create a single operation that performs both tasks, but that’s not a good architectural design.

Classes should ideally perform a single task, enabling reuse within and across projects. If you had built the networking code into the tilt shift operation directly, then it wouldn’t be usable for an already-bundled image. While you could add many initialization parameters specifying whether or not the image would be provided or downloaded from the network, that bloats the class. Not only does it increase the long-term maintenance of the class — imagine switching from URLSession to Alamofire — it also increases the number of test cases which have to be designed.

Specifying dependencies

Adding or removing a dependency requires just a single method call on the dependent operation. Consider a fictitious example in which you’d download an image, decrypt it and then run the resultant image through a tilt shift:

let networkOp = NetworkImageOperation()
let decryptOp = DecryptOperation()
let tiltShiftOp = TiltShiftOperation()

decryptOp.addDependency(op: networkOp)
tiltShiftOp.addDependency(op: decryptOp)
tiltShiftOp.removeDependency(op: decryptOp)

Avoiding the pyramid of doom

Dependencies have the added side effect of making the code much simpler to read. If you tried to write three chained operations together using GCD, you’d end up with a pyramid of doom. Consider the following pseudo-code for how you might have to represent the previous example with GCD:

let network = NetworkClass()
network.onDownloaded { raw in
  guard let raw = raw else { return }

  let decrypt = DecryptClass(raw)
  decrypt.onDecrypted { decrypted in
    guard let decrypted = decrypted else { return }
    
    let tilt = TiltShiftClass(decrypted)
    tilt.onTiltShifted { tilted in
      guard let tilted = tilted else { return }
    }
  }
}

Watch out for deadlock

In Chapter 5, “Concurrency Problems,” you learned about deadlock. Any time a task is dependent on another, you introduce the possibility of deadlock, if you aren’t careful. Picture in your mind — better yet graph out — the dependency chain. If the graph draws a straight line, then there’s no possibility of deadlock.

Passing data between operations

Now that you’ve got a way to safely make one operation depend on another, there has to be a way to pass data between them. Enter the power of protocols. The NetworkImageOperation has an output property called image. What about the case, though, in which the property is called something else?

Using protocols

Here’s what you’re really saying: “When this operation finishes, if everything went well, I will provide you with an image of type UIImage.”

import UIKit

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

Adding extensions

Open up NetworkImageOperation.swift and add this code to the very bottom of the file:

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

Searching for the protocol

The TiltShiftOperation needs a UIImage as its input. Instead of just requiring the inputImage property be set, it can now check whether any of its dependencies provides a UIImage as output.

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

guard let inputImage = inputImage ?? dependencyImage else {
  return
}
init(image: UIImage? = nil) {
  inputImage = image
  super.init()
}

Updating the table view controller

Head back over to TiltShiftTableViewController.swift and see if you can update it to download the image, tilt shift it, and then assign it to the table view cell.

let downloadOp = NetworkImageOperation(url: urls[indexPath.row])
let tiltShiftOp = TiltShiftOperation()
tiltShiftOp.addDependency(downloadOp)
tiltShiftOp.completionBlock = {
  DispatchQueue.main.async {
    guard let cell = tableView.cellForRow(at: indexPath) 
      as? PhotoCell else { return }

    cell.isLoading = false
    cell.display(image: tiltShiftOp.image)
  }
}
queue.addOperation(downloadOp)
queue.addOperation(tiltShiftOp)

Custom completion handler

The code as currently written is using the default completionBlock provided by the Operation class. You’re having to do a little extra work there to grab the image and dispatch back to the main queue. In a case like this, you may want to consider adding a custom completion block.

/// Callback which will be run *on the main thread* 
/// when the operation completes.
var onImageProcessed: ((UIImage?) -> Void)?
if let onImageProcessed = onImageProcessed {
  DispatchQueue.main.async { [weak self] in
    onImageProcessed(self?.outputImage)
  }
}
tiltShiftOp.onImageProcessed = { image in
  guard let cell = tableView.cellForRow(at: indexPath) 
    as? PhotoCell else {
    return
  }

  cell.isLoading = false
  cell.display(image: image)
}

Where to go from here?

Throughout this chapter, you’ve learned how to tie the start of one operation to the completion of another. Consider where, in your existing apps, you could implement operations and operation dependencies to better modularize your app and remove the Pyramid of Doom indentation that you’ve likely implemented.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now