[Download Xcode project with all source code from GitHub to follow along.]
Today, we’ll finish our discussion of the benefits of using Objective-C blocks and Swift closures by writing code to define and use a closure in Swift 3. For the full background on this topic, please read my last post entitled “Make blocks (closures) your friend (Objective-C and Swift 3).” Let’s plunge into Swift 3:
1) Let’s define a type (typealias) for the closure. We want to be able to easily pass the closure as a function argument:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// MARK: - Define closure /** Define a closure TYPE for updating a UIImageView once an image downloads. - parameter imageView: UKit construct in which to display the image - parameter imageData: raw NSData making up the image - parameter success: YES if the image was downloaded and displayed; NO otherwise - parameter message: message reporting the outcome of downloading and displaying the image */ public typealias ImageDownloadCompletionClosure = (_ imageView: UIImageView, _ imageData: NSData, _ success: Bool, _ message: String ) -> Void |
2) Now we define the closure, encoding what we want it to do, and pass it as an argument to a method:
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 |
// MARK: - Concurrent, asynchronous image downloads /** Specify the completion handler/callback to be executed when finished downloading each image in a set of very large images. We do the downloading in the background (concurrently and asynchronously). */ func prepareForAsyncImageDownloads() -> Void { // Define a closure (completion block) INSTANCE for updating a UIImageView // once an image downloads. let completionClosure = { (imageView: UIImageView?, imageData: NSData, success: Bool, message: String ) -> Void in if success { imageView?.image = UIImage(data: imageData as Data) self.imageCounter += 1 self.progressView.progress = Float(self.imageCounter) / Float(self.imageURLs.count) self.view.setNeedsDisplay() print(message) } else { imageView?.image = nil print(message) } } // end let completionClosure... // start download process; each download task will call back/communicate // that it's finished by executing the completionHanlder paramter performAsyncImageDownloadsWithCompletionBlock(completionHanlder: completionClosure) } // end prepareForAsyncImageDownloads |
3) When our background task for downloading an image completes, it calls our closure (completion handler) to inform the caller of success or failure and updates the user interface (UI) on the main thread:
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 |
/** Download a set of images specified by a list of URLs to those images. Spin each image download task off asynchronously on a concurrent queue (i.e., in the background). When each image finishes downloading, call a closure which updates the user interface to display the latest image. - parameter completionHanlder: A closure to execute on the MAIN THREAD to update the UI with the latest image */ func performAsyncImageDownloadsWithCompletionBlock(completionHanlder: @escaping ImageDownloadCompletionClosure) { // 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 { // 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 Closure // I'm PURPOSEFULLY downloading the image using a synchronous call // (NSData), but I'm doing so in the BACKGROUND. let imageView : UIImageView = self.view.viewWithTag(i) as! UIImageView let imageURL = URL(string: self.imageURLs[i-10]) let imageData = NSData(contentsOf: imageURL!) print("image tag: \(i)") // Once the image finishes downloading, I jump onto the MAIN // THREAD TO UPDATE THE UI. DispatchQueue.main.async { // if an image was successfully downloaded... if let imageData = imageData { // this CLOSURE updates the UI completionHanlder(imageView, imageData, true, "Image download succeeded.") } else { // this CLOSURE updates the UI completionHanlder(imageView, imageData!, false, "Image download failed.") } } // end DispatchQueue.main.async } // end DispatchQueue.global } // end for i in imageViewTags } // end performAsyncImageDownloadsWithCompletionBlock |
Note that the “completionHanlder” argument is marked as “@escaping.” Apple defines an “escaping closure” thusly:
A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.
One way that a closure can escape is by being stored in a variable that is defined outside the function. As an example, many functions that start an asynchronous operation take a closure argument as a completion handler. The function returns after it starts the operation, but the closure isn’t called until the operation is completed—the closure needs to escape, to be called later.
So there you have it. We’ve learned about, formally defined, reviewed some code for, and written some code for blocks in Objective-C, and written some code for closures in Swift. Remember that blocks are one of the most important programming language constructs you’ll ever learn about. Please leave a comment if you have questions or feedback.
[Download Xcode project with all source code from GitHub to follow along.]