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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Grand Central Dispatch Tutorial for Swift 5: Part 2/2
30 mins
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:
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:
- The synchronous
wait
method blocks the current thread. Thus, you need to useasync
to place the entire method into a background queue. This ensures you don’t block the main thread. - Create a new dispatch group.
- Call
enter()
to manually notify the group that a task has started. You must balance out the number ofenter()
calls with the number ofleave()
calls, or your app will crash. - Notify the group that this work is done.
- 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 usewait(timeout:)
to specify a timeout and bail out on waiting after a specified time. - 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.
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:
- This time, you don’t need to put the method in an
async
call since you’re not blocking the main thread. -
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:
Exploring Concurrency Looping
With all these new tools at your disposal, you should probably thread everything, right?!

Thread ALL THE CODE!
Take a look at downloadPhotos(withCompletion:)
in PhotoManager
. You might notice that there’s a for
loop in there that cycles through three iterations and downloads three separate images. Your job is to see if you can run this for
loop concurrently to try and speed things up.
This is a job for DispatchQueue.concurrentPerform(iterations:execute:)
. It works like a for
loop in that it executes different iterations concurrently. It’s synchronous and returns only when all work is done.
You must take care when figuring out the optimal number of iterations for a given amount of work. Many iterations and a small amount of work per iteration can create so much overhead that it negates any gains from making the calls concurrent. The technique known as striding helps you out here. Striding allows you to do multiple pieces of work for each iteration.
When is it appropriate to use DispatchQueue.concurrentPerform(iterations:execute:)
? You can rule out serial queues because there’s no benefit there – you may as well use a normal for
loop. It’s a good choice for concurrent queues that contain looping, though, especially if you need to keep track of progress.
In PhotoManager.swift, replace the code inside downloadPhotos(withCompletion:)
with the following:
var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [
PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces
]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
let address = addresses[index]
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)
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
You replaced the former for
loop with DispatchQueue.concurrentPerform(iterations:execute:)
to handle concurrent looping.
This implementation includes a curious line of code: let _ = DispatchQueue.global(qos: .userInitiated)
. This causes GCD to use a queue with a .userInitiated
quality of service for the concurrent calls.
Build and run the app. Verify that the internet download functionality still behaves properly:
Running this new code on a device will sometimes produce marginally faster results. But was all this work worth it?
Actually, it’s not worth it in this case. Here’s why:
- You’ve probably created more overhead by running the threads in parallel than you would have by just running the
for
loop in the first place. You should useDispatchQueue.concurrentPerform(iterations:execute:)
for iterating over very large sets, along with the appropriate stride length. - You have limited time to create an app — don’t waste time pre-optimizing code that you don’t know is broken. If you’re going to optimize something, do so with something that is noticeable and worth your time. Find the methods with the longest execution times by profiling your app in Instruments. Check out How to Use Instruments in Xcode to learn more.
- Typically, optimizing code makes your code more complicated for yourself and for other developers coming after you. Make sure the added complication is worth the benefit.
Remember, don’t go crazy with optimizations. You’ll only make it harder on yourself and others who have to wade through your code.
Canceling Dispatch Blocks
Thus far, you haven’t seen code that allows you to cancel enqueued tasks. This is where dispatch block objects represented by DispatchWorkItem
comes into focus. Be aware that you can only cancel a DispatchWorkItem
before it reaches the head of a queue and starts executing.
Let’s show this by starting download tasks for several images from Le Internet then canceling some of them.
Still in PhotoManager.swift, replace the code in downloadPhotos(withCompletion:)
with the following:
var storedError: NSError?
let downloadGroup = DispatchGroup()
var addresses = [
PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces
]
// 1
addresses += addresses + addresses
// 2
var blocks: [DispatchWorkItem] = []
for index in 0..<addresses.count {
downloadGroup.enter()
// 3
let block = DispatchWorkItem(flags: .inheritQoS) {
let address = addresses[index]
guard let url = URL(string: address) else {
downloadGroup.leave()
return
}
let photo = DownloadPhoto(url: url) { _, error in
storedError = error
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
blocks.append(block)
// 4
DispatchQueue.main.async(execute: block)
}
// 5
for block in blocks[3..<blocks.count] {
// 6
let cancel = Bool.random()
if cancel {
// 7
block.cancel()
// 8
downloadGroup.leave()
}
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
Here’s a step-by-step walk-through of the code above:
- You expand the
addresses
array to hold three copies of each image. - You initialize a
blocks
array to hold dispatch block objects for later use. - You create a new
DispatchWorkItem
. You pass in aflags
parameter to specify that the block should inherit its Quality of Service class from the queue you dispatch it to. Then, you define the work to do in a closure. - You dispatch the block asynchronously to the main queue. For this example, using the main queue makes it easier to cancel select blocks since it's a serial queue. The code that sets up the dispatch blocks is already executing on the main queue. Thus, you know that the download blocks will execute at some later time.
- You skip the first three download blocks by slicing the
blocks
array. - Here, you use
Bool.random()
to randomly pick betweentrue
andfalse
. It's like a coin toss. - If the random value is
true
, you cancel the block. This can only cancel blocks that are still in a queue and haven't began executing. You can't cancel a block in the middle of execution. - Here, you remember to remove the canceled block from the dispatch group.
Build and run the app, then add images from Le Internet. You'll see that the app now downloads more than three images. The number of extra images changes each time you re-run your app. You cancel some of the additional image downloads in the queue before they start.
This is a pretty contrived example, but it's a nice illustration of how to use — and cancel — dispatch blocks.
Dispatch blocks can do a lot more, so be sure to check out Apple's documentation.
Miscellaneous GCD Fun
But wait! There’s more! Here are some extra functions that are a little farther off the beaten path. Although you won't use these tools that often, they can be helpful in the right situations.
Testing Asynchronous Code
This might sound like a crazy idea, but did you know that Xcode has testing functionality? :] Writing and running tests is important when building complex relationships in code.
Xcode tests are all contained in subclasses of XCTestCase
. They're any method whose signature begins with test
. Tests run on the main thread, so you can assume that every test happens in a serial manner.
As soon as a given test method completes, Xcode considers the test to have finished and moves on to the next test. This means that any asynchronous code from the previous test will continue to run while the next test is running.
Networking code is usually asynchronous, since you don't want to block the main thread while performing a network fetch. That, coupled with the fact that tests finish when the test method finishes, can make it hard to test networking code.
Take a brief look at how you can use semaphores to test asynchronous code.
Using Semaphores
Semaphores are an old-school threading concept introduced to the world by the ever-so-humble Edsger W. Dijkstra. Semaphores are a complex topic because they build upon the intricacies of operating system functions.
If you want to learn more about semaphores, check out this detailed discussion on semaphore theory. If you're the academic type, you may also want to check out Dining Philosophers Problem, which is a classic software development problem that uses semaphores.
Open GooglyPuffTests.swift and replace the code inside downloadImageURL(withString:)
with the following:
let url = try XCTUnwrap(URL(string: urlString))
// 1
let semaphore = DispatchSemaphore(value: 0)
_ = DownloadPhoto(url: url) { _, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
// 2
semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)
// 3
if semaphore.wait(timeout: timeout) == .timedOut {
XCTFail("\(urlString) timed out")
}
Here's how the semaphore works in the code above:
- You create a semaphore and set its start value. This represents the number of things that can access the semaphore without needing to increment it. Another name for incrementing a semaphore is signaling it.
- You signal the semaphore in the completion closure. This increments its count and signals that the semaphore is available to other resources.
- You wait on the semaphore with a given timeout. This call blocks the current thread until you signal the semaphore. A non-zero return code from this function means that the timeout period expired. In this case, the test fails because the network should not take more than 10 seconds to return — a fair point!
Run your tests by selecting Product ▸ Test from the menu or using Command-U, if you have the default key bindings. They should all succeed:
Disable your connection and run the tests again. If you're running on a device, put it in airplane mode. If you're running on the simulator, turn off your connection. The tests complete with a fail result after 10 seconds. It worked!
These are rather trivial tests. But if you're working with a server team, they can prevent finger-pointing of who is to blame for the latest network issue.
XCTWaiter
first before going down to these low-level APIs. XCTWaiter's APIs are much nicer and provide a lot of powerful technology for asynchronous testing.Using Dispatch Sources
Dispatch sources are a particularly interesting feature of GCD. You can use a dispatch source to monitor for some type of event. Events can include Unix signals, file descriptors, Mach ports, VFS Nodes and other obscure stuff.
When setting up a dispatch source, you tell it what type of events you want to check. You also need to define the dispatch queue on which its event handler block should execute. You then assign an event handler to the dispatch source.
Upon creation, dispatch sources start off in a suspended state. This allows you to perform any extra configuration required — such as setting up the event handler. After configuring your dispatch source, you must resume it to start processing events.
In this tutorial, you'll get a small taste of working with dispatch sources by using it in a rather peculiar way: to monitor when your app goes into debug mode.
Open PhotoCollectionViewController.swift and add the following just below the backgroundImageOpacity
global property declaration:
// 1
#if DEBUG
// 2
var signal: DispatchSourceSignal?
// 3
private let setupSignalHandlerFor = { (_ object: AnyObject) in
let queue = DispatchQueue.main
// 4
signal =
DispatchSource.makeSignalSource(signal: SIGSTOP, queue: queue)
// 5
signal?.setEventHandler {
print("Hi, I am: \(object.description ?? "")")
}
// 6
signal?.resume()
}
#endif
The code is a little involved, so walk through it step-by-step:
- You compile this code only in debug mode to prevent "interested parties" from gaining a lot of insight into your app. :] Add -D DEBUG under Project Settings ▸ Build Settings ▸ Swift Compiler - Custom Flags ▸ Other Swift Flags ▸ Debug. It should be set already in the starter project.
- Declare
signal
variable of typeDispatchSourceSignal
for use in monitoring Unix signals. - Create a block assigned to the
setupSignalHandlerFor
global property. You use it for one-time setup of your dispatch source. - Here, you set up
signal
. You're interested in monitoring theSIGSTOP
Unix signal. The main queue hands received events — you'll discover why shortly. - Next, you register an event handler closure that's invoked whenever you receive the
SIGSTOP
signal. Your handler prints a message that includes the class description. - All sources start off in the suspended state by default. Here, you tell the dispatch source to resume so it can start monitoring events.
Add the following code to viewDidLoad()
below the call to super.viewDidLoad()
:
#if DEBUG
setupSignalHandlerFor(self)
#endif
This code invokes the dispatch source's initialization code.
Build and run the app. Pause the program execution and resume the app immediately by pressing the Pause then Play buttons in Xcode's debugger:
Check out the console. Here's what you'll see:
Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7fbf0af08a10>
Your app is now debugging-aware! That's pretty awesome, but how would you use this in real life?
You could use this to debug an object and display data whenever you resume the app. You could also give your app custom security logic to protect itself (or the user's data) when malicious attackers attach a debugger to your app.
An interesting idea is to use this approach as a stack trace tool to find the object you want to manipulate in the debugger.

Time to flex some GCD muscles.
Think about that situation for a second. When you stop the debugger out of the blue, you're almost never on the desired stack frame. Now you can stop the debugger at any time and have code execute at your desired location. This is very useful if you want to execute code at a point in your app that's tedious to access from the debugger. Try it out!
Put a breakpoint on the print()
statement inside the setupSignalHandlerFor
block that you just added.
Pause in the debugger, then start again. The app will hit the breakpoint you added. You're now deep in the depths of your PhotoCollectionViewController
method. Now you can access the instance of PhotoCollectionViewController
to your heart's content. Pretty handy!
In the debugger console, type the following:
expr object.navigationItem.prompt = "WOOT!"
The Xcode debugger can sometimes be uncooperative. You might get this message:
error: use of unresolved identifier 'self'
If you do, you have to do it the hard way to work around a bug in LLDB. First, take note of the address of object
in the debug area:
po object
Then, manually cast the value to the type you want by running the following commands in the debugger, replacing 0xHEXADDRESS
with the outputted address:
expr let $vc = unsafeBitCast(0xHEXADDRESS, to: GooglyPuff.PhotoCollectionViewController.self)
expr $vc.navigationItem.prompt = "WOOT!"
If this doesn't work, lucky you – you encountered another bug in LLDB! In that case, you may have to try building and running the app again.
Once you've run this command successfully, resume execution of the app. You'll see the following:
With this method, you can make updates to the UI, inquire about the properties of a class and even execute methods — all while not having to restart the app to get into that special workflow state. Pretty neat.
Where to Go From Here?
You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.
Beyond GCD, you should check out Operation and OperationQueue Tutorial in Swift, a concurrency technology that is built on top of GCD. In general, it's best practice to use GCD if you are using simple fire-and-forget tasks. Operation
offers better control, an implementation for handling maximum concurrent operations and a more object-oriented paradigm at the cost of speed.
Also, take a look at the iOS Concurrency with GCD and Operations video course, which covers a lot of the same topics covered in this tutorial.
If you'd like to explore how to use async
and await
in your code, please check out our book, Modern Concurrency in Swift.
Remember, unless you have a specific reason to go lower, always try and stick with a higher-level API. Only venture into the dark arts of Apple if you want to learn more or to do something really, really "interesting." :]
If you have any questions or comments, please join the forum discussion below!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more