Dispatch queues are an integral part of Grand Central Dispatch. In this episode, we cover the fundamentals of dispatch queues. Let's start with an example.

An Example

Download the starter project of this episode if you'd like to follow along. Open Main.storyboard. The storyboard contains a single view controller that displays four image views. Each image view has an activity indicator view at its center. The activity indicator view is visible as long as the image view doesn't have an image to display.

Main Storyboard

Open ViewController.swift. The view controller keeps a reference to the image views through its imageViews property.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var imageViews: [UIImageView]!

    ...

}

The images property of the ViewController class is of type [URL]. Each element of the array points to an image. Some of the images are included in the application's bundle while others are located on a remote server.

private lazy var images: [URL] = [
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")!,
    Bundle.main.url(forResource: "2", withExtension: "jpg")!,
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/3.jpg")!,
    Bundle.main.url(forResource: "4", withExtension: "jpg")!
]

The ViewController class also implements a private helper method to populate an image view with the contents of a URL. The loadImage(with:for:) method accepts a URL instance and a UIImageView instance.

The application creates a Data instance using the URL that is passed to the loadImage(with:for:) method and uses it to create a UIImage instance. The UIImage instance is assigned to the image property of the image view.

private func loadImage(with url: URL, for imageView: UIImageView) {
    // Load Data
    guard let data = try? Data(contentsOf: url) else {
        return
    }

    // Create Image
    let image = UIImage(data: data)

    // Update Image View
    imageView.image = image
}

The final piece of the puzzle is the implementation of the viewDidLoad() method. The application iterates through the collection of image views and uses the array of images to load an image for each image view. Notice that I've added a print statement before and after the for loop. You'll find out why that's useful when we run the application.

override func viewDidLoad() {
    super.viewDidLoad()

    print("Start")

    for (index, imageView) in imageViews.enumerated() {
        // Fetch URL
        let url = images[index]

        // Populate Image View
        loadImage(with: url, for: imageView)
    }

    print("Finish")
}

Build and run the application in the simulator or on a physical device. Observe the behavior of the application. What do you see? It takes a few moments before you see the images appear. Can you guess why that is?

Blocking the Main Thread

The viewDidLoad() method is always invoked on the main thread, which means the application iterates through the collection of image views on the main thread. This implies that the application loads the data for the images on the main thread. That operation blocks the main thread and, as a result, the application is unable to update its user interface as long as it's populating the image views with images.

Remember what I said in What Is the Main Thread, the user interface of an application is updated on the main thread. If the main thread is blocked, then the user experiences that as the application being unresponsive. I recommend watching that episode if you'd like to learn more about the main thread. The idea is simple, though. The application updates the user interface multiple times per second. If the main thread is blocked, then it cannot update the user interface or respond to user interaction.

Blocking the Main Thread

The output in the console confirms this. The print statements indicate that it takes more than a second for the application to load the images and populate the image views. While one second may not seem like a long time, it is unacceptable if your goal is building a fast and responsive application.

Start 2018-11-18 09:00:01 +0000
Finish 2018-11-18 09:00:02 +0000

How long it takes isn't important. Operations that run for a non-trivial amount of time should never take place on the main thread. Let me show you how we can resolve this issue using Grand Central Dispatch.

Dispatching Work to a Dispatch Queue

As I mentioned in the previous episode, a dispatch queue is responsible for managing the execution of blocks of work. Let's create and use a dispatch queue to make sure the images are not loaded on the main thread.

Open ViewController.swift and define a private, constant property, dispatchQueue. We instantiate and assign a DispatchQueue instance to the dispatchQueue property. As a minimum, we need to assign a label to the dispatch queue. The label of a dispatch queue is helpful for debugging. I show you this in a moment.

private let dispatchQueue = DispatchQueue(label: "My Dispatch Queue")

We can use the dispatch queue in the viewDidLoad() method. We invoke the async(execute:) method on the dispatch queue, passing it a closure. Remember from the previous episode that the closure accepts no arguments and returns nothing. In Swift parlance, nothing equals Void or an empty tuple.

override func viewDidLoad() {
    super.viewDidLoad()

    print("Start \(Date())")

    for (index, imageView) in imageViews.enumerated() {
        // Fetch URL
        let url = images[index]

        dispatchQueue.async {

        }
    }

    print("Finish \(Date())")
}

We invoke the loadImage(with:for:) method in the closure we pass to the async(execute:) method. Notice that we weakly capture self, the ViewController instance, to avoid a retain cycle.

override func viewDidLoad() {
    super.viewDidLoad()

    print("Start \(Date())")

    for (index, imageView) in imageViews.enumerated() {
        // Fetch URL
        let url = images[index]

        dispatchQueue.async { [weak self] in
            // Populate Image View
            self?.loadImage(with: url, for: imageView)
        }
    }

    print("Finish \(Date())")
}

Build and run the application to see the result. We immediately see the activity indicator views of the image views. This shows that the main thread of the application is no longer being blocked by the loading of the images. That's a good start.

Something isn't right, though. It takes a long time before the image views display the images and the output in Xcode's console shows us that something is wrong.

Main Thread Checker: UI API called on a background thread: -[UIImageView setImage:]
PID: 18635, TID: 8180890, Thread name: (none), Queue name: Dispatch Queue, QoS: 0
Backtrace:
4   Tasks                               0x0000000102c135e4 $S5Tasks14ViewControllerC9loadImage33_7C6B6637D8CD23E2730CA9021BA9E096LL4with3fory10Foundation3URLV_So07UIImageB0CtF + 360
5   Tasks                               0x0000000102c132a0 $S5Tasks14ViewControllerC11viewDidLoadyyFyycfU_ + 216
6   Tasks                               0x0000000102c13360 $SIeg_IeyB_TR + 52
7   libdispatch.dylib                   0x0000000103a9f840 _dispatch_call_block_and_release + 24
8   libdispatch.dylib                   0x0000000103aa0de4 _dispatch_client_callout + 16
9   libdispatch.dylib                   0x0000000103aa8e88 _dispatch_lane_serial_drain + 720
10  libdispatch.dylib                   0x0000000103aa9b7c _dispatch_lane_invoke + 460
11  libdispatch.dylib                   0x0000000103ab3c18 _dispatch_workloop_worker_thread + 1220
12  libsystem_pthread.dylib             0x00000002090e20f0 _pthread_wqthread + 312
13  libsystem_pthread.dylib             0x00000002090e4d00 start_wqthread + 4

The debugger warns us that the application updates the user interface of the application on a thread that isn't the main thread. Remember that the user interface should always be updated from the main thread.

The problem is located in the loadImage(with:for:) method. Grand Central Dispatch dispatches the loading of the images to a background thread. The Data instance is used to create a UIImage instance, which is assigned to the image property of the image view. This takes place on a background thread, not the main thread. In other words, the image view is updated on a thread that isn't the main thread. That is a red flag and leads to undefined or unexpected behavior.

The solution is surprisingly simple thanks to Grand Central Dispatch. We need to dispatch the updating of the image view to the main thread. Grand Central Dispatch makes this straightforward. We ask the DispatchQueue class for the dispatch queue that is associated with the main thread. Any work that is submitted to the main dispatch queue is guaranteed to be executed on the main thread. We invoke the async(execute:) method and, in the closure that is submitted to the main dispatch queue, the image view is updated.

private func loadImage(with url: URL, for imageView: UIImageView) {
    // Load Data
    guard let data = try? Data(contentsOf: url) else {
        return
    }

    // Create Image
    let image = UIImage(data: data)

    DispatchQueue.main.async {
        // Update Image View
        imageView.image = image
    }
}

Performing work in the background and updating the user interface on the main thread after the work is completed is a common pattern. Grand Central Dispatch makes that easy.

Build and run the application one more time to see the result. The debugger should no longer output warnings in Xcode's console.

Debugging Grand Central Dispatch

It's important that you understand how to use Grand Central Dispatch, but it's equally important to know how to debug issues that are related to Grand Central Dispatch. Let's explore the current implementation of the application in more detail.

Add two breakpoints to the loadImage(with:for:) method. Add the first breakpoint to the guard statement and add the second breakpoint to the closure we submit to the main dispatch queue.

Adding Breakpoints

Build and run the application. Open the Debug Navigator on the left and wait for the first breakpoint to be hit. The Debug Navigator shows us a lot of interesting information about the internal workings of Grand Central Dispatch.

Debugging Grand Central Dispatch

The Debug Navigator shows us the thread on which the application loads the data for the image. It isn't the main thread. That isn't surprising. The Debug Navigator also shows us the dispatch queue that dispatched the block of work to the background thread. The label of the dispatch queue is equal to My Dispatch Queue, which is the label we assigned to the dispatch queue of the view controller.

The Debug Navigator also displays the type of dispatch queue in parentheses. The dispatch queue of the view controller is a serial dispatch queue. We discuss serial and concurrent dispatch queues in more detail in the next episode.

Continue the execution of the application by clicking the Continue button in the Debug Bar at the bottom. The second breakpoint is hit. The Debug Navigator confirms that the image view is updated on the main thread. The dispatch queue that dispatched the block of work to the main thread is the main dispatch queue of the application. It has a label that is equal to com.apple.main-thread and it's also a serial dispatch queue.

Debugging Grand Central Dispatch

The stack trace also shows that the closure that was submitted to the main dispatch queue was enqueued or submitted from the dispatch queue with label My Dispatch Queue. This can be useful for debugging because Grand Central Dispatch can sometimes result in complex stack traces.

The Debug Navigator by default displays the processes by thread, but it's also possible to show the processes by dispatch queue. Click the button in the top right of the Debug Navigator and select View Process by Queue to show the processes by dispatch queue.

Viewing Processes by Queue

Exploring the processes by dispatch queue can be useful if you're debugging a threading problem. The Debug Navigator shows us that the main dispatch queue is executing a block and it displays the number of blocks that are pending, that is, waiting to be executed.

Viewing Processes by Queue

The same is true for the dispatch queue of the view controller. One block is being executed and several blocks are waiting for execution. This makes sense since the view controller submitted four blocks to the dispatch queue, one for each image view.

What's Next?

As I mentioned earlier in this series, it's important that you understand how Grand Central Dispatch works. Knowing how to use it isn't sufficient. This also means that you need to understand how Grand Central Dispatch works under the hood. Being able to explore the stack trace in the Debug Navigator is something every developer should be able to do.

In the next episode, we explore the different types of dispatch queues. We find out what a serial dispatch queue is and how it's different from a concurrent dispatch queue.