Tải bản đầy đủ
Chapter 5. Closures and Operation Queues

Chapter 5. Closures and Operation Queues

Tải bản đầy đủ

In Objective-C, closures are known as blocks. For this reason, sever‐
al 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 effec‐
tively 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 anima‐
tion’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")

106

|

Chapter 5: Closures and Operation Queues

}
// 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 similar‐
ities. The operation queue system manages a pool of threads, which are activated when‐
ever 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.

Concurrency with Operation Queues

|

107

Operation Queues and NSOperation
At its simplest, an operation queue runs operations in a first-in-first-out order. Oper‐
ations 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 sub‐
classes 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()

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 expen‐
sive. 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.

108

| Chapter 5: Closures and Operation Queues

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.
}
}

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:
• Set the “Subclass Of ” to UITableViewCell.
• Set the name of the class to FaviconTableViewCell.

Putting It All Together

|

109

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 appro‐
priate favicon, and then display the image. To do this, we’ll use the NSURLConnec
tion class’s sendAsynchronousRequest 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() {

110

|

Chapter 5: Closures and Operation Queues

// 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"]

Putting It All Together

|

111

11. Next, we need an NSOperationQueue to give to the FaviconTableViewCell instan‐
ces 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: num
berOfRowsInSection) 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 the hosts 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
}

112

|

Chapter 5: Closures and Operation Queues

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 appli‐
cation quits.
15. Open AppDelegate.swift.
16. Replace applicationWillEnterBackground: with the following method:
var backgroundTask : UIBackgroundTaskIdentifier?
func applicationDidEnterBackground(application: UIApplication) {
// Register a background task. This keeps the app from being
// terminated until we tell the system that the task is complete.
self.backgroundTask =
application.beginBackgroundTaskWithExpirationHandler {
() -> Void in
// When this method is run, we're out of time.
// Clean up, and then run endBackgroundTask.
application.endBackgroundTask(self.backgroundTask!)
}
// Make a new background queue to run our background code on.
var backgroundQueue = NSOperationQueue()
backgroundQueue.addOperationWithBlock() {
// Send a request to the server.
// Prepare the URL
var notificationURL = NSURL(string: "http://www.oreilly.com/")
// Prepare the URL request
var notificationURLRequest = NSURLRequest(URL: notificationURL!)
// Send the request, and log the reply
var loadedData =
NSURLConnection.sendSynchronousRequest(
notificationURLRequest,
returningResponse: nil,
error: nil)
if let theData = loadedData {
// Convert the data to a string
var loadedString = NSString(data: theData,

Putting It All Together

|

113

encoding: NSUTF8StringEncoding)
println("Loaded: \(loadedString)")
}

// Signal that we're done working in the background
application.endBackgroundTask(self.backgroundTask!)
}
}

17. Run the application.
The application will now perform tasks when it quits.

114

|

Chapter 5: Closures and Operation Queues

CHAPTER 6

Drawing Graphics in Views

The fundamental class for showing any kind of graphical image to the user is the view.
Graphical images are things like buttons, photos, and text—anything that the user
can see.
Cocoa and UIKit provide a wide variety of controls that suit almost all needs—you can
display text, images, buttons, and more. However, some data needs to be drawn in a
specific way: you might want to draw a chart for data, or create a custom button class
that displays exactly the way you want it to. If you’re making a graphics app, you’ll need
to be able to display any kind of graphical content, which means that your code will
need to know how to draw it.
In this chapter, you’ll learn how to create custom view objects that display any kind of
image to the user. You’ll learn how to use the high-level APIs for drawing, and create a
custom view class that will scale up to any size at all without losing quality. Finally, you’ll
learn how to design your graphics to take advantage of the Retina display on iOS and
OS X hardware.

How Drawing Works
Before we start writing code that draws content for the user to see, it’s helpful to review
how graphics work in OS X and iOS. Note that the same terminology and techniques
apply for both OS X and iOS, but the specific API is different.
When an application draws graphics, it does so by first creating a canvas for drawing
in. Cocoa calls this the graphics context. The context defines, among other things, the
size of the canvas and how color information is used (e.g., you can have a black-andwhite canvas, a grayscale canvas, a 16-bit color canvas, etc.).
Once you have a graphics context to draw in, you start asking Cocoa to begin drawing
content.

115

The fundamental drawing unit is the path. A path is just the name for any kind of shape:
circles, squares, polygons, curves, and anything else you can imagine.
Paths can be stroked or filled. Stroking a path means drawing a line around its edge
(Figure 6-1). Filling a path means filling it with a color (Figure 6-2).

Figure 6-1. A stroked path

Figure 6-2. A filled path

116

|

Chapter 6: Drawing Graphics in Views