Closures and Operation Queues - Swift Development with Cocoa (2015)

Swift Development with Cocoa (2015)

Chapter 5. Closures and Operation Queues

The Swift language allows you to store code in variables. When you do this, the code is known as a closure, and Cocoa provides a number of tools that allow you to work with closures. One of the most important tools is called an operation queue, which lets you schedule closures to run in the future.

Closures allow code to do some very interesting things, including the following:

§ Provide code to a function that should be run when that function is complete, such as a download or a long-running computation

§ Enumerate over a collection of objects, and run some code on each one

§ Provide code for objects to call when they feel it’s necessary (e.g., event handlers)

Closures are discussed in detail in Functions and Closures. From a language perspective, they’re identical to functions—they take parameters and return values. From a code-writing perspective, the only real difference between functions and closures is that closures don’t have parameter names, while functions do.

To quickly refresh your memory, this is what closures look like:

let aClosure : Void -> Int = { return 1 }

aClosure() // returns 1

Because code can be divided up into chunks using closures, you can also give closures to the system for it to run. The mechanism that you use to do this is called operation queues, and they’re tremendously powerful tools for managing how the system does the work that you need to do.

NOTE

In Objective-C, closures are known as blocks. For this reason, several methods that belong to Cocoa and Cocoa Touch contain the word block, where you should provide a closure.

For example, the NSOperationQueue class has a method called addOperationWithBlock, which takes a single closure parameter. Just remember to keep in mind that the terms block and closure are effectively identical.

In this chapter, you’ll learn how to use closures effectively in your apps, what they’re good for, and how to use them in conjunction with operation queues, a powerful tool for performing tasks in the background.

Closures in Cocoa

Many classes in Cocoa have methods that make use of closures. These are typically methods that use a closure as a completion handler—that is, the method will perform some action that might take a moment (e.g., an animation), and once that action is complete, the completion handler closure is called:

// In this code, aViewController and anotherViewController

// are both UIViewControllers.

// Slide up a view controller, and then when the slide animation is

// finished, change its view's background color to yellow.

aViewController.presentViewController(anotherViewController, animated: true) {

// This closure is run after the animation is finished

anotherViewController.view.backgroundColor = UIColor.yellowColor()

}

Closures allow you to defer the execution of something until you need it to actually happen. This makes them very useful in the context of animations (“when the animation’s done, do this thing”), for networking (“when the download’s done, do something else”), or in general user interface manipulation (“when I return from this new screen, do some work”).

They also allow you to keep related pieces of code close together. For example, before the introduction of closures, the only way that it was possible to filter an array was to create a function elsewhere in your code that was called for each element in the array. This made for a lot of scrolling around your source code. Now you can do this:

// Filter an array of strings down to only strings that begin with the word

// "Apple"

let array = ["Orange", "Apple", "Apple Juice"]

let filteredArray = array.filter() {

return $0.hasPrefix("Apple")

}

// filteredArray now contains "Apple", "Apple Juice"

In this case, the code that actually performs the processing of the objects is very close to the line that instructs the array to be filtered. This means that your code isn’t scattered in as many places, which makes it clearer and less confusing. The less confusing your code is, the less likely it is that bugs will be introduced.

Concurrency with Operation Queues

In many cases, your application will need to do more than one thing at the same time. At least one of those things is responding to the user and ensuring that the user interface is responsive; other things could include talking to the network, reading and writing large amounts of data, or processing a chunk of data.

The highest priority of your application is to be responsive at all times. Users are willing to wait a few seconds for a task to complete, as long as they get feedback that work is happening. When the application stops responding to input, users perceive that the application, and by extension the device it’s running on, is slow.

The second priority of your application is to make sure that all of the resources available are being used, so that the task completes quickly.

Operation queues allow you to achieve both goals. Operation queues are instances of the NSOperationQueue class. They manage a list, or queue, of operations, which are closures containing code to perform a chunk of work.

More than one operation queue can exist at the same time, and there is always at least one operation queue, known as the main queue. So far, all the code in this book has been run on the main queue. All work that is done with the GUI is done on the main queue, and if your code takes up too much time in processing something, the main queue slows down and your GUI starts to lag or freeze up.

Operation queues are not quite the same thing as threads, but they share some similarities. The operation queue system manages a pool of threads, which are activated whenever work needs to be done. Because threads are a rather resource-intensive way of doing work concurrently (to say nothing of the development complexity involved in managing them properly), operation queues provide a much simpler and more efficient way of dealing with them.

Operation queues are also aware of the computing resources available on whatever hardware your application is running on. If you create an operation queue and add operations to it, the operation queue will attempt to balance those operations across as many CPU cores as are available on your computer. Every single device that Apple ships now has two or more CPU cores, which means that code that uses operation queues automatically gains an increase in speed when performing concurrent work.

Operation Queues and NSOperation

At its simplest, an operation queue runs operations in a first-in-first-out order. Operations are instances of the NSOperation class, which define exactly how the work will be done. NSOperations can be added to an NSOperationQueue; once they are added, they will perform whatever task they have been designed to do.

The simplest way to add an operation to an operation queue is to provide a closure to the queue by sending the addOperationWithBlock message to an NSOperationQueue object:

var mainQueue = NSOperationQueue.mainQueue()

mainQueue.addOperationWithBlock() {

// Add code here

}

There are other kinds of operations, including invocation operations and concrete subclasses of the NSOperation base class, but they’re very similar to closure operations—they offer more flexibility and features at the cost of having to write more setup code.

If you don’t deliberately choose to run code on another queue, it will run on the main queue. You can also explicitly instruct the main queue to perform an operation; when you do this, the work for this operation is scheduled to take place at some point in the future.

Performing Work on Operation Queues

To add things to an operation queue, you need an NSOperationQueue instance. You can either ask the system for the main queue, or you can create your own. If you create your own queue, it will run asynchronously. If you add multiple operations to a background queue, the operation queue will run as many as possible at the same time, depending on the available computing hardware:

// Getting the main queue (will run on the main thread)

var mainQueue = NSOperationQueue.mainQueue()

// Creating a new queue (will run on a background thread, probably)

var backgroundQueue = NSOperationQueue()

NOTE

Queues aren’t the same as threads, and creating a new queue doesn’t guarantee that you’ll create a new thread—the operating system will reuse an existing thread if it can, because creating threads is expensive. The only thing using multiple queues guarantees is that the operations running on them won’t block each other from running at the same time.

Once you have a queue, you can put an operation on it:

mainQueue.addOperationWithBlock() {

println("This operation ran on the main queue!")

}

backgroundQueue.addOperationWithBlock() {

println("This operation ran on another queue!")

}

If your code is running on a background queue and you want to update the GUI, you need to run the GUI updating code on the main queue. One way to do this is to add a closure to the main queue:

backgroundQueue.addOperationWithBlock() {

// Do some work in the background

println("I'm on the background queue")

// Schedule a block on the main queue

mainQueue.addOperationWithBlock() {

println("I'm on the main queue")

// GUI work can safely be done here.

}

}

WARNING

Any work involving the GUI can only be done from the main queue. If you access it from any other queue, your application will crash.

Putting It All Together

We’ll now write an application that downloads the favicons from a number of websites asynchronously. To do this, it will create a table view that has a list of websites, and each cell in the table view will show both the website’s name as well as download its favicon. It will also contact a server when the application exits.

We’ll need to create a subclass of UITableViewCell that has a NSURL property, and uses operation queues to download the favicon whenever that property changes.

The steps to create the new app are as follows:

1. Create a new, single view iOS application and call it OperationQueues.

2. Create a new Cocoa Touch subclass:

o Set the “Subclass Of” to UITableViewCell.

o Set the name of the class to FaviconTableViewCell.

3. Because this table view cell downloads in the background, we should give it an NSOperationQueue that it should use to manage the download. This means that we need to give it a property to store it in.

Add the following code to FaviconTableViewCell.swift:

// The operation queue to run the download's completion handler

var operationQueue : NSOperationQueue?

4. The next step is to implement the property that stores the URL that the cell is showing. When this property is changed, we want the cell to download the appropriate favicon, and then display the image. To do this, we’ll use the NSURLConnection class’ssendAsynchronousRequest method, which takes an NSURLRequest, an NSOperationQueue, and a closure; it then performs the download specified in the NSURLRequest, and runs the provided closure on the specified NSOperationQueue.

The closure itself takes three parameters: an NSURLResponse, which describes the response that the server sent back; an NSData, which contains the data that was delivered (if any); and an NSError, which contains information about any problem that the download might have encountered. Potential problems include issues with reaching the server (such as the user being in airplane mode), as well as problems on the server’s end (such as the file not being found).

Add the following code to FaviconTableViewCell:

// The URL that this cell shows.

var url : NSURL? {

// When the URL changes, run this code.

didSet {

// We've just been given a URL, so create a request

var request = NSURLRequest(URL: self.url!)

// Display this text

self.textLabel.text = self.url?.host

// Fire off the request, and give it a completion handler

// plus a queue to run on

NSURLConnection.sendAsynchronousRequest(request,

queue: self.operationQueue!,

completionHandler: {

(response: NSURLResponse!, data: NSData!, error: NSError!) in

// The 'data' variable now contains the loaded data;

// turn it into an image

var image = UIImage(data: data)

// Updates to the UI have to be done on the main queue.

NSOperationQueue.mainQueue().addOperationWithBlock() {

// Give the image view the loaded image

self.imageView.image = image

// The image view has probably changed size because of

// the new image, so we need to re-layout the cell.

self.setNeedsLayout()

}

})

}

}

We’re now done setting up the table view cell. It’s time to make a table view that uses this cell! Here are the steps to accomplish this:

1. Add a table view to the view controller.

2. Add a prototype cell to the table view by selecting it and changing the Prototype Cells number from 0 to 1.

3. Change the table view’s new prototype cell style to Basic.

4. Select the prototype cell and change its identifier to FaviconCell.

5. Switch to the Identity Inspector for the table view cell, and change its class to FaviconTableViewCell.

6. Change the table’s Selection style to No Selection.

7. Make the view controller the table view’s data source and delegate by holding the Control key, dragging from the table view to the View Controller, and choosing dataSource.

8. Repeat the process and choose delegate.

9. Open ViewController.swift in the assistant.

10.The first thing to do is to define the list of websites that the app should show. We’ll store this an an array of strings.

Create the hosts property, by adding the following code:

let hosts = ["google.com", "apple.com", "secretlab.com.au",

"oreilly.com", "yahoo.com", "twitter.com", "facebook.com"]

11.Next, we need an NSOperationQueue to give to the FaviconTableViewCell instances that this class will be displaying.

Create the queue property, by adding the following code:

let queue = NSOperationQueue()

12.As with all table views, we need to provide information about how many sections it has, and how many rows are in each section. In this application, there is only one section, and the number of rows in that section is equal to the number of websites in the hosts array.

Implement the numberOfSectionsInTableView and tableView(tableView: numberOfRowsInSection) methods:

func numberOfSectionsInTableView(tableView: UITableView!) -> Int {

return 1

}

func tableView(tableView: UITableView,

numberOfRowsInSection section: Int) -> Int {

return hosts.count

}

13.The last thing to do is to make the table view use the FaviconTableViewCell class for its table view cells, and for each cell to use the right website. It does this by using the table view’s dequeueReusableCellWithIdentifier method to retrieve the cell, and then uses thehosts array to create an NSURL to give it.

Implement the tableView(tableView: cellForRowAtIndexPath:) method:

func tableView(tableView: UITableView,

cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

var cell = tableView.dequeueReusableCellWithIdentifier("FaviconCell")

as FaviconTableViewCell

var host = hosts[indexPath.row]

var url = NSURL(string: "http://\(host)/favicon.ico")

cell.operationQueue = queue

cell.url = url

return cell

}

14.Run the application.

The app will start up, and you’ll see the list of websites; after a moment, website icons will start appearing next to them.

Finally, we’ll make the application run some code in the background when the application quits.

15.Open AppDelegate.swift.

16.Replace applicationWillEnterBackground: with the following method:

17. var backgroundTask : UIBackgroundTaskIdentifier?

18.

19. func applicationDidEnterBackground(application: UIApplication) {

20.

21. // Register a background task. This keeps the app from being

22. // terminated until we tell the system that the task is complete.

23.

24. self.backgroundTask =

25. application.beginBackgroundTaskWithExpirationHandler {

26. () -> Void in

27.

28. // When this method is run, we're out of time.

29. // Clean up, and then run endBackgroundTask.

30.

31. application.endBackgroundTask(self.backgroundTask!)

32.

33. }

34.

35. // Make a new background queue to run our background code on.

36. var backgroundQueue = NSOperationQueue()

37.

38. backgroundQueue.addOperationWithBlock() {

39. // Send a request to the server.

40.

41. // Prepare the URL

42. var notificationURL = NSURL(string: "http://www.oreilly.com/")

43.

44. // Prepare the URL request

45. var notificationURLRequest = NSURLRequest(URL: notificationURL!)

46.

47. // Send the request, and log the reply

48. var loadedData =

49. NSURLConnection.sendSynchronousRequest(

50. notificationURLRequest,

51. returningResponse: nil,

52. error: nil)

53.

54. if let theData = loadedData {

55. // Convert the data to a string

56. var loadedString = NSString(data: theData,

57. encoding: NSUTF8StringEncoding)

58.

59. println("Loaded: \(loadedString)")

60.

61. }

62.

63.

64. // Signal that we're done working in the background

65. application.endBackgroundTask(self.backgroundTask!)

66. }

67.

}

68.Run the application.

The application will now perform tasks when it quits.