Grand Central Dispatch Tutorial for Swift 5: Part 1/2

Learn all about multithreading, dispatch queues and concurrency in the first part of this Swift 5 tutorial on Grand Central Dispatch. By Fabrizio Brancati.

4.3 (6) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Understanding Queue Types

GCD provides three main types of queues:

  • Main queue: Runs on the main thread and is a serial queue.
  • Global queues: Concurrent queues shared by the whole system. Four such queues exist, each with different priorities: high, default, low and background. The background priority queue has the lowest priority and is throttled in any I/O activity to minimize negative system impact.
  • Custom queues: Queues you create that can be serial or concurrent. Requests in these queues end up in one of the global queues.

When sending tasks to the global concurrent queues, you don’t specify the priority directly. Instead, you specify a quality of service (QoS) class property. This indicates the task’s importance and guides GCD in determining the priority to give to the task.

The QoS classes are:

  • User-interactive: This represents tasks that must complete immediately to provide a nice user experience. Use it for UI updates, event handling and small workloads that require low latency. The total amount of work done in this class during the execution of your app should be small. This should run on the main thread.
  • User-initiated: The user initiates these asynchronous tasks from the UI. Use them when the user is waiting for immediate results and for tasks required to continue user interaction. They execute in the high-priority global queue.
  • Utility: This represents long-running tasks, typically with a user-visible progress indicator. Use it for computations, I/O, networking, continuous data feeds and similar tasks. This class is designed to be energy efficient. This gets mapped into the low-priority global queue.
  • Background: This represents tasks the user isn’t directly aware of. Use it for prefetching, maintenance and other tasks that don’t require user interaction and aren’t time-sensitive. This gets mapped into the background priority global queue.

Scheduling Synchronous vs. Asynchronous Functions

With GCD, you can dispatch a task either synchronously or asynchronously.

A synchronous function returns control to the caller after the task completes. You can schedule a unit of work synchronously by calling DispatchQueue.sync(execute:).

An asynchronous function returns immediately, ordering the task to start but not waiting for it to complete. Thus, an asynchronous function doesn’t block the current thread of execution from proceeding to the next function. You can schedule a unit of work asynchronously by calling DispatchQueue.async(execute:).

Managing Tasks

You’ve heard about tasks quite a bit by now. For the purposes of this tutorial, you can consider a task to be a closure. Closures are self-contained, callable blocks of code you can store and pass around.

Each task you submit to a DispatchQueue is a DispatchWorkItem. You can configure the behavior of a DispatchWorkItem, such as its QoS class or whether to spawn a new detached thread.

Handling Background Tasks

With all this pent-up GCD knowledge, it’s time for your first app improvement!

Head back to the app and add some photos from your photo library or use the Le Internet option to download a few. Tap a photo. Notice how long it takes for the photo detail view to show up. The lag is more pronounced when viewing large images on slower devices.

Overloading a view controller’s viewDidLoad() is easy to do, resulting in long waits before the view appears. It’s best to offload work to the background if it’s not absolutely essential at load time.

This sounds like a job for DispatchQueue‘s async!

Open PhotoDetailViewController.swift. Modify viewDidLoad() and replace these two lines:

guard let overlayImage = image.faceOverlayImageFrom() else {
  return
}
fadeInNewImage(overlayImage)

With the following code:

// 1
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  guard let overlayImage = self?.image?.faceOverlayImageFrom() else {
    return
  }

  // 2
  DispatchQueue.main.async { [weak self] in
    // 3
    self?.fadeInNewImage(overlayImage)
  }
}

Here’s what the code’s doing, step by step:

  1. You move the work to a background global queue and run the work in the closure asynchronously. This lets viewDidLoad() finish earlier on the main thread and makes the loading feel more snappy. Meanwhile, the face detection processing starts and will finish at some later time.
  2. At this point, the face detection processing is complete, and you’ve generated a new image. Since you want to use this new image to update your UIImageView, you add a new closure to the main queue. Remember — anything that modifies the UI must run on the main thread!
  3. Finally, you update the UI with fadeInNewImage(_:), which performs a fade-in transition of the new googly eyes image.

In two spots, you add [weak self] to capture a weak reference to self in each closure. If you aren’t familiar with capture lists, check out this tutorial on memory management.

Build and run the app. Download photos through the option Le Internet. Select a photo, and you’ll notice that the view controller loads up noticeably faster and adds the googly eyes after a short delay:

GooglyPuff app showing photo of woman with googly eyes added

This lends a nice before and after effect to the app as the googly eyes appear. Even if you’re trying to load a huge image, your app won’t hang as the view controller loads.

In general, you want to use async when you need to perform a network-based or CPU-intensive task in the background without blocking the current thread.

Here’s a quick guide on how and when to use the various queues with async:

  • Main queue: This is a common choice to update the UI after completing work in a task on a concurrent queue. To do this, you code one closure inside another. Targeting the main queue and calling async guarantees that this new task will execute sometime after the current method finishes.
  • Global queue: This is a common choice to perform non-UI work in the background.
  • Custom serial queue: A good choice when you want to perform background work serially and track it. This eliminates resource contention and race conditions since only one task executes at a time. Note that if you need the data from a method, you must declare another closure to retrieve it or consider using sync.

Delaying Task Execution

DispatchQueue allows you to delay task execution. Don’t use this to solve race conditions or other timing bugs through hacks like introducing delays. Instead, use this when you want a task to run at a specific time.

It would be a good idea to display a prompt to the user if there aren’t any photos. You should also consider how users’ eyes will navigate the home screen. If you display a prompt too quickly, they might miss it while their eyes linger on other parts of the view. A two-second delay should be enough to catch users’ attention and guide them.

Open PhotoCollectionViewController.swift and fill in the implementation for showOrHideNavPrompt():

// 1
let delayInSeconds = 2.0

// 2
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [weak self] in
  guard let self = self else {
    return
  }

  if !PhotoManager.shared.photos.isEmpty {
    self.navigationItem.prompt = nil
  } else {
    self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
  }

  // 3
  self.navigationController?.viewIfLoaded?.setNeedsLayout()
}

Here’s what’s going on above:

  1. You specify the amount of time to delay.
  2. You wait for the specified time, then asynchronously run the block, which updates the photo count and prompt.
  3. Force the navigation bar layout after setting the prompt to make sure it looks kosher.

showOrHideNavPrompt() executes in viewDidLoad() and any time your UICollectionView reloads.

Build and run the app. There should be a slight delay before you see a prompt displayed:

GooglyPuff app with instructions at top of screen

Note: You can ignore the Auto Layout messages in the Xcode console. They all come from iOS and don’t indicate a mistake on your part.

Why not use Timer? You could consider using it if you have repeated tasks that are easier to schedule with Timer. Here are two reasons to stick with dispatch queue’s asyncAfter():

  • One is readability. To use Timer, you have to define a method, then create the timer with a selector or invocation to the defined method. With DispatchQueue and asyncAfter(), you simply add a closure.
  • Timer is scheduled on run loops, so you’d also have to make sure you scheduled it on the correct run loop — and in some cases for the correct run loop modes. In this regard, working with dispatch queues is easier.