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

8. Asynchronous Operations
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.

Up to this point, your operations have been synchronous, which works very well with the Operation class’ state machine. When the operation transitions to the isReady state, the system knows that it can start searching for an available thread.

Once the scheduler has found a thread on which to run the operation, the operation will transition to the isExecuting state. At that point, your code executes and completes, and the state then becomes isFinished.

How would that work with an asynchronous operation, though? When the main method of the operation executes, it will kick off your asynchronous task, and then main exits. The state of the operation can’t switch to isFinished at that point because the asynchronous method likely has yet to complete.

Asynchronous operations

It’s possible to wrap an asynchronous method into an operation, but it takes a bit more work on your part. You’ll need to manage the state changes manually as the operation can’t determine automatically when the task has finished executing. To make matters worse, the state properties are all read-only!

If you’re ready to throw in the towel, don’t worry. Managing the states is actually quite simple to accomplish. In fact, you will now create a base class that all asynchronous operations you use will inherit from so you never have to do it again. No, we don’t know why this class isn’t part of the framework.

AsyncOperation

In the download materials for this chapter, open AsyncAddOperation.playground in the starter folder. You can ignore the compilation error as you’ll resolve it in a moment when you add some code.

State tracking

Since the state of an operation is read-only, you’ll first want to give yourself a way to track changes in a read-write manner, so create a State enumeration at the top of the file:

extension AsyncOperation {
  enum State: String {
    case ready, executing, finished

    fileprivate var keyPath: String {
      return "is\(rawValue.capitalized)"
    }
  }
}
var state = State.ready {
  willSet {
    willChangeValue(forKey: newValue.keyPath)
    willChangeValue(forKey: state.keyPath)
  }
  didSet {
    didChangeValue(forKey: oldValue.keyPath)
    didChangeValue(forKey: state.keyPath)
  }
}

Base properties

Now that you have a way to track state changes and signal that a change was in fact performed, you’ll need to override the base class’ instances of those methods to use your state instead. Add these three overrides to the class, below // Override properties:

override var isReady: Bool {
  return super.isReady && state == .ready
}

override var isExecuting: Bool {
  return state == .executing
}

override var isFinished: Bool {
  return state == .finished
}
override var isAsynchronous: Bool {
  return true
}

Starting the operation

All that’s left to do is implement the start method. Whether you manually execute an operation or let the operation queue do it for you, the start method is what gets called first, and then it is responsible for calling main. Add the following code immediately below // Override start:

override func start() {
  main()
  state = .executing
}

Math is fun!

Take a look at the rest of the code that was provided for you in the playground. Nothing should be new to you, as long as you already worked through the chapters on GCD in this book. If you run the playground with the console displayed ( + + Y), you’ll see the numbers have been added together properly.

Networked TiltShift

Time to get back to your image filtering. So far, you’ve used a hardcoded list of images. Wouldn’t it be great if the images came from the network instead? Performing a network operation is simply an asynchronous task! Now that you have a way to turn an asynchronous task into an operation, let’s get to it!

NetworkImageOperation

Open Concurrency.xcodeproj and create a new Swift file named NetworkImageOperation.swift. You’re going to make this do more than specifically needed for the project but this way you’ll have a reusable component for any other project you work on.

import UIKit

typealias ImageOperationCompletion = ((Data?, URLResponse?, Error?) -> Void)?

final class NetworkImageOperation: AsyncOperation {
  var image: UIImage?

  private let url: URL
  private let completion: ImageOperationCompletion
  
}
init(
  url: URL, 
  completion: ImageOperationCompletion = nil) {
  
  self.url = url
  self.completion = completion

  super.init()
}

convenience init?(
  string: String, 
  completion: ImageOperationCompletion = nil) {
  
  guard let url = URL(string: string) else { return nil }
  self.init(url: url, completion: completion)
}
override func main() {
  URLSession.shared.dataTask(with: url) { 
    [weak self] data, response, error in
    
  }.resume()
}
guard let self = self else { return }

defer { self.state = .finished }

if let completion = self.completion {
  completion(data, response, error)
  return
}

guard error == nil, let data = data else { return }

self.image = UIImage(data: data)

Using NetworkImageFilter

Head back over to TiltShiftTableViewController.swift. In order to get the list of URLs that you’ll be able to display, add the following code to the beginning of your view controller:

private var urls: [URL] = []

override func viewDidLoad() {
  super.viewDidLoad()

  guard let plist = Bundle.main.url(forResource: "Photos",
                                    withExtension: "plist"),
        let contents = try? Data(contentsOf: plist),
        let serial = try? PropertyListSerialization.propertyList(
    	                  from: contents,
    	                  format: nil),
        let serialUrls = serial as? [String] else {
    print("Something went horribly wrong!")
    return
  }

  urls = serialUrls.compactMap(URL.init)
}
let image = UIImage(named: "\(indexPath.row).png")!
let op = TiltShiftOperation(image: image)
let op = NetworkImageOperation(url: urls[indexPath.row])
cell.display(image: op.outputImage)
cell.display(image: op.image)

Where to go from here?

You’ve now got reusable components for both network image downloads as well as tilt shift filtering. Wouldn’t it be nice to be able to use both at once? The next chapter will show you how to link those two together and finally provide the “aha” moment as to why you’re using operations instead of sticking with Grand Central Dispatch.

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