A block is a chunk of code. Here is a block:
^{ NSLog(@"This is an instruction within a block."); }
It looks like a C function; it is a set of instructions inside curly braces. It does not, however, have a name. Instead, the caret (^) identifies this bit of code as a block.
Like a function, a block can take arguments and return values. Here is another block:
^(double dividend, double divisor) { double quotient = dividend / divisor; return quotient; }
This block takes two doubles as arguments and returns a double.
You can pass a block as an argument to a method that accepts a block. Many of Apple’s classes have methods that accept blocks as arguments.
For instance, NSArray, NSDictionary, and NSSet allow block-based enumeration: Each class has at least one method that accepts a block. When one of these methods is called, it will execute the code within the passed-in block once for each object in the collection. In this chapter, you are going to use NSArray’s enumerateObjectsUsingBlock: method.
(If you have a background in another programming language, you might know blocks as anonymous functions, closures, or lambdas. If you are familiar with function pointers, blocks may seem similar, but blocks allow for more elegant code than can be written with function pointers.)
Create a new Foundation Command Line Tool and call it VowelMovement. This program will iterate through an array of strings, remove the vowels from each string, and store the “devowelized” strings in a new array.
In main.m, set up three arrays: one for the original strings, one for the devowelized strings, and a third for a list of vowels.
int main (int argc, const char * argv[]) { @autoreleasepool { // Create array of strings and a container for devowelized ones NSArray *originalStrings = @[@"Sauerkraut", @"Raygun", @"Big Nerd Ranch", @"Mississippi"]; NSLog(@"original strings: %@", originalStrings); NSMutableArray *devowelizedStrings = [NSMutableArray array]; // Create a list of characters to be removed from the string NSArray *vowels = @[@"a", @"e", @"i", @"o", @"u"]; } return 0; }
Nothing new here; you are just setting up arrays. Build your program, and ignore the warnings about unused variables for now.
Soon you will compose your first block. This block will make a copy of a given string, remove the vowels from the copied string, and then add this string to the devowelizedStrings array.
You are going to send the originalStrings array the enumerateObjectsUsingBlock: message with your devowelizing block as its argument. But first, there is some more block syntax to learn.
A block can be stored in a variable. In main.m, type in the following block variable declaration.
int main (int argc, const char * argv[]) { @autoreleasepool { // Create array of strings and a container for devowelized ones NSArray *originalStrings = @[@"Sauerkraut", @"Raygun", @"Big Nerd Ranch", @"Mississippi"]; NSLog(@"original strings: %@", originalStrings); NSMutableArray *devowelizedStrings = [NSMutableArray array]; // Create a list of characters to be removed from the string NSArray *vowels = @[@"a", @"e", @"i", @"o", @"u"]; // Declare the block variable void (^devowelizer)(id, NSUInteger, BOOL *); } return 0; }
Let’s break down this declaration. The name of the block variable (devowelizer) is in a set of parentheses right after the caret.
The declaration includes the block’s return type (void
) and the types of its arguments (id, NSUInteger, BOOL *), just like in a function declaration.
What is the type of this block variable? It is not simply “block.” Its type is “a block that takes an object, an integer, and a BOOL pointer, and returns nothing.” This is the type of block that enumerateObjectsUsingBlock: expects. You will learn what each of these arguments is used for shortly.
Now you need to compose a block of the declared type and assign it to the new variable. In main.m, compose a block that makes a mutable copy of the original string, removes its vowels, and then adds the new string to the array of devowelized strings and assigns it to devowelizer:
int main (int argc, const char * argv[]) { @autoreleasepool { ... // Declare the block variable void (^devowelizer)(id, NSUInteger, BOOL *); // Compose a block and assign it to the variable devowelizer = ^(id string, NSUInteger i, BOOL *stop) { NSMutableString *newString = [NSMutableString stringWithString:string]; // Iterate over the array of vowels, replacing occurrences of each // with an empty string for (NSString *s in vowels) { NSRange fullRange = NSMakeRange(0, [newString length]); [newString replaceOccurrencesOfString:s withString:@"" options:NSCaseInsensitiveSearch range:fullRange]; } [devowelizedStrings addObject:newString]; }; // End of block assignment } return 0; }
Notice that the block assignment ends with a semi-colon just like any variable assignment would. Build your program to check your typing. The warnings about unused variables should disappear.
As with any variable, you can perform the declaration and assignment of devowelizer in one or two steps. Here is what it would look like in one step:
void (^devowelizer)(id, NSUInteger, BOOL *) = ^(id string, NSUInteger i, BOOL *stop) { NSMutableString *newString = [NSMutableString stringWithString:string]; // Iterate over the array of vowels, replacing occurrences of each // with an empty string. for (NSString *s in vowels) { NSRange fullRange = NSMakeRange(0, [newString length]); [newString replaceOccurrencesOfString:s withString:@"" options:NSCaseInsensitiveSearch range:fullRange]; } [devowelizedStrings addObject:newString]; };
In main.m, send the enumerateObjectsUsingBlock: message with devowelizer to the array of original strings and then print out the devowelized strings.
int main (int argc, const char * argv[]) { @autoreleasepool { ... // Declare the block variable void (^devowelizer)(id, NSUInteger, BOOL *); // Assign a block to the variable devowelizer = ^(id string, NSUInteger i, BOOL *stop) { NSMutableString *newString = [NSMutableString stringWithString:string]; // Iterate over the array of vowels, replacing occurrences of each // with an empty string. for (NSString *s in vowels) { NSRange fullRange = NSMakeRange(0, [newString length]); [newString replaceOccurrencesOfString:s withString:@"" options:NSCaseInsensitiveSearch range:fullRange]; } [devowelizedStrings addObject:newString]; }; // End of block assignment // Iterate over the array with your block [originalStrings enumerateObjectsUsingBlock:devowelizer]; NSLog(@"devowelized strings: %@", devowelizedStrings); } return 0; }
Build and run your program. You will see two arrays logged to the console.
2011-09-03 10:27:02.617 VowelMovement[787:707] original strings: ( Sauerkraut, Raygun, "Big Nerd Ranch", Mississippi ) 2011-09-03 10:27:02.618 VowelMovement[787:707] new strings: ( Srkrt, Rygn, "Bg Nrd Rnch", Msssspp )
The three arguments of this block type are specifically designed for iterating through an array. The first is a pointer to the current object. Notice that this pointer’s type is id so that it will work no matter what kind of objects the array contains. The second argument is an NSUInteger that is the index of the current object. The third argument is a pointer to a BOOL, which defaults to NO. Changing it to YES will stop executing the block after the current iteration.
Modify your block to check for an uppercase or lowercase ‘y’ character. If there is one, set the pointer to YES (which will prevent the block from performing any more iterations) and end the current iteration.
devowelizer = ^(id string, NSUInteger i, BOOL *stop){ NSRange yRange = [string rangeOfString:@"y" options:NSCaseInsensitiveSearch]; // Did I find a y? if (yRange.location != NSNotFound) { *stop = YES; // Prevent further iterations return; // End this iteration } NSMutableString *newString = [NSMutableString stringWithString:string]; // Iterate over the array of vowels, replacing occurrences of each // with an empty string. for (NSString *s in vowels) { NSRange fullRange = NSMakeRange(0, [newString length]); [newString replaceOccurrencesOfString:s withString:@"" options:NSCaseInsensitiveSearch range:fullRange]; } [devowelizedStrings addObject:newString]; }; // End of block assignment
Build and run the program. Again, two arrays are logged to the debugger output, but this time, the array enumeration was cancelled during the second iteration when the block encountered a word with the letter ‘y’ in it. All you get is Srkrt.
Block syntax can be confusing, but you can make it clearer using the typedef keyword that you learned about in Chapter 11. Remember that typedefs belong at the top of the file or in a header, outside of any method implementations. In main.m, add the following line of code:
#import <Foundation/Foundation.h> typedef void (^ArrayEnumerationBlock)(id, NSUInteger, BOOL *); int main (int argc, const char * argv[]) {
Notice that this looks identical to a block variable declaration. However, here you are defining a type rather than a variable, hence the appropriate type name next to the caret. This allows you to simplify declarations of similar blocks.
Now you can declare devowelizer using your new type:
int main(int argc, const char * argv[]) { @autoreleasepool { ... // Declare the block variable void (^devowelizer)(id, NSUInteger, BOOL *); ArrayEnumerationBlock devowelizer; // Compose and assign a block to the variable devowelizer = ^(id string, NSUInteger i, BOOL *stop) { ...
Note that the block type itself only defines the block’s arguments and return types; it has no bearing on the set of instructions within a block of that type.