The aim of this tutorial is to give a gentle introduction to Objective-C blocks while paying special emphasis to their syntax, as well as exploring the ideas and implementation patterns that blocks make possible.
In my opinion, there are two main stumbling blocks (pun intended!) for beginners when attempting to truly understanding blocks in Objective-C:
- Blocks have a somewhat arcane and "funky" syntax. This syntax is inherited from function pointers as part of the C roots of Objective-C. If you haven't done a lot of programming in pure C, then it's likely you haven't had occasion to use function pointers, and the syntax of blocks might seem a bit intimidating.
- Blocks give rise to "programming idioms" based on the functional programming style that the typical developer with a largely imperative programming-style background is unfamiliar with.
In simpler words, blocks look weird and are used in weird ways. My hope is that after reading this article, neither of these will remain true for you!
Admittedly, it is possible to utilize blocks in the iOS SDK without an in-depth understanding of their syntax or semantics; blocks have made their way into the SDK since iOS 4, and the API of several important frameworks and classes expose methods that take blocks as parameters: Grand Central Dispatch (GCD), UIView
based animations, and enumerating an NSArray
, to name a few. All you need to do is mimic or adapt some example code and you're set.
However, without properly understanding blocks, you'll be limited in your use of them to this method-takes-block-argument pattern, and you'll only be able to use them where Apple has incorporated them in the SDK. Conversely, a better understanding of blocks will let you harness their power and it will open the door to discovering new design patterns made possible by them that you can apply in your own code.
Running the Code Samples
Since in this tutorial we'll be discussing the core concepts of blocks, which apply to recent versions of both Mac OS X and iOS, most of the tutorial code can be run from a Mac OS X command-line project.
To create a command line project, choose OS X > Application from the left-hand pane and choose the "Command Line Tool" option in the window that comes up when you create a new project.
Give your project any name. Ensure that ARC (Automatic Reference Counting) is enabled!
Most of the time code will be presented in fragments, but hopefully the context of the discussion will make it clear where the code fragment fits in, as you experiment with the ideas presented here.
The exception to this is at the end of the tutorial, where we create an interesting UIView
subclass, for which you obviously need to create an iOS project. You shouldn't have any problem writing a toy app to test out the class, but regardless, you'll be able to download sample code for this project if you'd like.
The Many Facets of Blocks
Have you ever heard of a strange animal called the platypus? Wikipedia describes it as an "egg-laying, venomous, duck-billed, beaver-tailed, otter-footed mammal". Blocks are a bit like the platypus of the Objective-C world in that they share aspects of variables, objects, and functions. Let's see how:
A "block literal" looks a lot like a function body, except for some minor differences. Here's what a block literal looks like:
^(double a, double b) // the caret represents a block literal. This block takes two double parameters. Note we don't have to explicitly specify the return type { double c = a + b; return c; // }
So, syntax-wise, what are the differences in comparison with function definitions?
- The block literal is "anonymous" (i.e. nameless)
- The caret (^) symbol
- We didn't have to specify the return type - the compiler can "infer" it. We could've explicitly mentioned it if we wanted to.
There's more, but that's what should be obvious to us so far.
Our block literal encapsulates a bunch of code. You might say this is what a function does too, and you'd be right, so in order to see what else blocks are capable of, read on!
A block pointer lets us handle and store blocks, so that we can pass around blocks to functions or have functions return blocks - stuff that we normally do with variables and objects. If you're already adept with using function pointers, you'll immediately remark that these points apply to function pointers too, which is absolutely true. You'll soon discover that blocks are like function pointers "on steroids"! If you aren't familiar with function pointers, don't worry, I'm actually not assuming you know about them. I won't go into function pointers separately because everything that can be achieved with function pointers can be achieved with blocks - and more! - so a separate discussion of function pointers would only be repetitive and confusing.
double(^g)(double, double) = ^(double a, double b) { double c = a + b; return c; };
The above statement is key and deserves a thorough examination.
The right-hand side is the block literal that we saw a moment ago.
On the left side, we've created a block pointer called g
. If we want to be pedantic, we'll say the '^' on the left signifies a block pointer, whereas the one on the right marks the block literal. The block pointer has to be given a "type", which is the same as the type of the block literal it points to. Let's represent this type as double (^) (double, double)
. Looking at the type this way, though, we should observe that the variable (f
) is "ensconced" within its type, so the declaration needs be read inside out. I'll talk a bit more about the "type" of functions and blocks a bit later.
The "pointing" is established through the assignment operator, "=".
The above line is like a typical C statement - note the semicolon in the end! We've just defined a block literal, created a block pointer to identify it, and assigned it to point to the block. In that sense, it's similar to a statement of the following type:
char ch // ch identifies a variable of type char = // that we assigned 'a' // the character literal value 'a'. ; // semicolon terminates the statement
Of course, the above statement doesn't involve pointers, so beware of this when drawing semantic comparisons.
I want to "milk" the previous comparison a bit more, just so you that you begin to see blocks in the same light as our humble char ch = 'a'
statement:
We could split the block pointer declaration and the assignment:
double (^g)(double, double); // declaration g = ^(double m, double n) { return m * n; }; // the above is like doing: // char ch; // ch = 'a';
We could reassign g
to point to something else (although this time the analogy is with a pointer or reference to an object instance):
double (^g)(double, double); g = ^(double m, double n) { return a + b; }; // .. later g = ^(double x, double y) { return x * y; }; // g reassigned to point to a new block; same type, so no problem // but not: // g = ^(int x) { return x + 1; }; // types are different! The literal on the right has type int(^)(int), not double(^) (double, double)! double (^h)(double, double) = g; // no problem here! h has the correct type and can be made to point to the same block as g // compare with: int i = 10, j = 11; // declaring a coupling of integers int *ptrToInt; // declaring a pointer to integers ptrToInt = &i; // ptrToInt points to i // later... ptrToInt = &l // ptrToInt now points to j float f = 3.14; // ptrToInt = &f; // types are different! technically can be done, but compiler will warn you of the type difference. And typically you don't want to do this! int *anotherPtrToInt; anotherPtrToInt = ptrToInt; // both pointers point to the same integer's location in memory
Another key difference between ordinary functions and blocks is that functions need to be defined in the "global scope" of the program, meaning a function can't be defined inside the body of another function!
int sum(int, int); // This is a declaration: we tell the compiler about a function sum that takes two ints and returns an int (but will be defined somewhere else). //... int main() { //... // we are *inside* main. Can't define a function here! int s = sum(5, 4); // sum function being invoked (called). This must happen inside another function! (except for main() itself) } int sum(int a, int b) // This is the function definition. It must be outside any function! { int c = a + b; return c; // we're *inside* the function body for sum(). We can't define a new function here! }
A lot of the power of blocks comes from the fact that they can be defined anywhere a variable can! Compare what we just saw with functions to what we can do with blocks:
int main() { // inside main(). Block CAN be defined here! int (^sum)(int, int) = ^(int a, int b) { return a + b; }; // the point here is that we're doing this inside the function main()! }
Again, it helps to recall the char ch = 'a'
analogy here; a variable assignment would ordinarily happen within the scope of a function. Except if we were defining a global variable, that is. Although we could do the same with blocks - but then there wouldn't be any practical difference between blocks and functions, so that's not very interesting!
So far, we've only looked at how blocks are defined and how their pointers are assigned. We haven't actually used them yet. It is important that you realise this first! In fact, if you were to type in the above code into Xcode, it would complain that the variable sum is unused.
The invocation of a block - actually using it - looks like a normal function call. We pass arguments to them through their pointers. So after the line of code we just saw, we could go:
#import <Foundation/Foundation.h> int main() { int (^sum)(int, int) = ^(int a, int b) { return a + b; }; // point here is that we're doing this inside main()! double x = 5.1, y = 7.3; double s = sum(5.1, 7.3); // block invoked. Note we're passing arguments (x,y) whose type matches the block's parameters. Return type matches assigned variable (s) as well NSLog(@"the sum of %f and %f is %f", x, y, s); }
Actually, we can do even better than that: we could define and invoke our block all in one go.
Observe the following:
#import <Foundation/Foundation.h> int main() { double x = 5.1, y = 7.3; double s = ^(double x, double y) { return x + y; }(x, y); // we're applying the arguments directly to the block literal! NSLog(@"the sum of %f and %f is %f", x, y, s); }
Perhaps the last one looked a bit like a "parlor trick" - flashy but not terribly useful - but in fact the ability to define blocks at the point of their use is one of the best things about them. If you've ever called block-accepting methods in the SDK, you've probably already encountered this use. We'll talk about this in more detail, shortly.
Let's exercise our block syntax writing skills first. Can you declare a block pointer for a block that takes a pointer to an integer as a parameter and then returns a character?
char(^b)(int *); // b can point to a block that takes an int pointer and returns a char
OK, now how about defining a block that takes no parameters, and returns no value, and encapsulates code to print "Hello, World!" to the console (upon invocation)? Assign it to a block pointer.
void (^hw)(void) = ^{NSLog(@"hello, world!"); }; // block literal could also be written as ^(void){ NSLog(@"hello, world!"); };
Note that for a block literal that takes no parameters, the parantheses are optional.
Question: Will the above line actually print anything to the console?
Answer: No. We would need to call (or invoke) the block first, like this:
hw();
Now, let's talk about functions that can take blocks as parameters or return blocks. Anything we say here applies to methods that take and return blocks, too.
Can you write the prototype of an ordinary function that takes a block of the same type as the "Hello, world" block we defined above, and returns nothing? Recall that a prototype just informs the compiler about the signature of a function whose definition it should expect to see at some later point. For example, double sum(double, double);
declares a function sum taking two double
values and returning a double
. However, it is sufficient to specify the type of the arguments and return value without giving them a name:
void func(void (^) (void));
Let's write a simple implementation (definition) for our function.
void func(void (^b) (void)) // we do need to use an identifier in the parameter list now, of course { NSLog(@"going to invoke the passed in block now."); b(); }
At the risk of repeating myself too many times, func
is an ordinary function so its definition must be outside the body of any other function (it can't be inside main(), for instance).
Can you now write a small program that invokes this function in main(), passing it our "Hello, World" printing block?
#include <Foundation/Foundation.h> void func(void (^b) void) // if the function definition appears before it is called, then no need for a separate prototype { NSLog(@"going to invoke the passed in block now."); b(); } int main() { void (^hw)(void) = ^{NSLog(@"hello, world!"); }; func(hw); // Note the type of hw matches the type of b in the function definition } // The log will show: // going to invoke the passed in block now. // hello, world!
Did we have to create a block pointer first only so we could pass the block in?
The answer is a resounding no! We could've done it inline as follows:
// code in main(): func(^{NSLog(@"goodbye, world!"); }); // inline creation of block!
Question: ff
is a function whose prototype or definition you haven't seen, but it was invoked as follows. Assume that the right kind of arguments were passed in to the function. Can you guess the prototype of ff
?
int t = 1; int g = ff(^(int *x) { return ((*x) + 1); }, t);
This is an exercise in not getting intimidated by syntax! Assuming no warnings are generated, ff
has a return type int
(because its return value is being assigned to g
, which is an int
). So we have int f(/* mystery */)
What about the parameters?
Notice that the function is invoked with an inline block which is where the scariness comes from. Let's abstract this out and represent it by "blk". Now the statement looks like int g = ff(blk, t);
Clearly, ff takes two parameters, the second one being an int (since t was an int). So we say tentatively, int ff(type_of_block, int)
where we only have to work out the type of the block. To do that, recall that knowing the block type entails knowing the types of its parameters and its return type (and that's all). Clearly, block takes one parameter of type int *
(pointer to int
). What about the return type? Let's infer it, just like the compiler would: *x
is dereferencing a pointer to an int
, so that yields an int
, adding one to which is also an int.
int *x
and the meaning of * in the expression int
value stored at the address x.
So, our block returns an int. The type_of_block
is thus int(^)(int *)
, meaning ff's prototype is:
int ff(int (^) (int *), int);
Question: Could we have passed in the "hello, world" printing block we created a while ago to ff
?
Answer: Of course not, its type was void(^)(void))
, which is different from the type of the block that ff
accepts.
I want to digress briefly and talk a bit more about something we've been using implicitly: the "type" of a block. We defined the type of a block to be determined by the types and number of its arguments and the type of its return value. Why is this a sensible definition? First, keep in mind that a block is invoked just like a function, so let's talk in terms of functions.
A C program is just a pool of functions that call each other: from the perspective of any function, all other functions in the programs are "black boxes". All that the calling function needs to concern itelf with (as far as syntactical correctness is concerned) is the number of arguments the "callee" takes, the types of these arguments, and the type of the value returned by the "callee". We could swap out one function body with another having the same type and the same number of arguments and the same return type, and then the calling function would be none the wiser. Conversely, if any of these were different, then we won't be able to substitute one function for another. This should convince you (if you needed convincing!) that our idea of what constitutes the type of a function or a block is the right one. Blocks reinforce this idea even more strongly. As we saw, we could pass an anonymous block to a function defined on the fly, as long as the types (as we defined them!) match.
We could now talk about functions that return blocks! This is even more interesting. If you think about it, essentially you're writing a function that is returning code to the caller! Since the "returned code" will be in the form of a block pointer (which would be invoked exactly as a function) we'd effectively have a function that could return different functions (blocks, actually).
Let's write a function that takes on options integer representing different types of binary operations (like addition, subtraction, multiplication, etc.) and returns a "calculator block" that you can apply to your operands in order to get the result of that operation.
Suppose we're dealing with double
operands. Our calculations also return a double
. What's the type of our binary operation block? Easy! It would be double(^) (double, double)
.
Our function (let's called it "operation_creator") takes an int
which encodes the type of operation we want it to return. So, for example, calling operation_creator(0)
would return a block capable of performing addition, operation_creator(1)
would give a subtraction block, etc. So operation_creator
's declaration looks like return-type operation_creator(int)
. We just said that return-type is double (^)(double, double)
. How do we put these two together? The syntax gets a little hairy, but don't panic:
double (^operation_creator(int)) (double, double);
Don't let this declaration get the better of you! Imagine you just came across this declaration in some code and wanted to decipher it. You could deconstruct it like this:
- The only identifier (name) is
operation_creator
. Start with that. -
operation_creator
is a function. How do we know? It's immediately followed by an opening paranthesis(
. The stuff between it and the closing paranthesis)
tells us about the number and types of parameters this function takes. There's only argument, of typeint
. - What remains is the return type. Mentally remove
operation_creator(int)
from the picture, and you're left withdouble (^) (double, double)
. This is just the type of a block that takes twodouble
values and returns adouble
. So, what our functionoperation_creator
returns is a block of this type. Again, make a mental note that if the return type is a block, then the identifier is "ensconced" in the middle of it.
Let's digress with another practice problem for you: Write the declaration of a function called blah
that takes as its only parameter a block with no parameters and returns no value, and returns a block of the same type.
void(^blah(void(^)(void)))(void);
If you had difficulty with this, let's break down the process: we want to define a function blah() that takes a block that takes no parameters and returns nothing (that is, void
), giving us blah(void(^)(void))
. The return value is also a block of type void(^)(void)
so ensconce the previous bit, starting immediately after the ^
, giving void(^blah(void(^)(void)))(void);
.
OK, now you can dissect or construct complex block and function declarations, but all these brackets and void
s are probably making your eyes water! Directly writing out (and making sense of) these complex types and declarations is unwieldy, error prone, and involves more mental overhead than it should!
It's time to talk about using the typedef
statement, which (as you hopefully know), is a C construct that lets you hide complex types behind a name! For example:
typedef int ** IntPtrPtr;
Gives the name IntPtrPtr
to the type "pointer to pointer to int
". Now you can substitute int **
anywhere in code (such as a declaration, a type cast, etc.) with IntPtrPtr
.
Let's define a type name for the block type int(^)(int, int)
. You can do it like this:
typedef int (^BlockTypeThatTakesTwoIntsAndReturnsInt) (int, int);
This says that BlockTypeThatTakesTwoIntsAndReturnsInt
is equivalent to int(^)(int, int)
, that is, a block of the type that takes two int
values and returns an int
value.
Again, notice that the identifier (BlockTypeThatTakesTwoIntsAndReturnsInt) in the above statement, which represents the name we want to give the type we're defining, is wrapped up between the details of the type being typedef
'd.
How do we apply this idea to the blah
function we just declared? The type for the parameter and the return type is the same: void(^)(void)
, so let's typedef this as follows:
typedef void(^VVBlockType)(void);
Now rewrite the declaration of blah
simply as:
VVBlockType blah(VVBlockType);
There you go, much nicer! Right?
At this point, it's very important that you are able to distinguish between the following two statements:
typedef double (^BlockType)(double); // (1) double(^blkptr)(double); // (2)
They look the same, barring the typedef
keyword, but they mean very different things.
With (2), you've declared a block pointer variable that can point at blocks of type double(^)(double)
.
With (1), you've defined a type by the name BlockType
that can stand in as the type double (^)double
. So, after (2), you could do something like: blkptr = ^(double x){ return 2 * x; };
. And after (1), you could've actually written (2) as BlockType blkptr;
(!)
Do not proceed unless you've understood this distinction perfectly!
Let's go back to our operation
function. Can you write a definition for it? Let's typedef
the block type out:
typedef double(^BinaryOpBlock_t)(double, double); BinaryOpBlock_t operation_creator(int op) { if (op == 0) return ^(double x, double y) { return x + y; }; // addition block if (op == 1) return ^(double x, double y) { return x * y; }; // multiplication block // ... etc. } int main() { BinaryOpBlock_t sum = operation_creator(0); // option '0' represents addition NSLog(@"sum of 5.5 and 1.3 is %f", sum(5.5, 1.3)); NSLog(@"product of 3.3 and 1.0 is %f", operation_creator(1)(3.3, 1.0)); // cool, we've composed the function invocation and the block invocation! }
enum
type) to represent the options integer that our function accepts.
Another example. Define a function that takes an array of integers, an integer representing the size of the array, and a block that takes an int
and returns an int
. The job of the function will be to apply the block (which we imagine represents a mathematical formula) on each value in the array.
We want the type of our block to be int(^)(int)
. Let's typedef
and then define our function:
typedef int(^iiblock_t)(int); void func(int arr[], int size, iiblock_t formula) { for ( int i = 0; i < size; i++ ) { arr[i] = formula(arr[i]); } }
Let's use this in a program:
int main() { int a[] = {10, 20, 30, 40, 50, 60}; func(a, 6, ^(int x) { return x * 2; }); // after this function call, a will be {20, 40, 60, 80, 100, 120} }
Isn't that cool? We were able to express our mathematical formula right where we needed it!
Add the following statements after the function call in the previous code:
// place the following lines after func(a, 6, ^(int x) { return x * 2; }); int n = 10; // n declared and assigned func(a, 6, ^(int x) { return x - n; } ); // n used in the block! } // closing braces of main
Did you see what we did there? We used the variable n
which was in our block's lexical scope in the body of the block literal! This is another a tremendously useful feature of blocks (although in this trivial example it might not be obvious how, but let's defer that discussion).
A block can "capture" variables that appear in the lexical scope of the statement calling the block. This is actually a read-only capture, so we couldn't modify n
within the block's body. The value of the variable is actually copied by the block at the time of its creation. Effectively, this means if we were to change this variable at some point after the creation of the block literal but before the block's invocation, then the block would still use the "original" value of the variable, that is, the value held by the variable at the time of the block's creation. Here's a simple example of what I mean, based on the previous code:
int n = 5; iiblock_t b = ^(int r) { return r * n; }; // created block, but haven't invoked it yet // .. stuff n = 1000; // we've the value of n, but that won't affect the block which was defned previously func(a, 6, b); // after this block gets invoked, each element in the array is multiplied by 5, not 1000!
So, how is this ability of blocks useful? In high-level terms, it allows us to use "contextual information" in an inline block from the scope where the block is defined. It might not make sense to pass this information as a parameter to the block as it is only important in certain contexts, yet in those particular situations we do want to utilize this information in the implementation of our block.
Let's make this idea concrete with an example. Suppose you're working on an educational app about the countries of the world, and as part of the app you want to be able to rank (i.e. sort) countries with respect to different metrics, such as population, natural resources, GDP, or any complex formula you come up with combining data you have about these countries. There are different sorting algorithms available, but most of them work on the principle of being able to compare two entities and decide which one is greater or smaller.
Let's say you come up with the following helper method in one of your classes:
-(NSArray *) sortCountriesList:(NSArray *)listOfCountries withComparisonBlock: BOOL(^cmp) (Country *country1, Country *country2) { // Implementation of some sorting algorithm that will make several passes through listOfCountries // whatever the algorithm, it will perform several comparisons in each pass and do something based on the result of the comparison BOOL isGreater = comp(countryA, countryB); // block invoked, result is YES if countryA is "greater" than countryB based on the passed in block if (isGreater) // do something, such as swapping the countries in the array // ... }
Note that before this example we only talked about blocks taken or returned by functions, but it's pretty much the same with Objective-C methods as far as the block syntax and invocation is concerned.
Again, you should appreciate that the ability to "plug in code" to our method in the form of a comparison block gives us the power to sort the countries according to some formula we can specify on-the-spot. All well and good. Now suppose you realise it would also be great if we could also rank the countries from lowest rank to highest. This is how you could achieve this with blocks.
// Inside the body of some method belonging to the same class as the previous method bool sortInAscendingOrder = YES; // calling our sorting method: NSArray *sortedList = [self sortCountriesList:list withComparisonBlock:^(Country *c1, Country *c2) { if (c1.gdp > c2.gdp ) // comparing gdp for instance { if (sortInAscendingOrder) return YES; else return NO; } }];
And that's it! We used the flag sortInAscendingOrder
that carried contextual information about how we wanted the sort to be carried out. Because this variable was in the lexical scope of the block declaration, we were able to use its value within the block and not have to worry about the value changing before the block completes. We did not have to touch our -sortCountriesList: withComparisonBlock:
to add a bool
parameter to it, or touch its implementation at all! If we'd used ordinary functions, we would be writing and rewriting code all over the place!
Let's end this tutorial by applying all we've learned here with a cool iOS application of blocks!
If you've ever had to do any custom drawing in a UIView object, you know you have to subclass it and override its drawRect:
method where you write the drawing code. You probably find it to be a chore, create an entire subclass even if all you're wanting to do is draw a simple line, plus having to look into the implementation of drawRect:
whenever you need to be reminded of what the view draws. What a bore!
Why can't we do something like this instead:
[view drawWithBlock:^(/* parameters list */){ // drawing code for a line, or whatever }];
Well, with blocks, you can! Here, view
is an instance of our custom UIView
subclass endowed with the power of drawing with blocks. Note that while we're still having to subclass UIView
, we only have to do it once!
Let's plan ahead first. We'll call our subclass BDView
(BD for "block drawing"). Since drawing happens in drawRect:
, we want BDView
's drawRect:
to invoke our drawing block!. Two things to think of: (1) how does BDView
hold on to the block? (2) What would be the block's type?
Remember I mentioned way at the beginning that blocks are also like objects? Well, that means you can declare block properties!
@property (nonatomic, copy) drawingblock_t drawingBlock;
We haven't worked out what the block type should be, but after we do we'll typedef
it as drawingblock_t
!
Why have we used copy
for the storage semantics? Truthfully, in this introductory tutorial we haven't talked about what happens behind the scenes, memory-wise, when we create a block or return a block from a function. Luckily, ARC will do the right thing for us under most circumstances, saving us from having to worry about memory maangement, so for our purposes now it is enough to keep in mind that we want to use copy
so that a block gets moved to the heap and doesn't disappear once the scope in which it was created ends.
What about the type of the block? Recall that the drawRect:
method takes a CGRect
parameter that defines the area in which to draw, so our block should take that as a parameter. What else? Well, drawing requires the existence of a graphics context, and one is made available to us in drawRect:
as the "current graphics context". If we draw with UIKit classes such as UIBezierPath
, then those draw into the current graphics context implicitly. But if we choose to draw with the C-based Core Graphics API, then we need to pass in references to the graphics context to the drawing functions. Therefore, to be flexible and allow the caller to use Core Graphics functions, our block should also take a parameter of type CGContextRef
which in BDView
's drawRect:
implementation we pass in the current graphics context.
We're not interested in returning anything from our block. All it does is draw. Therefore, we can typedef
our drawing block's type as:
typedef void (^drawingblock_t)(CGContextRef, CGRect);
Now, in our -drawWithBlock:
method we'll set our drawingBlock
property to the passed in block and call the UIView
method setNeedsDisplay
which will trigger drawRect:
. In drawRect:
, we'll invoke our drawing block, passing it the current graphics context (returned by the UIGraphicsGetCurrentContext()
function) and the drawing rectangle as parameters. Here's the complete implementation:
//BDView.h #import <UIKit/UIKit.h> typedef void (^drawingblock_t)(CGContextRef, CGRect); @interface BDView : UIView - (void)drawWithBlock:(drawingblock_t) blk; @end
// BDView.m #import "BDView.h" @interface BDView () @property (nonatomic, copy) drawingblock_t drawingBlock; @end @implementation BDView - (void)drawWithBlock:(drawingblock_t)blk { self.drawingBlock = blk; // copy semantics [self setNeedsDisplay]; } - (void)drawRect:(CGRect)rect { if (self.drawingBlock) { self.drawingBlock(UIGraphicsGetCurrentContext(), rect); } } @end
So, if we had a view controller whose view
property was an instance of BDView
, we could call the following method from viewDidLoad
(say) to draw a line that ran diagonally across the screen from the top-left corner to the bottom right corner:
- (void)viewDidLoad { [super viewDidLoad]; BDView *view = (BDView *)self.view; [view drawWithBlock:^(CGContextRef context, CGRect rect){ CGContextMoveToPoint(context, rect.origin.x, rect.origin.y); CGContextAddLineToPoint(context, rect.origin.x + rect.size.width, rect.origin.y + rect.size.height); CGContextStrokePath(context); }]; }
Brilliant!
More to Learn
In this introductory tutorial on blocks, we've admittedly left out a few things. I'll mention a couple of these here, so you can look into them in more advanced articles and references:
- We haven't talked much about memory semantics of blocks. Although automatic reference counting relieves us of a lot of burden in this regard, in certain scenarios more understanding is required.
- The
__block
specifier that allows a block to modify a variable in its lexical scope.
Conclusion
In this tutorial, we had a leisurely introduction to blocks where we paid special attention to the syntax of block declaration, assignment, and invocation. We tried to leverage what most developers already know from C regarding variables, pointers, objects, and functions. We ended with an interesting practical application of blocks that should get the wheels in your head turning and get you excited about using blocks effectively in your own code. Proper use of blocks can improve code understandability and sometimes offer very elegant solutions to problems.
I also recommend you take a look at BlocksKit, which includes many interesting utilities of blocks that can make your life as an iOS developer easier. Happy coding!
Comments