Movatterモバイル変換


[0]ホーム

URL:


Advertisement
  1. Code
  2. Mobile Development
  3. iOS Development

Understanding Objective-C Blocks

Scroll to top
Advertisement
Advertisement
Advertisement
Advertisement
Advertisement
Akiel Khan
29 min read

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:

  1. 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.
  2. 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, itis 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 anNSArray, 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, chooseOS 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.

newprojectnewprojectnewproject

Give your project any name. Ensure that ARC (Automatic Reference Counting) is enabled!

ARCARCARC

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 interestingUIView 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:

1
2
^(doublea,doubleb)// the caret represents a block literal. This block takes two double parameters. Note we don't have to explicitly specify the return type
3
{
4
doublec=a+b;
5
returnc;//
6
}

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.

1
2
double(^g)(double,double)=^(doublea,doubleb)
3
{
4
doublec=a+b;returnc;
5
};

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 ablock pointer calledg. 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 asdouble (^) (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:

1
2
charch// ch identifies a variable of type char
3
=// that we assigned
4
'a'// the character literal value 'a'.
5
;// 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 humblechar ch = 'a' statement:

We could split the block pointer declaration and the assignment:

1
2
double(^g)(double,double);// declaration
3
g=^(doublem,doublen){returnm*n;};
4
// the above is like doing:
5
// char ch;
6
// ch = 'a';

We could reassigng to point to something else (although this time the analogy is with a pointer or reference to an object instance):

1
2
double(^g)(double,double);
3
g=^(doublem,doublen){returna+b;};
4
5
// .. later
6
g=^(doublex,doubley){returnx*y;};// g reassigned to point to a new block; same type, so no problem
7
8
// but not:
9
// g = ^(int x) { return x + 1; }; // types are different! The literal on the right has type int(^)(int), not double(^) (double, double)!
10
11
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
12
13
// compare with:
14
inti=10,j=11;// declaring a coupling of integers
15
int*ptrToInt;// declaring a pointer to integers
16
ptrToInt=&i;// ptrToInt points to i
17
// later...
18
ptrToInt=&l// ptrToInt now points to j
19
floatf=3.14;
20
// 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!
21
22
int*anotherPtrToInt;
23
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!

1
2
intsum(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).
3
4
//...
5
6
7
intmain()
8
{
9
//...
10
// we are *inside* main. Can't define a function here!
11
ints=sum(5,4);// sum function being invoked (called). This must happen inside another function! (except for main() itself)
12
}
13
14
15
intsum(inta,intb)// This is the function definition. It must be outside any function!
16
{
17
intc=a+b;
18
returnc;
19
// we're *inside* the function body for sum(). We can't define a new function here!
20
}

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:

1
2
intmain()
3
{
4
// inside main(). Block CAN be defined here!
5
int(^sum)(int,int)=^(inta,intb){returna+b;};// the point here is that we're doing this inside the function main()!
6
}

Again, it helps to recall thechar 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 actuallyused 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:

1
2
#import <Foundation/Foundation.h>
3
4
intmain()
5
{
6
int(^sum)(int,int)=^(inta,intb){returna+b;};// point here is that we're doing this inside main()!
7
8
doublex=5.1,y=7.3;
9
doubles=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
10
NSLog(@"the sum of %f and %f is %f",x,y,s);
11
}

Actually, we can do even better than that: we could define and invoke our block all in one go.

Observe the following:

1
2
#import <Foundation/Foundation.h>
3
4
intmain()
5
{
6
doublex=5.1,y=7.3;
7
doubles=^(doublex,doubley){returnx+y;}(x,y);// we're applying the arguments directly to the block literal!
8
NSLog(@"the sum of %f and %f is %f",x,y,s);
9
}

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?

1
2
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.

1
2
void(^hw)(void)=^{NSLog(@"hello, world!");};
3
// 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:

1
2
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 twodouble values and returning adouble. However, it is sufficient to specify the type of the arguments and return value without giving them a name:

1
2
voidfunc(void(^)(void));

Let's write a simple implementation (definition) for our function.

1
2
voidfunc(void(^b)(void))// we do need to use an identifier in the parameter list now, of course
3
{
4
NSLog(@"going to invoke the passed in block now.");
5
b();
6
}

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?

1
2
#include<Foundation/Foundation.h>
3
4
voidfunc(void(^b)void)// if the function definition appears before it is called, then no need for a separate prototype
5
{
6
NSLog(@"going to invoke the passed in block now.");
7
b();
8
}
9
10
intmain()
11
{
12
void(^hw)(void)=^{NSLog(@"hello, world!");};
13
func(hw);// Note the type of hw matches the type of b in the function definition
14
}
15
16
// The log will show:
17
// going to invoke the passed in block now.
18
// 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:

1
2
// code in main():
3
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 offf?

1
2
intt=1;
3
intg=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 typeint (because its return value is being assigned tog, which is anint). So we haveint 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 typeint * (pointer toint). What about the return type? Let's infer it, just like the compiler would:*x is dereferencing a pointer to anint, so that yields anint, adding one to which is also an int.

Make sure you don't get mixed up between the meaning of * inint *x and the meaning of * in theexpression*x. In the former, it means "x is a pointer to an int variable" in the context of a type declaration, while the latter retrieves theint value stored at the address x.

So, our block returns an int. Thetype_of_block is thusint(^)(int *), meaning ff's prototype is:

1
2
intff(int(^)(int*),int);

Question: Could we have passed in the "hello, world" printing block we created a while ago toff?

Answer: Of course not, its type wasvoid(^)(void)), which is different from the type of the block thatff 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.) andreturns a "calculator block" that you can apply to your operands in order to get the result of that operation.

By a binary operation, we simply mean an operation that operates on two values, known as the "operands".

Suppose we're dealing withdouble operands. Our calculations also return adouble. What's the type of our binary operation block? Easy! It would bedouble(^) (double, double).

Our function (let's called it "operation_creator") takes anint which encodes the type of operation we want it to return. So, for example, callingoperation_creator(0) would return a block capable of performing addition,operation_creator(1) would give a subtraction block, etc. Sooperation_creator's declaration looks likereturn-type operation_creator(int). We just said thatreturn-type isdouble (^)(double, double). How do we put these two together? The syntax gets a little hairy, but don't panic:

1
2
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:

  1. The only identifier (name) isoperation_creator. Start with that.
  2. 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.
  3. What remains is the return type. Mentally removeoperation_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 calledblah that takes as its only parameter a block with no parameters and returns no value, and returns a block of the same type.

1
2
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 usblah(void(^)(void)). The return value is also a block of typevoid(^)(void) so ensconce the previous bit, starting immediately after the^, givingvoid(^blah(void(^)(void)))(void);.

OK, now you can dissect or construct complex block and function declarations, but all these brackets andvoids 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 thetypedef statement, which (as you hopefully know), is a C construct that lets you hide complex types behind a name! For example:

1
2
typedefint**IntPtrPtr;

Gives the nameIntPtrPtr to the type "pointer to pointer toint". Now you can substituteint ** anywhere in code (such as a declaration, a type cast, etc.) withIntPtrPtr.

Let's define a type name for the block typeint(^)(int, int). You can do it like this:

1
2
typedefint(^BlockTypeThatTakesTwoIntsAndReturnsInt)(int,int);

This says thatBlockTypeThatTakesTwoIntsAndReturnsInt is equivalent toint(^)(int, int), that is, a block of the type that takes twoint values and returns anint 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 beingtypedef'd.

How do we apply this idea to theblah 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:

1
2
typedefvoid(^VVBlockType)(void);

Now rewrite the declaration ofblah simply as:

1
2
VVBlockTypeblah(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:

1
2
typedefdouble(^BlockType)(double);// (1)
3
double(^blkptr)(double);// (2)

They look the same, barring thetypedef keyword, but they mean very different things.

With (2), you'vedeclared ablock pointer variable that can point at blocks of typedouble(^)(double).

With (1), you'vedefined atype by the nameBlockType that can stand in as the typedouble (^)double. So, after (2), you could do something like:blkptr = ^(double x){ return 2 * x; };. And after (1), you could've actually written (2) asBlockType blkptr;(!)

Do not proceed unless you've understood this distinction perfectly!

Let's go back to ouroperation function. Can you write a definition for it? Let'stypedef the block type out:

1
2
typedefdouble(^BinaryOpBlock_t)(double,double);
3
4
BinaryOpBlock_toperation_creator(intop)
5
{
6
if(op==0)
7
return^(doublex,doubley){returnx+y;};// addition block
8
if(op==1)
9
return^(doublex,doubley){returnx*y;};// multiplication block
10
// ... etc.
11
}
12
13
intmain()
14
{
15
BinaryOpBlock_tsum=operation_creator(0);// option '0' represents addition
16
NSLog(@"sum of 5.5 and 1.3 is %f",sum(5.5,1.3));
17
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!
18
}
For a cleaner implementation, you'd probably want to define an enumeration (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 anint and returns anint. 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 beint(^)(int). Let'stypedef and then define our function:

1
2
typedefint(^iiblock_t)(int);
3
4
voidfunc(intarr[],intsize,iiblock_tformula)
5
{
6
for(inti=0;i<size;i++)
7
{
8
arr[i]=formula(arr[i]);
9
}
10
}

Let's use this in a program:

1
2
intmain()
3
{
4
inta[]={10,20,30,40,50,60};
5
func(a,6,^(intx){returnx*2;});// after this function call, a will be {20, 40, 60, 80, 100, 120}
6
7
}

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:

1
2
// place the following lines after func(a, 6, ^(int x) { return x * 2; });
3
intn=10;// n declared and assigned
4
func(a,6,^(intx){returnx-n;});// n used in the block!
5
}// closing braces of main

Did you see what we did there? We used the variablen which was in our block'slexical 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 modifyn 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:

1
2
intn=5;
3
iiblock_tb=^(intr){returnr*n;};// created block, but haven't invoked it yet
4
// .. stuff
5
n=1000;// we've the value of n, but that won't affect the block which was defned previously
6
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:

1
2
-(NSArray*)sortCountriesList:(NSArray*)listOfCountries
3
withComparisonBlock:BOOL(^cmp)(Country*country1,Country*country2)
4
{
5
// Implementation of some sorting algorithm that will make several passes through listOfCountries
6
// whatever the algorithm, it will perform several comparisons in each pass and do something based on the result of the comparison
7
8
BOOLisGreater=comp(countryA,countryB);// block invoked, result is YES if countryA is "greater" than countryB based on the passed in block
9
10
if(isGreater)// do something, such as swapping the countries in the array
11
// ...
12
}

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.

1
2
// Inside the body of some method belonging to the same class as the previous method
3
boolsortInAscendingOrder=YES;
4
// calling our sorting method:
5
NSArray*sortedList=[selfsortCountriesList:listwithComparisonBlock:^(Country*c1,Country*c2){
6
if(c1.gdp>c2.gdp)// comparing gdp for instance
7
{
8
if(sortInAscendingOrder)returnYES;
9
elsereturnNO;
10
}
11
}];

And that's it! We used the flagsortInAscendingOrder 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 abool 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 itsdrawRect: 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 ofdrawRect: whenever you need to be reminded of what the view draws. What a bore!

Why can't we do something like this instead:

1
2
[viewdrawWithBlock:^(/* parameters list */){// drawing code for a line, or whatever }];

Well, with blocks, you can! Here,view is an instance of our customUIView subclass endowed with the power of drawing with blocks. Note that while we're still having to subclassUIView, we only have to do it once!

Let's plan ahead first. We'll call our subclassBDView (BD for "block drawing"). Since drawing happens indrawRect:, we wantBDView'sdrawRect: to invoke our drawing block!. Two things to think of: (1) how doesBDView 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!

1
2
@property(nonatomic,copy)drawingblock_tdrawingBlock;

We haven't worked out what the block type should be, but after we do we'lltypedef it asdrawingblock_t!

Why have we usedcopy 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 usecopy 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 thedrawRect: method takes aCGRect 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 indrawRect: as the "current graphics context". If we draw with UIKit classes such asUIBezierPath, 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 typeCGContextRef which inBDView'sdrawRect: 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 cantypedef our drawing block's type as:

1
2
typedefvoid(^drawingblock_t)(CGContextRef,CGRect);

Now, in our-drawWithBlock: method we'll set ourdrawingBlock property to the passed in block and call theUIView methodsetNeedsDisplay which will triggerdrawRect:. IndrawRect:, we'll invoke our drawing block, passing it the current graphics context (returned by theUIGraphicsGetCurrentContext() function) and the drawing rectangle as parameters. Here's the complete implementation:

1
2
3
//BDView.h
4
#import <UIKit/UIKit.h>
5
6
typedefvoid(^drawingblock_t)(CGContextRef,CGRect);
7
8
@interfaceBDView:UIView
9
10
-(void)drawWithBlock:(drawingblock_t)blk;
11
12
@end
1
2
3
// BDView.m
4
#import "BDView.h"
5
6
@interfaceBDView()
7
8
@property(nonatomic,copy)drawingblock_tdrawingBlock;
9
10
@end
11
12
@implementationBDView
13
14
-(void)drawWithBlock:(drawingblock_t)blk
15
{
16
self.drawingBlock=blk;// copy semantics
17
[selfsetNeedsDisplay];
18
}
19
20
21
-(void)drawRect:(CGRect)rect
22
{
23
if(self.drawingBlock)
24
{
25
self.drawingBlock(UIGraphicsGetCurrentContext(),rect);
26
}
27
}
28
29
@end

So, if we had a view controller whoseview property was an instance ofBDView, we could call the following method fromviewDidLoad (say) to draw a line that ran diagonally across the screen from the top-left corner to the bottom right corner:

1
2
-(void)viewDidLoad
3
{
4
[superviewDidLoad];
5
6
BDView*view=(BDView*)self.view;
7
[viewdrawWithBlock:^(CGContextRefcontext,CGRectrect){
8
CGContextMoveToPoint(context,rect.origin.x,rect.origin.y);
9
CGContextAddLineToPoint(context,rect.origin.x+rect.size.width,rect.origin.y+rect.size.height);
10
CGContextStrokePath(context);
11
}];
12
}

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 atBlocksKit, which includes many interesting utilities of blocks that can make your life as an iOS developer easier. Happy coding!

Advertisement
Advertisement
Advertisement
Advertisement
Every type of asset, for any type of project
Plus all the leading gen-AI tools
Unlimited Downloads
From $16.50/month
Get access to over one million creative assets on Envato.
Over 9 Million Digital Assets
Everything you need for your next creative project.
Create Beautiful Logos, Designs
& Mockups in Seconds
Design like a professional without Photoshop.
Join the Community
Share ideas. Host meetups. Lead discussions. Collaborate.

[8]ページ先頭

©2009-2025 Movatter.jp