You already learned quite a bit about Grand Central Dispatch in this series. Most developers stop once they have a good grasp of the fundamentals. That's unfortunate because Grand Central Dispatch offers a number of more advanced APIs that add even more power to Apple's concurrency library. This episode focuses on dispatch work items.
Being In Control
Passing blocks of work to a dispatch queue is the most common use of Grand Central Dispatch. The API is concise and easy to use, but it has a few drawbacks. From the moment you submit a block of work to a dispatch queue, you no longer have control over its execution and that isn't always what you want. It can be useful to have the ability to cancel a block of work. This is especially useful for tasks that are time-sensitive or that require significant time or resources to execute.
Working with dispatch work items is in many ways identical to working with blocks of work, that is, closures. But there's an important difference. The DispatchWorkItem class offers more control and flexibility. In this episode, I show you how the DispatchWorkItem class can help solve a number of common problems.
Download the starter project of this episode if you'd like to follow along. The application is similar to the one we used for What Is the Main Thread. Build and run the application to freshen up your memory.
The application shows a collection of images in a table view. It takes a few moments for the images to render since they're located on a remote server. The application does its job, but there are a few issues, such as the performance of the table view. This is in part due to the size of the images, but there are more subtle issues that we need to address. Let's take a look at the implementation of the ImageTableViewCell class.
An ImageTableViewCell instance displays a thumbnail on the left and a title on the right. A view controller configures each table view cell by invoking the configure(title:url:) method. The method accepts a title and a URL.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
DispatchQueue.global(qos: .background).async {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
}
}
The URL instance is safely unwrapped and is used to load the data for the remote resource. The application uses the data to create a UIImage instance, which is assigned to the image property of the thumbnailImageView property of the table view cell. To ensure that the main thread isn't blocked, we take advantage of Grand Central Dispatch by loading the data on a background thread. The image view is updated on the main thread, using Grand Central Dispatch. This should look familiar by now.
The images are fetched from a remote server and they're quite large. This means that it takes a few moments to load and display them to the user. Can you imagine what happens if you quickly scroll the table view? Let me show you. The problem is more prominent if you run the application on a physical device with a slow network connection. Build and run the application, scroll the table view up and down a few times, and click the pause button in the debug bar at the bottom.
Open the Debug Navigator on the left to see the impact of the current implementation. Grand Central Dispatch creates dozens of background or worker threads to service the blocks of work the application dispatches to the global dispatch queue. This is only the tip of the iceberg.

Click the button in the top right of the Debug Navigator and choose View Process by Queue.

The dispatch queue to which the blocks of work are submitted has a label of com.apple.root.default-qos. The Debug Navigator indicates that dozens of blocks are running and dozens of blocks are pending, that is, waiting to be executed. This shows that Grand Central Dispatch caps the number of background or worker threads it creates to service the blocks of work.
What is happening and how can we prevent this from happening? Every time a table view cell is configured with a title and a URL, the data for the remote resource is fetched using Grand Central Dispatch. More blocks of work are added to the dispatch queue if we scroll the table view up and down a few times. The problem is that more blocks of work are added to the dispatch queue even if some of the table view cells are no longer visible. In other words, data is being fetched for table view cells that are no longer visible. This means that time and resources are wasted and it takes a while before the images are displayed to the user. Remember that a dispatch queue is a FIFO queue, first in first out.
Being in Control
We need the ability to cancel a block of work if it's no longer relevant to execute it. There are several options. We could use the URLSession API. It offers the ability to cancel a request at any time. That works fine, but this series focuses on Grand Central Dispatch. The solution I have in mind takes advantage of Grand Central Dispatch and the DispatchWorkItem class.
We start by defining a private, variable property, fetchDataWorkItem, of type DispatchWorkItem? in the ImageTableViewCell class.
import UIKit
final class ImageTableViewCell: UITableViewCell {
...
// MARK: -
private var fetchDataWorkItem: DispatchWorkItem?
...
}
We create a DispatchWorkItem instance by invoking its initializer. The initializer accepts a closure as its only argument. The closure is the block of work we would like to execute. We assign the result of the initialization to a constant, workItem.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
// Initialize Dispatch Work Item
let workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
})
DispatchQueue.global().async {
}
}
}
To execute the closure of the DispatchWorkItem instance, we submit it to the global dispatch queue for execution. The syntax should look familiar.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
// Initialize Dispatch Work Item
let workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
})
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().async(execute: workItem)
}
}
Before we continue, I'd like to make a subtle improvement. If the user scrolls the table view, then there's no need to immediately fetch the images for the table view cells. By adding a small delay to the execution of the dispatch work item, we can drastically improve the performance of the application. Grand Central Dispatch makes this trivial. Instead of passing the dispatch work item to the async(execute:) method, we pass it to the asyncAfter(deadline:execute:) method. The syntax isn't complicated.
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
The first argument of the asyncAfter(deadline:execute:) method is of type DispatchTime. We ask the DispatchTime struct for the current time by invoking now(), a static method of the DispatchTime struct, and add one second to it. That's it.
Last but not least, we store a reference to the DispatchWorkItem instance in the fetchDataWorkItem property. Why we do that becomes clear in a moment.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
// Initialize Dispatch Work Item
let workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
})
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
Build and run the application to make sure we didn't break anything.
We use the DispatchWorkItem class for its ability to cancel its execution. We want to cancel the execution of the dispatch work item if the table view cell is no longer visible. It doesn't make any sense to fetch the data for a remote resource if the table view cell is no longer visible.
There are several strategies to accomplish this. The easiest solution is to cancel the execution of the dispatch work item in the prepareForReuse() method of the ImageTableViewCell class. This is easy. We invoke the cancel() method on the DispatchWorkItem instance. That is why we keep a reference to the DispatchWorkItem instance in the fetchDataWorkItem property.
override func prepareForReuse() {
super.prepareForReuse()
// Cancel Execution
fetchDataWorkItem?.cancel()
// Reset Thumnail Image View
thumbnailImageView.image = nil
}
Build and run the application, scroll the table view a few times up and down, and pause the execution of the application. Open the Debug Navigator on the left.

The Debug Navigator shows us that the number of running and pending blocks has dropped substantially. This is the result of adding the delay and cancelling the dispatch work item in the prepareForReuse() method.
It's important that you understand how Grand Central Dispatch handles the cancellation of a dispatch work item. If a dispatch work item is cancelled before it's scheduled for execution, then the dispatch work item is not executed. This makes sense.
What happens if the dispatch work item is cancelled as it's being executed? You need to know that Grand Central Dispatch doesn't cancel a dispatch work item that is being executed. That's only part of the story, though. If the cancel() method of a DispatchWorkItem instance is invoked, its isCancelled property is set to true. It's up to the developer to decide what should happen if a dispatch work item that is being executed is cancelled.
With this in mind, we can improve the current implementation. It's possible that the table view cell is no longer visible the moment the dispatch work item has fetched the data for the remote resource. In that scenario, it doesn't make sense to update the image view of the table view cell. Not only is it unnecessary, it also results in odd, visual artifacts. When this happens, the image view of a table view cell is updated two or more times. It also decreases performance because the image view is updated on the main thread.
The plan is simple. The application should inspect the value of the isCancelled property after fetching the data for the remote resource. If the dispatch work item is cancelled, then there's no need to update the image view.
We keep the implementation simple. We inspect the value of the isCancelled property to decide whether it's necessary to continue. We only update the image view on the main thread if the dispatch work item hasn't been cancelled.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
// Initialize Dispatch Work Item
let workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
})
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
There's one problem, though. The compiler notifies us that it isn't possible to reference the DispatchWorkItem instance within the closure that is passed to the initializer. The solution is simple, but it looks a bit odd. We declare a variable property, workItem, of type DispatchWorkItem?.
We assign the DispatchWorkItem instance to the workItem variable. Because workItem is an optional, we need to safely unwrap it in the closure that is passed to the initializer. We also need to safely unwrap the value stored in workItem to submit it to the global dispatch queue.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}
Build and run the application to see the result. The performance of the table view has increased substantially because the application no longer updates the image view on the main thread if the dispatch work item has been cancelled.
Memory Management
The result looks good and the implementation isn't that complicated. But I'm afraid there's a problem. Scroll the table view up and down a few times and click the Debug Memory Graph button in the debug bar at the bottom. The debugger interrupts the execution and the Debug Navigator on the left shows a list of the objects that are currently in memory. The text field at the bottom allows us to filter the list. Enter DispatchWorkItem in the text field to only show DispatchWorkItem instances in memory.

That doesn't look good. The Debug Navigator shows us that hundreds of DispatchWorkItem instances are in memory. We take a close look at memory management in the next episode.
What's Next
Apple introduced automatic reference counting in Objective-C many years ago and Swift also benefits from this technology. Automatic reference counting is great, but it often gives developers the false impression that memory management isn't something you need to worry about. That isn't true. Memory management should always be in the back of your mind when you're writing Swift or Objective-C. That's the focus of the next episode.