Today, I’ll show you how to use Swift 4 and the Grand Central Dispatch (GCD) application programming interface (API) to implement the execution of (multiple) tasks in the background, i.e., parallel/concurrent execution of tasks on a multicore CPU. I’ve built a sample app that gives you two options: 1) synchronous execution of tasks in the background and 2) asynchronous execution of tasks in the background. All my Swift 4 code from this article, part of an Xcode 9 project which builds a fully-functional working sample app, is available for download here. Join me in: reviewing concurrent programming concepts; reviewing my concurrent Swift 4 code; and, examining videos of my app in action, videos of console output from my app, and the console output text itself. I’ll even show you how to graphically visualize my app’s CPU and thread usage with Xcode’s Debug Navigator.
This is a look at the app — a snapshot — after all images have finished downloading asynchronously in the background:
Here’s a video of the app downloading images asynchronously in the background:
While the code I’ll show you today is written in Swift 4, it was originally written in Swift 3. This article is a revised edition of two Swift 3-based articles I wrote in in 2017, “Concurrency in iOS — Grand Central Dispatch (GCD) with Swift 3” and “Concurrency in iOS: serial and concurrent queues in Grand Central Dispatch (GCD) with Swift 3”, and an Xcode 8.3.1 project stored in a GitHub repo. So I do want to spend some time today discussing the process of upgrading an Xcode project targeting Swift 3 to one targeting Swift 4.
If you need a refresher on concurrency and GCD, you can refer back to my original two 2017 articles here and here.
Introduction
We now live in the day and age of writing apps that can run on devices with CPUs that have multiple cores. We can go way beyond the notion of “multitasking” as a bunch of processes vying for a “timeslice” of the CPU. We can literally run processes simultaneously on different cores. As iOS developers, it is vitally important that we understand the concept of concurrency. Why?
From the perspective of a user, it is easy to understand: An app’s user interface (UI) should always be responsive, no matter how many things the app is doing — and doing simultaneously, like downloading a file and performing geo-location; like a UITableViewCell's
thumbnail images loading on demand as a user scrolls up and down through the UITableView
.
Concurrency
Let’s briefly refresh our memories. According to Apple, “Concurrency is the notion of multiple things happening at the same time.” Instead of going through all-things-concurrency again, I’m going to provide you with a list of links to concepts and terms that you can easily review:
- Terminology: threads, process, and tasks
- Concurrent versus parallel
- Serial versus concurrent queues (deterministic versus nondeterministic)
- Asynchronous versus synchronous
Grand Central Dispatch (GCD)
Apple’s Grand Central Dispatch (GCD) is probably still the most popular means of tackling this crucial iOS topic of providing concurrency — especially UI responsiveness in apps. While GCD is nice, it’s a bit on the low-level-API end of the spectrum, and hard to fine-tune (like it’s hard to cancel a background task). I encourage you to take a look at my article introducing the abstract Operation
class, specifically the BlockOperation
class to achieve what I’m accomplishing herein with GCD, but with code that I feel reads as semantically cleaner. But GCD is extremely powerful and still ubiquitous, so I do want you to know about it — and keep in mind that Operation
was built on the shoulders of GCD (AH, HA!).
The code
Upgrading my project from Swift 3 to Swift 4
Look through my source code for annotations marked [*0], [*1], [*2], [*3], and [*4] for inline commentary explaining the results of the automatic Swift language upgrade tool I used when I moved from Xcode 8.3.1 to Xcode 9, and thus from Swift 3 to Swift 4. The changes weren’t too drastic but, for example, look at [*1] and [*3].
Consider the statement that the Swift 4 compiler now flags as an error: “UIView.viewWithTag(_:) must be used from main thread only.” I was (past tense; my Swift 3 version) calling it from a background thread:
1 |
let imageView : UIImageView = self.view.viewWithTag(i) as! UIImageView |
The compiler was (is) smart enough to detect the fact that I was making this call inside a GCD block that would not be executed on the main thread. It is now smart enough with Swift 4. It wasn’t in Swift 3.
Since I was “just” getting a reference to a UIImageView
instance (object), a read-only operation, I assumed it was “safe” to do in a background thread.
Eh, not so fast thar, partner. We’ins gonna git you fer makin’ potentially unsafe calls. (That’s supposed to be the Swift 4 compiler talking. Shhhrrriiiiinnnggggg!)
Doing things concurrently involves a stochastic (random) element. What if another thread/block/task accessed that UIImageView
object at the same time I accessed it in my code shown below — like two threads trying to set the UIImageView.image
property at the same time? What if iOS did something to that UIImageView
object? These scenarios are unlikely because of the way I wrote my code. Each of my download tasks handles one UIImage
object.
My gut instinct tells me that the Swift 4 compiler will flag all statements involving the UI (UIKit
) from a background thread, even just a reference.
I like it. Better safe than sorry.
Listen to your father! “Never update the UI from a background thread; always use the main thread.” I’ll have more to say on all this Swift version transition/upgrade process soon…
Use of serial and concurrent queues
You can advantageously choose between serial and concurrent GCD queues. If you have a set of tasks, for example a bunch of image downloads that you need to run in the background, but don’t care about the order in which the downloads occur, use a concurrent queue (see function startAsyncImageDownload(_)
below). If you have a set of CPU-intensive tasks that you’d like to run in the background, but must be run in a specific order, use a serial queue (see function startSyncImageDownload(_)
below). Since a serial queue executes tasks one at a time in the order you specify, they’re great for protecting shared resources; no two or more tasks can access that resource at the same time. Serial queues are also great for tasks that have interdependencies and must be run in a specific order. Think of building a house. You can’t put up the roof until the house’s foundation has been laid and at least one floor with walls has been constructed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
import UIKit import Foundation class ViewController: UIViewController { // MARK: - ViewController properties @IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var tapCountTextField: UITextField! // // [*0] // NOTE: I'm applying the @objc attribute to the following // Swift properties to make them accessibile to the Objective-C // frameworks like Foundation and UIKit. // @objc let imageViewTags: [Int] = [10,11,12,13,14,15,16,17,18,19,20] // NASA images used pursuant to https://www.nasa.gov/multimedia/guidelines/index.html @objc let imageURLs: [String] = ["https://cdn.spacetelescope.org/archives/images/publicationjpg/heic1509a.jpg", "https://cdn.spacetelescope.org/archives/images/publicationjpg/heic1501a.jpg", "https://cdn.spacetelescope.org/archives/images/publicationjpg/heic1107a.jpg", "https://cdn.spacetelescope.org/archives/images/large/heic0715a.jpg", "https://cdn.spacetelescope.org/archives/images/publicationjpg/heic1608a.jpg", "https://cdn.spacetelescope.org/archives/images/publicationjpg/potw1345a.jpg", "https://cdn.spacetelescope.org/archives/images/large/heic1307a.jpg", "https://cdn.spacetelescope.org/archives/images/publicationjpg/heic0817a.jpg", "https://cdn.spacetelescope.org/archives/images/publicationjpg/opo0328a.jpg", "https://cdn.spacetelescope.org/archives/images/publicationjpg/heic0506a.jpg", "https://cdn.spacetelescope.org/archives/images/large/heic0503a.jpg"] @objc var imageCounter = 0 @objc var buttonTapCount = 0 // MARK: - UIViewController delegate override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. progressView.progress = 0.0 tapCountTextField.text = String(0) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(true) //buildLayerWithTag(tag: 10) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } // MARK: - User interactions @IBAction func pressAsyncTapped(_ sender: Any) { buttonTapCount += 1 tapCountTextField.text = String(buttonTapCount) } @IBAction func clearImages(_ sender: Any) { progressView.progress = 0.0 imageCounter = 0 buttonTapCount = 0 tapCountTextField.text = String(buttonTapCount) for i in imageViewTags { let imageView : UIImageView = self.view.viewWithTag(i) as! UIImageView imageView.image = nil } } @IBAction func startAsyncImageDownload(_ sender: Any) { printDateTime() // Download enough images to fill in the UIImageViews on my // UIViewController; I've set each UIImageView's "tag" property // so I can access each UIImageView programmatically for i in imageViewTags { // // [*1] Thread initialization: "you should always call the methods // of the UIView class from code running in the main thread of // your application" // https://developer.apple.com/documentation/uikit/uiview // let imageView : UIImageView = self.view.viewWithTag(i) as! UIImageView // show adding the download task to the queue for execution; // note that tasks will be ADDED almost INSTANTLY, and you'll // see them finish executing later IN ORDER print("Image tagged \(i) enquued for execution") // Start each image download task asynchronously by submitting // it to the default background queue; this task is submitted // and DispatchQueue.global returns immediately. DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { /// *** BEGIN DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ // // I'm PURPOSEFULLY downloading the image using a synchronous call // (NSData), but I'm doing so in the BACKGROUND. // // // [*1] // let imageView : UIImageView = self.view.viewWithTag(i) as! UIImageView // // ERROR: "UIView.viewWithTag(_:) must be used from main thread only" // This is a "Runtime" error. // let imageURL = URL(string: self.imageURLs[i-10]) let imageData = NSData(contentsOf: imageURL!) // Once the image finishes downloading, I jump onto the MAIN // THREAD TO UPDATE THE UI. DispatchQueue.main.async { // ** BEGIN DEFINITION OF TASK/BLOCK COMPLETION ** \\ // // [*2] // imageView.image = UIImage(data: imageData as! Data) // // "Forced cast from 'NSData?' to 'Data' only // unwraps and bridges; did you mean to use '!' with 'as'?" // This is a "Buildtime" warning. // // So... forcibly unwrap imageData and cast to Data imageView.image = UIImage(data: imageData! as Data) self.imageCounter += 1 self.progressView.progress = Float(self.imageCounter) / Float(self.imageURLs.count) print("Image tagged \(i) finished downloading") self.view.setNeedsDisplay() if self.imageCounter == (self.imageURLs.count) { self.printDateTime() } // ** END DEFINITION OF TASK/BLOCK COMPLETION ** \\ } // end DispatchQueue.main.async /// *** END DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ } // end DispatchQueue.global } // end for i in imageViewTags } // end startAsyncImageDownload @IBAction func startSyncImageDownload(_ sender: Any) { printDateTime() // Define a custom SERIAL background queue. I specify serial by not specifying // anything. Serial is the default. let serialQueue: DispatchQueue = DispatchQueue(label: "com.iosbrain.SerialImageQueue") // Download enough images to fill in the UIImageViews on my // UIViewController; I've set each UIImageView's "tag" property // so I can access each UIImageView programmatically for i in imageViewTags { // // [*3] Thread initialization: "you should always call the methods // of the UIView class from code running in the main thread of // your application" // https://developer.apple.com/documentation/uikit/uiview // let imageView : UIImageView = self.view.viewWithTag(i) as! UIImageView // show adding the download task to the queue for execution; // note that tasks will be ADDED almost INSTANTLY, and you'll // see them finish executing later IN **ANY** ORDER print("Image tagged \(i) enquued for execution") // Start each image download task asynchronously by submitting // it to the CUSTOM SERIAL background queue; this task is submitted // and serialQueue.async returns immediately. serialQueue.async { /// *** BEGIN DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ // // I'm PURPOSEFULLY downloading the image using a synchronous call // (NSData), but I'm doing so in the BACKGROUND. // // // [*3] // let imageView : UIImageView = self.view.viewWithTag(i) as! UIImageView // // "UIView.viewWithTag(_:) must be used from main thread only" // This is a "Runtime" error // let imageURL = URL(string: self.imageURLs[i-10]) let imageData = NSData(contentsOf: imageURL!) // Once the image finishes downloading, I jump onto the MAIN // THREAD TO UPDATE THE UI. DispatchQueue.main.async { // ** BEGIN DEFINITION OF TASK/BLOCK COMPLETION *** \\ // // [*4] // imageView.image = UIImage(data: imageData as! Data) // // "Forced cast from 'NSData?' to 'Data' only // unwraps and bridges; did you mean to use '!' with 'as'?" // This is a "Buildtime" warning. // // So... forcibly unwrap imageData and cast to Data imageView.image = UIImage(data: imageData! as Data) self.imageCounter += 1 self.progressView.progress = Float(self.imageCounter) / Float(self.imageURLs.count) print("Image tagged \(i) finished downloading") self.view.setNeedsDisplay() if self.imageCounter == (self.imageURLs.count) { self.printDateTime() } // ** END DEFINITION OF TASK/BLOCK COMPLETION *** \\ } // end DispatchQueue.main.async /// *** END DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ } // end serialQueue.async } // end for i in imageViewTags } // end func startSyncImageDownload // MARK: - Utilities func printDateTime() -> Void { let date = Date() let formatter = DateFormatter() formatter.dateFormat = "dd/MM/yyyy HH:mm:ss:SSS" let result = formatter.string(from: date) print(result) } } // end class ViewController |
Asynchronous execution – console output
Note that the download image tasks are queued for background execution almost instantly and in order from imageURLs
array index 0 to 10 (11 items). The images download asynchronously — the ordering of image download tasks is always stochastic (random) when using a concurrent queue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
07/03/2018 08:35:32:641 Image tagged 10 enquued for execution Image tagged 11 enquued for execution Image tagged 12 enquued for execution Image tagged 13 enquued for execution Image tagged 14 enquued for execution Image tagged 15 enquued for execution Image tagged 16 enquued for execution Image tagged 17 enquued for execution Image tagged 18 enquued for execution Image tagged 19 enquued for execution Image tagged 20 enquued for execution ... Image tagged 16 finished downloading Image tagged 10 finished downloading Image tagged 12 finished downloading Image tagged 19 finished downloading Image tagged 17 finished downloading Image tagged 20 finished downloading Image tagged 11 finished downloading Image tagged 18 finished downloading Image tagged 15 finished downloading Image tagged 14 finished downloading Image tagged 13 finished downloading 07/03/2018 08:36:11:336 |
Watch the console output from asynchronous task execution:
Synchronous execution – console output
Note again that the download image tasks are queued for background execution almost instantly and in order from imageURLs
array index 0 to 10 (11 items). While a serial queue executes its tasks one at a time, in the order submitted, it’s still running in the background. It may be slower than a concurrent queue, but it’s still in the background. My rough calculations show that the serial queue download takes, on average, 21% longer than the concurrent queue download.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
07/03/2018 08:46:42:086 Image tagged 10 enquued for execution Image tagged 11 enquued for execution Image tagged 12 enquued for execution Image tagged 13 enquued for execution Image tagged 14 enquued for execution Image tagged 15 enquued for execution Image tagged 16 enquued for execution Image tagged 17 enquued for execution Image tagged 18 enquued for execution Image tagged 19 enquued for execution Image tagged 20 enquued for execution ... Image tagged 10 finished downloading Image tagged 11 finished downloading Image tagged 12 finished downloading Image tagged 13 finished downloading Image tagged 14 finished downloading Image tagged 15 finished downloading Image tagged 16 finished downloading Image tagged 17 finished downloading Image tagged 18 finished downloading Image tagged 19 finished downloading Image tagged 20 finished downloading 07/03/2018 08:47:29:664 |
Watch the console output from synchronous task execution:
Good tool – Debug Navigator -> CPU
Worker threads being created
If you bring up Xcode’s Debug Navigator and click on CPU, you can see a visual representation of how hard (or easy) your app is pushing the target device’s CPU. You can see your main thread (1) and count how many other threads iOS has decided are necessary for your app to perform well. Note that as soon as I submitted my 11 download image tasks to dispatch queues
, the number of threads jumped almost immediately. The number of threads does not necessarily equal the number of tasks I submit. It’s up to iOS to optimize the details.
Worker thread cleanup
If you’re patient and watch the threading details until after the images are downloaded, you’ll see the extra threads get cleaned up. Be patient. It may take awhile for iOS to self-regulate — or the threads may be cleaned up quickly. It’s hard to tell. iOS is pretty smart.
Wrapping up
After reviewing everything I’ve presented, you should have a good grasp of concurrency and implementing it through the very popular GCD API. I can’t stress enough how very important it is for you to keep your app’s UI responsive all the time. We live in a very crowded app space. There a millions of smartphone apps out there for consumers to choose from — and choose they will, voting with their feet, if some app they download is sluggish.
I encourage you to broaden your concurrency horizons and look at APIs like Operation
which I’ve introduced here. It takes more time to learn, but it is a much more feature-full and semantically abstract technology.
Enjoy!