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

Learn all about multithreading, dispatch queues, and concurrency in the second part of this Swift 5 tutorial on Grand Central Dispatch. By David Piper.

Leave a rating/review
Download materials
Save for later
Share
Update note: David Piper updated this tutorial for iOS 15, Swift 5.5 and Xcode 13. Christine Abernathy wrote the original.

Welcome to the second and final part of this Grand Central Dispatch tutorial series!

In the first part of this series, you learned about concurrency, threading and how GCD works. You made a singleton thread safe for reading and writing. To do so, you used a combination of dispatch barriers and synchronous dispatch queues. You also enhanced the app’s user experience by using dispatch queues to delay the display of a prompt. This asynchronously offloaded CPU-intensive work when instantiating a view controller.

In this second Grand Central Dispatch tutorial, you’ll work with the same GooglyPuff app you know and love from the first part. You’ll delve into advanced GCD concepts, including:

  • Dispatch groups
  • Canceling dispatch blocks
  • Asynchronous testing techniques
  • Dispatch sources

It’s time to explore some more GCD!

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Pick up where you left off with the sample project from Part One if you followed along.

Run the app, tap + and select Le Internet to add internet photos. You may notice that a download completion alert message pops up well before the images have finished downloading:

Although the images didn't finish downloading, the Download Completed alert is already presented.

That’s the first thing you’ll work to fix.

Using Dispatch Groups

Open PhotoManager.swift and check out downloadPhotos(withCompletion:):

func downloadPhotos(
  withCompletion completion: BatchPhotoDownloadingCompletionClosure?
) {
  var storedError: NSError?
  for address in [
    PhotoURLString.overlyAttachedGirlfriend,
    PhotoURLString.successKid,
    PhotoURLString.lotsOfFaces
  ] {
    guard let url = URL(string: address) else { return }
    let photo = DownloadPhoto(url: url) { _, error in
      storedError = error
    }
    PhotoManager.shared.addPhoto(photo)
  }

  completion?(storedError)
}

The completion closure passed into the method fires the alert. You call this after the for loop, which downloads the photos. But, it incorrectly assumes the downloads are complete before you call the closure.

Kick off photo downloads by calling DownloadPhoto(url:). This call returns immediately, but the actual download happens asynchronously. Thus, when completion runs, there’s no guarantee that all the downloads have finished.

What you want is for downloadPhotos(withCompletion:) to call its completion closure after all the photo download tasks are complete. How can you monitor these concurrent asynchronous events to achieve this? With the current method, you don’t know when the tasks are complete and they can finish in any order.

Good news! This is exactly why dispatch groups exist. With dispatch groups, you can group together multiple tasks. Then, you can either wait for them to complete or receive a notification once they finish. Tasks can be asynchronous or synchronous and can even run on different queues.

DispatchGroup manages dispatch groups. You’ll first look at its wait method. This blocks your current thread until all the group’s enqueued tasks finish.

In PhotoManager.swift, replace the code in downloadPhotos(withCompletion:) with the following:

// 1
DispatchQueue.global(qos: .userInitiated).async {
  var storedError: NSError?

  // 2
  let downloadGroup = DispatchGroup()
  for address in [
    PhotoURLString.overlyAttachedGirlfriend,
    PhotoURLString.successKid,
    PhotoURLString.lotsOfFaces
  ] {
    guard let url = URL(string: address) else { return }

    // 3
    downloadGroup.enter()
    let photo = DownloadPhoto(url: url) { _, error in
      storedError = error

      // 4
      downloadGroup.leave()
    }   
    PhotoManager.shared.addPhoto(photo)
  }   

  // 5      
  downloadGroup.wait()

  // 6
  DispatchQueue.main.async {
    completion?(storedError)
  }   
}

Here’s what the code is doing step-by-step:

  1. The synchronous wait method blocks the current thread. Thus, you need to use async to place the entire method into a background queue. This ensures you don’t block the main thread.
  2. Create a new dispatch group.
  3. Call enter() to manually notify the group that a task has started. You must balance out the number of enter() calls with the number of leave() calls, or your app will crash.
  4. Notify the group that this work is done.
  5. Call wait() to block the current thread while waiting for tasks’ completion. This waits forever — which is fine because the photos creation task always completes. You can use wait(timeout:) to specify a timeout and bail out on waiting after a specified time.
  6. At this point, you know that all image tasks have either completed or timed out. You then make a call back to the main queue to run your completion closure.

Build and run the app. Download photos through the Le Internet option and verify that the alert doesn’t show up until all the images have downloaded.

Two screenshots of the app side by side. The first shows three images, the first two are already downloaded, the last one is still downloading. The second screenshot shows that all three images are downloaded and the Download Completed alert is presented.

If you’re running on iOS Simulator, use the Network Link Conditioner included in the Advanced Tools for Xcode to change your network speed. This is a good tool to have in your arsenal. It forces you to be conscious of what happens to your apps when connection speeds are less than optimal.

Note: The network activities may occur too quickly to discern when the completion closure should be called. If you’re running the app on a device, make sure this really works. You need to toggle some network settings in the Developer section of the iOS Settings. Go to the Network Link Conditioner section, enable it and select a profile. Very Bad Network is a good choice.

If you’re running on iOS Simulator, use the Network Link Conditioner included in the Advanced Tools for Xcode to change your network speed. This is a good tool to have in your arsenal. It forces you to be conscious of what happens to your apps when connection speeds are less than optimal.

Dispatch groups are a good candidate for all types of queues. You should be wary of using dispatch groups on the main queue if you’re waiting synchronously for the completion of all work. You don’t want to hold up the main thread, do you? ;] The asynchronous model is an attractive way to update the UI once several long-running tasks finish, such as network calls.

Your current solution is good, but in general it’s best to avoid blocking threads if at all possible. Your next task is to rewrite the same method to notify you asynchronously when all the downloads have completed.

Using Dispatch Groups, Take 2

Dispatching asynchronously to another queue then blocking work using wait is clumsy. Fortunately, there’s a better way. DispatchGroup can instead notify you when all the group’s tasks are complete.

Still in PhotoManager.swift, replace the code inside downloadPhotos(withCompletion:) with the following:

// 1
var storedError: NSError?
let downloadGroup = DispatchGroup()
for address in [
  PhotoURLString.overlyAttachedGirlfriend,
  PhotoURLString.successKid,
  PhotoURLString.lotsOfFaces
] {
  guard let url = URL(string: address) else { return }
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url) { _, error in
    storedError = error
    downloadGroup.leave()
  }   
  PhotoManager.shared.addPhoto(photo)
}   

// 2    
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

Here’s what’s going on:

  1. This time, you don’t need to put the method in an async call since you’re not blocking the main thread.
  2. notify(queue:work:) serves as the asynchronous completion closure. It runs when there are no more items left in the group. You also specify that you want to schedule the completion work to run on the main queue.

This is a much cleaner way to handle this particular job, as it doesn’t block any threads.

Build and run the app. Verify that the download complete alert is still displayed after all internet photos have downloaded:

All images are downloaded and the Download Completed alert is presented.