Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online
Authors: Aaron Hillegass,Joe Conway
Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming
Say you wanted an object that could take two numbers, add them together, and then return the result. You could probably write that pretty easily. But what if you now wanted that object to perform subtraction? Then multiplication? Division? You’d end up writing a number of methods for this object.
Instead of writing these methods, we can give this object an instance variable that points at a block. When we want to swap out the operation this object uses, we can construct a block literal that performs the appropriate operation. When the object is asked to compute the result, it executes the operation in the block.
Create a new
NSObject
subclass for
Blocky
and name it
BNRExecutor
. In
BNRExecutor.h
, add an instance variable and two methods to the
BNRExecutor
class.
The
setEquation:
method takes a block as an argument. Notice the syntax for declaring that a method takes a block argument. The argument has nearly the same format as a block variable. The only difference is the argument name (
block
) is separated from the argument’s type. Instead of following the caret, the argument name comes after the parentheses that declare the argument type (just like any other method argument).
Figure 27.2 BNRExecutor and its block
What do these methods do? Sending the message
setEquation:
to an instance of
BNRExecutor
gives it a reference to a block (
Figure 27.2
). Sending the message
computeWithValue:andValue:
will execute that block – passing in the two
int
arguments – and return the result. In
BNRExecutor.m
, define these two methods.
In
BNRAppDelegate.m
, import
BNRExecutor.h
, create an instance of
BNRExecutor
, set its
equation
, and then have it compute a value with that equation.
Build and run the application. You will see the same output as before.
Take a look at the message
computeWithValue:andValue:
. It takes two integer arguments, which means we can pass variables of type
int
:
Or we could bypass creating the variables and just pass literal values:
The same can be done when passing a block to a method. Instead of allocating a block, setting a variable to point at it, and then passing that variable to a method, you can just pass a block literal as the argument. (This is what we did back in
Chapter 13
when we passed a block to be executed when the modal view controller was dismissed.) In
application:didFinishLaunchingWithOptions:
, modify the code so that the variable
adder
is no longer used.
Build and run the application. You will see the same output, but your code is much more succinct. The block is allocated and immediately passed to the
BNRExecutor
, which sets its
equation
instance variable to point at it. Now you can see exactly what the block does (what code it contains) right where it’s called rather than having to find the definition back where the variable is declared. This makes for much less clutter in your code files.
Like any other instance variable, a block can be exposed as a property of a class. In
BNRExecutor.h
, declare a property for the
equation
block to replace the setter method. Since you will be synthesizing this property, you no longer need the instance variable explicitly declared either.
Synthesize this property in
BNRExecutor.m
and remove the previous implementation of
setEquation:
.
Build the application to check for syntax errors.
There is one special characteristic of blocks that makes them very useful: they
capture
variables.
A block, like a function, can use the arguments passed to it and can declare local variables. Modify the block in
BNRAppDelegate.m
’s
application:didFinishLaunchingWithOptions:
so that it declares and uses a local variable.
The variable
sum
is local to the block, so
sum
can be used inside this block. Both
x
and
y
are arguments of the block, so they, too, can be used inside this block.
A block can also use any variables that are visible within its
enclosing scope
. The enclosing scope of a block is the scope of the method in which it is defined. Thus, a block has access to all of the local variables of the method, arguments passed to the method, and instance variables that belong to the object running the method. Change
application:didFinishLaunchingWithOptions:
to declare a local variable and then use that variable in the block.
Build and run the application. The console will report the result of the equation as
21
. When a block accesses a variable that is declared outside of it, the block is said to
capture
that variable. Thus, the value of
multiplier
is copied into the memory for the block (
Figure 27.3
). Each time the block executes, that value will be used, regardless of what happens to the original
multiplier
variable. (The original local variable will be destroyed as soon as
application:didFinishLaunchingWithOptions:
returns.)
Figure 27.3 Captured variables are copied into the block
In fact, you can change the
multiplier
variable after the block is created, and it still won’t change the multiplier within the block. When a block copies the value of a variable, it doesn’t care what happens to the original variable afterwards. In
BNRAppDelegate.m
, change the value of
multiplier
after creating the block.
Build and run the application. Notice that the console reports the same output from the equation, even though the block is called after
multiplier
is changed. You don’t need to do anything special to capture a variable ߝ you simply must use it somewhere in the block.
This behavior is unique to blocks. The only way you can get values to functions is to pass them as arguments, and the only way you can get values to methods is to pass them as arguments or have them stored in an instance variable. A block can take arguments and capture variables from their enclosing scope.
This is an important point: the values of the captured variables remain the same every time the block is called, but the values of its arguments can change each time it is called. In this case, that means you can change the two values that are added together each time you call the block, but the multiplier will always remain the same.
You can capture any kind of variable, including a pointer to an object. This is useful because you can send a message to an object that you have a pointer to in the enclosing scope. Let’s try this out.
In every iOS application, there is a
main operation queue
. When you add an operation to this queue, it is essentially queued up as the next event in the run loop (
Figure 27.4
).
Figure 27.4 NSOperationQueue
A block can be an operation, so you can add a block to the main operation queue, and it will execute on the next cycle of the run loop. A block that can be added to the operation queue must return no value and take no arguments. (You can find more information in the documentation for
NSOperationQueue
).
In
BNRAppDelegate.m
, edit
application:didFinishLaunchingWithOptions:
to add a block to the main operation queue that will send
computeWithValue:andValue:
to the
equation
when executed.
Build and run the application. Notice the console reports it is exiting
application:didFinishLaunchingWithOptions:
, and shortly thereafter, you see the result of the
executor
running its equation. Using
addOperationWithBlock:
on the main queue is pretty common. Many times, you will want the run loop to finish drawing views and clearing the autorelease pool before a block is executed.
This makes sense except for one small thing: the variable,
executor
, is the only variable that points to the
BNRExecutor
instance. When
application:didFinishLaunchingWithOptions:
finishes, the block has yet to run, but the
executor
variable is destroyed – which should cause the
BNRExecutor
to also be destroyed. However, the
BNRExecutor
is clearly not destroyed because the block, which sends a message to the executor, runs without a hitch. (If
BNRExecutor
was already destroyed when the block executed, you’d get a crash.)
What, then, keeps the
BNRExecutor
alive? A block keeps a strong reference to any object it references within its scope. Thus, any time you send a message to an object or use that object in any way within the block, it will be kept around as long as the block exists. In the case of
addOperationWithBlock:
, the block is kept until it is executed, and then it is discarded. We can test this out. In
BNRExecutor.m
, implement
dealloc
to print out when the executor is destroyed.
Build and run the application. Notice that the
BNRExecutor
is destroyed after the block is executed. The block held the last strong reference to it and when the block was destroyed, it lost the last reference to the object. This works because the block captures the variable that points to the
BNRExecutor
in the same way it captures any other variable. The compiler is very smart: it figures out that this is a pointer to an object and gives the block ownership of that object.