Even though threading and concurrency are more advanced concepts, you need to understand the basics regardless of your level of experience. The devices we develop applications for are powered by multicore processors and it's important to take advantage of that power.
In the previous episode, we covered threads, queues, and concurrency. In today's episode, I'd like to take a closer look at the main thread. I'm sure you've heard of the main thread and you may also know about the importance of keeping it responsive by minimizing the work your application performs on the main thread. Why is that? What happens if you perform a heavy operation on the main thread?
Whenever I'm reviewing code for an interview or browsing a student's project, it doesn't take long to form an idea of the developer's experience with Swift and Cocoa development. How a developer treats the main thread is often a good indication.
To get the most out of this episode, I recommend following along and answering the questions I have for you. It's this type of questions you might get asked during a job interview or when you're applying for a project as a freelancer.
The first step is easy. Download the starter project of this episode and open it in Xcode. The project isn't complicated. The application shows a table view with a collection of images. Each table view cell has an image on the left and a title on the right. Run the application on a device. What do you notice? What is your first impression?
You may wonder why the application shows a white view. A few seconds after launch, however, the table view is populated with images and titles. This isn't a great user experience. Pause the video and inspect the project. Try to find out why it takes a number of seconds for the images and titles to appear. The project isn't complicated. Give it a try.
Blocking the Main Thread
Let me explain what's happening. The project contains one UIViewController
subclass, ImagesViewController
. The ImagesViewController
class defines a property, dataSource
, of type [Image]
. Image
is a private struct and it defines a title
and a url
property. The array of Image
instances is used to populate the table view.
private lazy var dataSource: [Image] = [
Image(title: "Misery Ridge", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")),
Image(title: "Stonehenge Storm", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/7.jpg")),
...
Image(title: "Mountain Sunrise", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/8.jpg")),
Image(title: "Colours of Middle Earth", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/9.jpg"))
]
In tableView(_:cellForRowAt:)
, a ImageTableViewCell
instance is configured with the data stored in an Image
instance.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.reuseIdentifier, for: indexPath) as? ImageTableViewCell else {
fatalError("Unable to Dequeue Image Table View Cell")
}
// Fetch Image
let image = dataSource[indexPath.row]
// Configure Cell
cell.configure(with: image.title, url: image.url)
return cell
}
We're interested in the implementation of the configure(with:url:)
method of the ImageTableViewCell
class. In this method, the application updates the text
property of the titleLabel
with the value stored in the title
parameter and it uses the URL stored in the url
parameter to initialize a Data
instance. Because the URL can point to any location, including a remote server, it may take some time to initialize the Data
instance. Even if the URL points to a file on disk, it can take a significant amount of time to initialize the Data
instance, depending on the size of the file.
func configure(with title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Load Data
if let url = url, let data = try? Data(contentsOf: url) {
// Configure Thumbnail Image View
thumbnailImageView.image = UIImage(data: data)
}
}
The application creates a UIImage
instance using the Data
instance, which is assigned to the image
property of thumbnailImageView
, a UIImageView
instance. The initialization of the Data
and UIImage
instances takes place on the main thread. We can verify this by asking the Thread
class for the value of isMainThread
, a class computed property, which returns a boolean that indicates whether or not the call is made on the main thread. Add the following print statements to the configure(with:url:)
method and run the application.
func configure(with title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
print(Date())
print(Thread.isMainThread)
// Load Data
if let url = url, let data = try? Data(contentsOf: url) {
// Configure Thumbnail Image View
thumbnailImageView.image = UIImage(data: data)
}
}
The timestamps hint that it takes several seconds to load the data and instantiate the UIImage
instance. The output in the console also shows us that we're currently loading the image data on the main thread.
**2018-08-01 15:47:32 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:33 +0000**
**true**
**2018-08-01 15:47:34 +0000**
**true**
**2018-08-01 15:47:34 +0000**
**true**
**2018-08-01 15:47:34 +0000**
**true**
**2018-08-01 15:47:35 +0000**
**true**
Unfortunately, the problems don't end once the table view is populated with data. Have you tried scrolling the table view? That doesn't look great either. What's happening and, more importantly, what can we do to resolve these issues?
What's Wrong?
The short answer is simple. The application is performing badly because we're blocking the main thread. You may have heard or read that you should never block the main thread. What does blocking the main thread mean? And why does it have such a dramatic impact on the application's performance?
The user interface of the application is drawn to the screen on the main thread. Drawing the user interface to the screen isn't a cheap operation. It takes time and resources. Modern devices perform this operation up to sixty times per second. This means that every fraction of a second counts.
Do you remember the print statements from earlier? It took several seconds to load the data and instantiate the UIImage
instance. During that time the application was unable to draw the user interface to the screen and that's why we only saw a white screen. The same is true for scrolling the table view. Every time a table view cell is about to be displayed to the user, it loads the data for the UIImage
instance from a remote location, blocking the main thread.
For smooth scrolling, the application needs to have the resources to draw the user interface to the screen several times per second. That isn't possible if the main thread is blocked.
Performing Work In the Background
Even though the application is very basic, there are a number of critical issues. What would you do if you were asked to improve the launch experience of the application? Pause the video for a moment and consider your options.
There are several possible solutions, but there's one solution that stands out. No matter what your solution looks like, the goal should be to prevent the main thread from being blocked. How do you plan to achieve that? The solution I have in mind is easy to implement. We take advantage of Grand Central Dispatch. Remember from the previous episode that Grand Central Dispatch makes it straightforward to perform work concurrently, that is, in parallel. This is important if you want to take advantage of the power of a device with one or more multicore processors.
Let me show you what the solution looks like. Open ImageTableViewCell.swift and navigate to the configure(with:url:)
method. We ask the DispatchQueue
class for a reference to a global queue. We covered global or background queues in the previous episode. A global queue is a queue the operating system makes available to the application to perform work on. We can optionally specify the quality of service of the global queue. The quality of service refers to the importance of the work we're about to perform. We ask the DispatchQueue
class for a background queue. The operating system uses the quality of service of a queue to determine when work performed on that queue is executed.
We invoke the async(group:qos:flags:execute:)
method on the dispatch queue and pass a closure to the method. The async(group:qos:flags:execute:)
method immediately returns and the closure is asynchronously executed on the dispatch queue. This is essential. We could also invoke the sync(group:qos:flags:execute:)
method. The sync(group:qos:flags:execute:)
method returns control after the work we perform in the closure has finished. That isn't what we want because that would still result in the main thread being blocked.
We initialize the Data
instance with the value stored in url
and create a UIImage
instance with the data.
func configure(with name: String, url: URL?) {
// Configure Name Label
nameLabel.text = name
// Load Image
if let url = url {
DispatchQueue.global(qos: .background).async {
if let data = try? Data(contentsOf: url) {
let image = UIImage(data: data)
}
}
}
}
The last step is updating the image
property of thumbnailImageView
with the UIImage
instance. Remember what you learned earlier, the user interface should always be updated on the main thread. That means the image
property of thumbnailImageView
needs to be assigned on the main thread. Grand Central Dispatch makes this easy. We ask the DispatchQueue
class for a reference to the queue that is associated with the main thread and we asynchronously schedule a block of work on that queue by invoking the async(group:qos:flags:execute:)
method.
func configure(with name: String, url: URL?) {
// Configure Name Label
nameLabel.text = name
// Load Image
if let url = url {
DispatchQueue.global(qos: .background).async {
if let data = try? Data(contentsOf: url) {
let image = UIImage(data: data)
DispatchQueue.main.async {
self.thumbnailImageView.image = image
}
}
}
}
}
Let's see what we've accomplished so far. Launch the application and take a look at the result. We immediately see a table view with names and we even see an activity indicator view to show the user that the image is loading. That looks much better. Scrolling has also improved quite a bit, but it isn't perfect on older, slower devices. The images we're fetching are quite large and they're too large for this purpose. We only need to display a thumbnail. This isn't something we can easily fix on the client.
What's Next?
If you scroll the table view, you notice that the same images are fetched every time a table view cell moves into view. This decreases performance and it's also wasteful. In the next episode, we continue to improve the current implementation.