Chapters

Hide chapters

Concurrency by Tutorials

Third Edition · iOS 16 · Swift 5.7 · Xcode 14

8. Asynchronous Operations
Written by Scott Grosch

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.

isReady isExecuting isFinished S a t chedulersearchesfor anvailablehread Y c e c ourodexecutes toompletion H - G ousekeeping etting the operation ready

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.

isReady isExecuting isFinished Asynchronous Task

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)"
    }
  }
}

Back in Chapter 6, “Operations,” I mentioned that the Operation class uses KVO notifications. When the isExecuting state changes, for example, a KVO notification will be sent. The states you set yourself don’t start with the ‘is’ prefix though, and, per the Swift style guide, enum entries should be lowercased.

The keyPath computed property you wrote is what helps support the aforementioned KVO notifications. When you ask for the keyPath of your current State, it will capitalize the first letter of the state’s value and prefix with the text is. Thus, when your state is set to executing, the keyPath will return isExecuting, which matches the property on the Operation base class.

Notice the fileprivate modifier. You thought the need to ever use that monstrosity went away with the fixes in Swift 4, didn’t you? This is a case in which it’s still helpful. The keyPath needs to be available to this entire file but not externally. If you just made it private, then it wouldn’t be visible outside of the enum itself.

Be aware that the scoping is now the entire file, so you’ll want to make sure the class lives in a file of its own for production code.

Now that you have the type of your state created, you’ll need a variable to hold the state. Because you need to send the appropriate KVO notifications when you change the value, you’ll attach property observers to the property. Add the following code to AsyncOperation:

var state = State.ready {
  willSet {
    willChangeValue(forKey: newValue.keyPath)
    willChangeValue(forKey: state.keyPath)
  }
  didSet {
    didChangeValue(forKey: oldValue.keyPath)
    didChangeValue(forKey: state.keyPath)
  }
}

By default, your state is ready. When you change the value of state, you’ll actually end up sending four KVO notifications! Take a minute to see if you can understand what is happening and why there are four entries there instead of just two.

Consider the case in which the state is currently ready and you are updating to executing. isReady will become false, while isExecuting will become true. These four KVO notifications will be sent:

  1. Will change for isReady.
  2. Will change for isExecuting.
  3. Did change for isReady.
  4. Did change for isExecuting.

The Operation base class needs to know that both the isExecuting and isReady properties are changing.

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:

override var isReady: Bool {
  super.isReady && state == .ready
}
override var isExecuting: Bool { state == .executing }
override var isFinished: Bool { state == .finished }

Note: It’s critical that you include a check to the base class’ isReady as your code isn’t aware of everything that goes on while the scheduler determines whether or not it is ready to find your operation a thread to use.

The final property to override is simply to specify that you are in fact using an asynchronous operation. Add the following piece of code:

override var isAsynchronous: Bool { 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 to the end of the class:

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

Note: Notice this code doesn’t invoke super.start(). The official documentation clearly mentions that you must not call super at any time when overriding start.

Those two lines probably look backwards to you. They’re really not. Because you’re performing an asynchronous task, the main method is going to almost immediately return, thus you have to manually put the state back to .executing so the operation knows it is still in progress.

Note: There’s a piece missing from the above code. Chapter 10, “Canceling Operations,” will talk about cancelable operations, and that code needs to be in any start method. It’s left out here to avoid confusion.

If Swift had a concept of an abstract class, which couldn’t be directly instantiated, you would mark this class as abstract. In other words, never directly use this class. You should always subclass AsyncOperation!

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 (Shift-Command-Y), you’ll see the numbers have been added together properly.

The key detail in the AsyncSumOperation to pay attention to is that you must manually set the state of the operation to .finished when the asynchronous task completes. If you forget to change the state then the operation will never be marked as complete, and you’ll have what is known as an infinite loop.

Note: Be sure to always set the state to .finished when your asynchronous method completes.

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!

The starter project for this chapter, located in the starter/TiltShift folder, continues the project you’ve been building but includes two new files, as well as a couple other small changes.

NetworkImageOperation

Create a new Swift file named NetworkImageOperation. 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.

The general requirements for the operation are as follows:

  1. Should take either a String representing a URL or an actual URL.
  2. Should download the data at the specified URL.
  3. If a URLSession-type completion handler is provided, use that instead of decoding.
  4. If successful, there’s no completion handler and it’s an image; should set an optional UIImage value.

The first two requirements should be pretty obvious. The third and fourth are to give maximum flexibility to the caller. In some cases, as with this project, you just want to grab the decoded UIImage and be done. Other projects though may require custom processing. For example, you may care about what the specific error is, whether the HTTP header has a valid Content-Type header, etc.

Begin by subclassing AsyncOperation and declaring the variables that the class will need. Create a new Swift file called NetworkImageOperation.swift and add the following code:

import UIKit

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

final class NetworkImageOperation: AsyncOperation {
  var image: UIImage?

  private let url: URL
  private let completion: ImageOperationCompletion

}

The completion signature is the same signature used by URLSession methods, just turned into an optional. To meet requirements 1 and 2, you’ll need to define appropriate initializers. Add the following code inside your new class:

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)
}

It’s probably not the normal case that someone will want to explicitly handle the HTTP return data themselves, so it makes sense to default the completion handler to nil. Passing an actual URL is the “designated initializer” and, thus, you’re declaring a convenience initializer that takes a string instead. Note how that constructor is itself an optional. If you pass a string that isn’t able to be converted to a URL, then the constructor will return nil.

Housekeeping, check! Now for the actual work of the operation. Override main and start a new URLSession task, as you would usually use URLSession. Add below your initializers:

override func main() {
  URLSession.shared.dataTask(with: url) {
    [weak self] data, response, error in

  }.resume()
}

Since this is an asynchronous operation, it’s always possible that the object will be removed before the data download happens, so you need to be sure you can retain self, thus using weak capture group.

Now, it’s time to handle the completed task. Add the following code inside the completion closure:

guard let 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 defer ensures that the operation is eventually marked as complete. There are already three possible exit paths in the current method, plus you never know what changes you might make in the future. Putting the defer statement right up front makes it bullet proof.

Meeting requirements 3 and 4 is as simple as checking for whether or not the caller has provided a completion handler. If they have provided a completion handler, you just pass the processing responsibility to it and exit. If not, then you can decode the image.

Note that there’s no need to throw exceptions or return any type of error condition. If anything fails, then the image property will be nil, and the caller will know something went wrong.

Using NetworkImageOperation

Head back over to TableView.swift. In order to get the list of URLs that you’ll be able to display, the starter project included a loadPhotoUrls method for you.

That’s the standard Swift mechanism for reading the contents of a .plist file and converting the strings to actual URL objects. You might not have seen compactMap before. It works just like map does, just for an array of Optional values. It excludes any nil items and returns only unwrapped, non-optional values. In this case, that means the urls array will just contain valid URL objects.

Notice that the body now calls ImageView by passing in a URL instead of a row number.

Edit ImageView.swift and replace the TiltShiftOperation call with NetworkImageOperation:

let op = NetworkImageOperation(url: url)

The TiltShiftOperation class used outputImage as the result variable, whereas NetworkImageOperation uses just image, so replace the following line:

if let outputImage = op.outputImage {

With:

if let outputImage = op.image {

At this point, you can build and run the app and scroll through a large list of interesting images.

The scrolling will be nice and smooth as the UI isn’t hung at any point during the network operation.

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.
© 2026 Kodeco Inc.