[Download the full Xcode 9 project, written in Swift 4, from GitHub.]
I’m going to introduce you to iOS concurrency with simple Swift 4 code that uses an API based on the Operation
abstract class. In more complex scenarios, you would subclass and customize Operation
, but iOS provides the built-in BlockOperation
subclass of Operation
which we’ll use here. I’ll review the tenets of concurrency, emphasize why it is necessary in almost all apps, explain the basic infrastructure of Operation
, compare it to Grand Central Dispatch (GCD), and then walk you through the Swift 4 code I wrote to implement concurrency in a real-live app based on BlockOperation
. I’ll even show you how to graphically visualize your app’s CPU and thread usage with Xcode’s Debug Navigator. Here’s the app that we’ll build together:
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
.
We’ve talked about using Apple’s Grand Central Dispatch (GCD) to tackle this crucial iOS topic in my articles here and here. GCD is nice, but 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). Today, I’m going to start introducing you to a higher-level alternative to GCD, the Foundation
framework’s family of classes headed by the abstract Operation
class. We’ll do everything in Swift 4 where NSOperation
has been renamed to Operation
. Specifically, I’ll concentrate on showing you how to use the BlockOperation
class to download a bunch of huge files in the background, allowing the sample app’s UI to remain responsive. We’ll also use BlockOperation
to run a lengthy computation in the background — at the same time that those files are downloading.
Introduction
Let’s briefly refresh our memories. According to Apple, “Concurrency is the notion of multiple things happening at the same time.”
I’m not going to regurgitate the discussion on concurrency. If you’re still unclear on the concept, read my articles on “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”.
Let’s concentrate on Apple’s high-level and abstract base class Operation
for implementing concurrency in our iOS apps. I’m going to go minimal today by presenting you with: 1) some basic definitions, 2) the Swift 4 code for a fully functional app capable of concurrency, and 3) videos and images of my sample app in action. You should review those three aspects of my explanation of Operation
and then download, build, run, and interact with my app. You might even want to set a few breakpoints. If you’ve never done much concurrent programming, if you swear by Grand Central Dispatch (GCD) as your only concurrent resource, or you’re curious about the differences between GCD and Operation
, you should compare today’s discussion and code to the discussion and code in my first two articles, purposely written on GCD to show you a lower level concurrency API, “closer to the Unix kernel” if you will, here and here.
Operation
In this section, I’ll quote straight from the horse’s mouth: Apple. Note that with Swift 4, a lot of type names that used to begin with NS
have lost the NS
. NSOperation
is now Operation
; NSOperationQueue
is now OperationQueue
; and so on. Pages titled Operation
and OperationQueue
start mentioning NSOperation
or NSOperationQueue
as you read further into the knowledge base articles. It seems Apple is playing catch-up with the fast-paced changes it’s making to its own technologies.
Operation
: “An abstract class that represents the code and data associated with a single task.” When you get deeper intoOperation
, you’ll start subclassing this guy to facilitate making your tasks capable of concurrency.
OperationQueue
: “A queue that regulates the execution of a set of operations. … After being added to an operation queue, an Operation instance remains in that queue until it is explicitly canceled or finishes executing its task. Operations within the queue (but not yet executing) are themselves organized according to priority levels and inter-operation object dependencies and are executed accordingly. An application may create multiple operation queues and submit operations to any of them. …”
Y’all remember what a queue is, correct? I hope so, because you’re not going to get any tasks running in the background without a queue. Here are two definitions from Oxford:
British A line or sequence of people or vehicles awaiting their turn to be attended to or to proceed. …
Computing A list of data items, commands, etc., stored so as to be retrievable in a definite order, usually the order of insertion.
IMPORTANT! Instead of subclassing Operation
today, we’re going to start simple with the following class:
BlockOperation
: “An operation that manages the concurrent execution of one or more blocks. … TheBlockOperation
class is a concrete subclass of Operation that manages the concurrent execution of one or more blocks. You can use this object to execute several blocks at once without having to create separate operation objects for each. When executing more than one block, the operation itself is considered finished only when all blocks have finished executing.
Blocks added to a block operation are dispatched with default priority to an appropriate work queue. …”
addExecutionBlock(_:)
: “Adds the specified block to the receiver’s list of blocks to perform. … The specified block should not make any assumptions about its execution environment. …”
addOperation(_:)
: “Adds the specified operation object to the receiver. … Once added, the specified operation remains in the queue until it finishes executing. …”
Why use Operation
?
The Operation
family of classes form a concurrency API that is much higher-level, abstract, and feature-full than GCD. And guess what: Operation
was built using GCD. Operation
provides you with the capabilities to:
- cancel a task or tasks;
- monitor the states of tasks (like through
isReady
,isExecuting
,isFinished
, orisCancelled
); - create dependencies between tasks, for example, so you can specify a specific order in which your tasks will execute in the background;
- specify whether tasks running synchronously or asynchronously (you could do that in GCD);
- not grow grey hairs over synchronization as you have “multicore aware[ness];” and,
- “prioritize the order in which operations [tasks] execute.”
Demonstrating Operation
through BlockOperation
Remember, I’m just introducing you to the whole Operation
hoo-hah today. It’s a big API and takes time, work, and a decent understanding of the technology to get a fully-featured Operation
-based app up and running in which you can cancel tasks, observe their states, prioritize tasks, specify synchronous vs. asynchronous execution, etc. That’s why I’m starting today with BlockOperation
, an “operation that manages the concurrent execution of one or more blocks” and which is a “concrete subclass of Operation that manages the concurrent execution of one or more blocks.”
Note that my BlockOperation
sample app code, shown below and downloadable here, looks an awful lot like the GCD code that I introduced you to in my article, “Concurrency in iOS: serial and concurrent queues in Grand Central Dispatch (GCD) with Swift 3”. That’s the whole idea. I want to gently introduce you into the wild world of Operation
.
When reading my code, and especially when running it, notice that the images I download always come down in a different order. The ordering of my download tasks is asynchronous. I’ll explain why later, but as homework, can you figure out why? Hint: We’d have to get into subclassing the Operation
class, which is generally what people with more-than-trivial concurrency needs do. I didn’t want to do that here. I wanted to show you that you can write some basic code for backgrounding tasks using just BlockOperation
when your needs are simple. Hint, hint: With Operation
, the default is asynchronous.
The code
Here’s my code demonstrating BlockOperation
in action. Please read the inline commentary as that’s where I explain a lot of this code:
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 |
import UIKit class ViewController: UIViewController { // MARK: - User interface objects @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var imageIndexText: UILabel! @IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var incrementCountText: UITextField! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! // MARK: - ViewController properties var imageCounter = 0 var incrementCounter = 0 // NASA images used pursuant to https://www.nasa.gov/multimedia/guidelines/index.html 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"] // MARK: - ViewController delegate override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. // no images have been downloaded, so there's no // image progress to report progressView.progress = 0.0 // the long calculation isn't running, so we don't show // any activity activityIndicator.alpha = 0.0 } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } // MARK: - ViewController methods: BlockOperation example code // // Start a long, mindless calculation IN THE BACKGROUND // so the UI, handled by the MAIN THREAD, doesn't freeze // up, and the user has a good experience // func startLongCalculation() -> Void { activityIndicator.alpha = 1.0 activityIndicator.startAnimating() // create a default queue so we can put our // long running calculation on it for processing; // this queue will only have one operation on it let longCalculationQueue = OperationQueue() // create an operation that manages the concurrent // execution of one blocks; that block is a simple // but long-running computation (addition) let blockOperationForLongCalculation = BlockOperation { /// *** BEGIN DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ // we'll repeatedly add 1 to this value // one million five hundred thousand times var total = 0 for _ in 1...10000000 { total = total + 1 print("total + 1 = \(total)") } // when the computation finished, we JUMP // ONTO THE MAIN QUEUE TO UPDATE THE UI OperationQueue.main.addOperation { // ** BEGIN DEFINITION OF TASK/BLOCK COMPLETION ** \\ print("FINAL total: \(total)") self.activityIndicator.stopAnimating() self.activityIndicator.alpha = 0.0 // ** END DEFINITION OF TASK/BLOCK COMPLETION ** \\ } // end OperationQueue.main.addOperation /// *** END DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ } // end let blockOperationForLongCalc = BlockOperation // by adding the BlockOperation to its corresponding // queue, it starts executing and "remains in the queue // until it finishes executing" longCalculationQueue.addOperation(blockOperationForLongCalculation) } // end func startLongCalculation() // // Start downloading 11 HUGE images IN THE BACKGROUND // so the UI, handled by the MAIN THREAD, doesn't freeze // up, and the user has a good experience // func startBackgroundDownload() -> Void { printDateTime() // create a default queue so we can put our // 11 long running image downloads on it for processing let imageDownloadQueue = OperationQueue() // "The maximum number of queued operations that can // execute at the same time." I set this for // illustrative purposes, as its probably best // to let iOS tune concurrency based on system // resources, but there may be special circumstances // where this could be helpful. imageDownloadQueue.maxConcurrentOperationCount = 20 // create an operation that manages the concurrent // execution of 11 blocks; each block 1) starts downloading // a LARGE image from a specific URL and 2) when finished // downloading, JUMPS ONTO THE MAIN THREAD TO DISPLAY // THE IMAGE let blockOperationForImageDownloads = BlockOperation() // add 11 image download blocks to our BlockOperation for index in 0..<imageURLs.count { // watch how these are printed to the console // IMMEDIATELY; this validates backgrounding // and concurrency print("Batch 1 - Image \(index) queued for download") // add the image download block to our BlockOperation blockOperationForImageDownloads.addExecutionBlock { /// *** BEGIN DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ let imageURL = URL(string: self.imageURLs[index]) let imageData = NSData(contentsOf: imageURL!) OperationQueue.main.addOperation { // ** BEGIN DEFINITION OF TASK/BLOCK COMPLETION *** \\ print("Batch 1 - Image \(index) has downloaded") self.imageCounter += 1 self.progressView.progress = Float(self.imageCounter) / Float(self.imageURLs.count) self.imageView.image = UIImage(data: imageData! as Data) self.imageIndexText.text = String(index) self.view.setNeedsDisplay() if self.imageCounter == (self.imageURLs.count) { self.printDateTime() } // ** END DEFINITION OF TASK/BLOCK COMPLETION ** \\ } // end OperationQueue.main.addOperation /// *** END DEFINITION OF TASK/BLOCK TO RUN IN BACKGROUND *** \\\ } // end blockOperationForImageDownloads.addExecutionBlock } // end for index in 0..<imageURLs.count // by adding the BlockOperation, with 11 image download // tasks, to its corresponding queue, it starts // executing and "remains in the queue until it // finishes executing" imageDownloadQueue.addOperation(blockOperationForImageDownloads) } // end func startBackgroundDownload() // MARK: - ViewController methods: User interactions // Called when the button labelled "Start images downloading - // Batch 1" is tapped; starts download of set of LARGE images // in the BACKGROUND. As each image finishes downloading, it // is displayed in the UIImageView in this view controller. @IBAction func startImagesDownloadingBatch1Tapped(_ sender: Any) { startBackgroundDownload() } // Called when the button labelled "Start Long Calculation" // is tapped. Starts a "long" and simple-minded calculation // in the BACKGROUND. It prints output to console. @IBAction func startLongCalcButtonTapped(_ sender: Any) { startLongCalculation() } // Called when the "Increment count:" button is tapped. // We add one to a property every time the button is // tapped and display the new property value in a // UITextField. This PROVES that the BlockOperations // are running in the BACKGROUND because the button // and text field ALWAYS remain responsive. @IBAction func incrementButtonTapped(_ sender: Any) { incrementCounter = incrementCounter + 1 incrementCountText.text = String(incrementCounter) } // MARK: - Utilities // print precise date so we can get an idea of // how long tasks take to execute 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 |
The app in action
Here’s my app downloading files asynchronously (video immediately below). Notice that my app’s UI stays fully responsive while images are downloaded in the background? I can keep pressing the “Increment count:” button and the UIActivityIndicatorView
keeps spinning all while images are being downloaded and a long-running calculation is being executed.
Console output from my code – downloading files
Here’s the console output when I download images:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
04/03/2018 10:27:28:938 Batch 1 - Image 0 queued for download Batch 1 - Image 1 queued for download Batch 1 - Image 2 queued for download Batch 1 - Image 3 queued for download Batch 1 - Image 4 queued for download Batch 1 - Image 5 queued for download Batch 1 - Image 6 queued for download Batch 1 - Image 7 queued for download Batch 1 - Image 8 queued for download Batch 1 - Image 9 queued for download Batch 1 - Image 10 queued for download Batch 1 - Image 6 has downloaded Batch 1 - Image 4 has downloaded Batch 1 - Image 0 has downloaded Batch 1 - Image 3 has downloaded Batch 1 - Image 7 has downloaded Batch 1 - Image 1 has downloaded Batch 1 - Image 2 has downloaded Batch 1 - Image 8 has downloaded Batch 1 - Image 9 has downloaded Batch 1 - Image 10 has downloaded Batch 1 - Image 5 has downloaded 04/03/2018 10:28:40:602 |
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). Note that the images download asynchronously.
Here’s a video of the console output from a different run of the app. It should reinforce the text version of my console output above and should reinforce my explanation of that output:
Console output from my code – downloading files and calculating
Here’s the console output from my app downloading files asynchronously — and performing a long-running calculation simultaneously:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... total + 1 = 1753075 total + 1 = 1753076 total + 1 = 1753077 total + 1 = 1753078 total + 1 = 1753079 total + 1 = 1753080 total + 1 = 1753081 Batch 1 - Image 0 has downloaded total + 1 = 1753082 total + 1 = 1753083 total + 1 = 1753084 total + 1 = 1753085 total + 1 = 1753086 ... |
Examining my app’s CPU usage
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 being on your app target device’s CPU. You can see your main thread (1) and 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 — and my processor-intensive arithmetic task — to OperationQueues
, the number of threads increased 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. If you’re patient and watch the threading details until after the images are downloaded and the calculation finishes, you’ll see the extra threads get cleaned up. This is so cool.
I’m not going into great depth here. Just note that you have another tool in your arsenal for tuning, examining, and debugging your apps.
Wrapping up
Do you feel you have a sense for Operation
and a simple demonstration of its usefulness with its subclass BlockOperation
? Remember, my purpose was not to throw you into all the complexities of full-blown
concurrency programming with Operation
. My purpose was to introduce the topic, show you that Operation
is built on the shoulders of GCD, and give you a teaser as to the many possibilities — and control — afforded to you by using Operation
instead of GCD.