How To Use Blocks in iOS 5 Tutorial – Part 2
This is a blog post by iOS Tutorial Team member Adam Burkepile, a full-time Software Consultant and independent iOS developer. Check out his latest app Pocket No Agenda, or follow him on Twitter. Welcome back to our tutorial series on using blocks in iOS 5 – with some Storyboard/Interface Builder practice along the way! In […] By .
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Use Blocks in iOS 5 Tutorial – Part 2
50 mins
- Getting Started: An Intro to Blocks
- Back to the iOS Diner: Setting Up Model Classes
- Setting Up Basic Properties in IODItem
- Setting Up Basic Properties in IODOrder
- Setting Up Basic Properties in IODViewController
- Loading the Inventory
- Dispatch Queues and Grand Central Dispatch
- Adding Helper Methods
- Add and Remove Current Item
- UIAnimation
- Getting the Total
- Useful Blocks Cheat Sheet
- Creating Your Own Blocks
- Blocks and Autocompletion
- Where to Go From Here?
This is a blog post by iOS Tutorial Team member Adam Burkepile, a full-time Software Consultant and independent iOS developer. Check out his latest app Pocket No Agenda, or follow him on Twitter.
Welcome back to our tutorial series on using blocks in iOS 5 – with some Storyboard/Interface Builder practice along the way!
In the first part of the series, we used iOS 5.0 Storyboards to set up the view and wound up with a nice-looking interface, close to what you see to the right.
In this second and final part of the series, we finally get to blocks! We’ll talk about what blocks are, how their syntax works, how you use them, and cover tons of examples.
We’ll show you how you can use blocks with NSArrays, for UIView animations, Grand Central Dispatch, and much more!
So unblock your schedule and keep reading for some blocks practice and fun!
Getting Started: An Intro to Blocks
Blocks are a new feature that was introduced in iOS 4.0 and Mac OSX 10.6. Blocks can greatly simplify code. They can help you reduce code, reduce dependency on delegates, and write cleaner, more readable code.
Even so, Blocks are a feature that remains unused by some developers who may not totally grasp how to use them. But they are definitely a tool that you, as an Objective-C programmer, will want to have in you toolbox and know how to use.
Let’s look at the “Who, What, Where, Why, and When” of Blocks.
What Are These ‘Block’ Things and Why Are They So Important?
At its core, a Block is a chunk of code that can be executed at some future time.
Blocks are first-class functions, which is a fancy way of saying that Blocks are regular Objective-C objects. Since they’re objects, they can be passed as parameters, returned from methods and functions, and assigned to variables.
Blocks are called closures in other languages such as Python, Ruby and Lisp, because they encapsulate state when they are declared. A block creates a const copy of any local variable that is referenced inside of its scope.
Before blocks, whenever you wanted to call some code and have it call you back later, you would typically use delegates or NSNotificationCenter. That worked fine, except it spreads your code all over – you start a task in one spot, and handle the result in another.
Blocks are nice because they keep your code related to handling a task all in one place, as you’ll see soon.
Who Are Blocks For?
YOU! Blocks are for everyone! Seriously though, Blocks are for everyone and everyone WILL use Blocks. Blocks are the future, so you might as well learn them now. Many built-in framework methods are being rewritten or augmented with Block-based versions of existing functionality.
How Do You Use Blocks?
This nifty image, via the iOS Developer Library, does a good job explaining Blocks syntax:
The declaration format for Blocks is as follows:
return_type (^block_name)(param_type, param_type, ...)
If you’ve programmed in any other C-type language, this should look pretty familiar to you, except for that ^ symbol. The ^ symbol is what denotes “this thing we are declaring is a block.”
If you can get your head around the idea that ^ means “I’m a block”, congrats – you’ve just learned the hardest thing about using blocks! ;]
Note that parameter names are not required at this point, but you can include them if you’d like.
Here’s an example declaration of a block:
int (^add)(int,int)
Next, here’s a block definition:
// Block Definition
^return_type(param_type param_name, param_type param_name, ...) { ... return return_type; }
This is how the Block is actually created. Notice that this has a different structure than the Block declaration. It begins with the ^ symbol and is followed by the parameters, which must be named at this point, and must match the type and order of the parameter list of the Block declaration to which it’s assigned. This is followed by the actual code.
When you define Blocks, the return type is optional and can be inferred from the return type in the code. If there are multiple return statements, they must all be of the same type (or cast to the same type).
Here’s an example of a Block definition:
^(int number1, int number2){ return number1+number2 }
If we put the Block declaration and definition example together, we get a full statement:
int (^add)(int,int) = ^(int number1, int number2){
return number1+number2;
}
And we can use the Block like so:
int resultFromBlock = add(2,2);
Let’s look at a couple of examples of using Blocks vs the same code written without Blocks.
Example: NSArray
Lets look at how Blocks change how we might do some operations on an array.
First let’s look at a normal for loop:
BOOL stop;
for (int i = 0 ; i < [theArray count] ; i++) {
NSLog(@"The object at index %d is %@",i,[theArray objectAtIndex:i]);
if (stop)
break;
}
The "stop" variable in the above method might not make much sense to you. But it will become clearer when you look at the Block-based approach for the same method. The Blocks approach provides for a "stop" variable which allows you to stop the loop processing at any point and we're simply duplicating that functionality here to provide equivalent code to the Blocks-based approach.
Now let's look at the same code as above using fast-enumeration:
BOOL stop;
int idx = 0;
for (id obj in theArray) {
NSLog(@"The object at index %d is %@",idx,obj);
if (stop)
break;
idx++;
}
And now with Blocks:
[theArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
NSLog(@"The object at index %d is %@",idx,obj);
}];
In the Block-specific code above, you might wonder what the "stop" variabl is. This is simply a variable that can be set to YES from within the block to stop further processing. This parameter is specified as part of the block to be used by the enumerateObjectsUsingBlock method.
The above is a pretty trivial example and it might be admittedly difficult to see the advantage of Blocks in this case. But there are two things I want to point out about the Blocks approach:
- Simplicity. Using Blocks, we have access to the object, the object index in the array, and a stop variable, all without having to write any code. This means less code, which means less chance of a coding error (not that we make any coding errors).
- Speed. There might be a slight speed advantage to using the Block method over the fast-enumeration method. This (possible) speed advantage is so minute in this case it is barely worth mentioning, but in more complex cases, the advantage becomes significant. (Source)
Example: UIView Animation
Let's take a simple animation that operates on a single UIView. It changes the view's alpha to 0 and moves the view down and to the right 50 points by 50 points, then removes the UIView from the superview. Easy, right?
The Non-Block approach:
- (void)removeAnimationView:(id)sender {
[animatingView removeFromSuperview];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIView beginAnimations:@"Example" context:nil];
[UIView setAnimationDuration:5.0];
[UIView setAnimationDidStopSelector:@selector(removeAnimationView)];
[animatingView setAlpha:0];
[animatingView setCenter:CGPointMake(animatingView.center.x+50.0,
animatingView.center.y+50.0)];
[UIView commitAnimations];
}
The Block approach:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIView animateWithDuration:5.0
animations:^{
[animatingView setAlpha:0];
[animatingView setCenter:CGPointMake(animatingView.center.x+50.0,
animatingView.center.y+50.0)];
}
completion:^(BOOL finished) {
[animatingView removeFromSuperview];
}];
}
If we look at these two methods, there are three advantages that stand out to me:
- Simplifies code. With blocks, we don't have to do things like declare an entirely separate method for completion callback, or call the beginAnimations/commitAnimations lines.
- Keeps code together. With blocks, we don't have to start the animation in one place, and have a callback method in another place. All of our code relating to animations is in one spot, which makes it easier to read and write.
- Apple says so. This is an instance of Apple having used Blocks to rewrite functionality that existed prior to Blocks, and now the official Apple recommendation is to transition over to the Block-based method, if possible. (Source)
When Do You Use Blocks?
I think that the best advice that can be given here is to use Blocks when they are most appropriate. There might be times that you want to continue using old methods either because you want to maintain backwards compatibility or because you are more familiar with the older way of doing things. But each time you come to such a decision point, think whether Blocks would simplify your life and whether you can substitute a Blocks-based method instead. Then do what works best for you.
Of course, you might find yourself needing to use Blocks more and more in the future simply because many frameworks, both third-party and Apple's own, are being written and re-written to use Blocks. So start using Blocks now so that you're armed to face the future.
Back to the iOS Diner: Setting Up Model Classes
You'll be picking up right where you left off in Part One. If you didn't do Part One or just need a fresh start, you can download the project in its current state here.
Open the project in Xcode and switch to the Project Navigator. Right-click on the iOSDiner folder and select New Group. Name it “Models.”
Right-click on the new Models folder and click New File. Select Objective-C Class. Name the class “IODItem” and make it a subclass of NSObject.
For the file location, select the iOSDiner folder, and select New Folder to create a matching Models folder in the file system. Make sure the newly created Models folder is selected and then hit Create. This will create .h and .m files for the IODItem class.
Repeat this file creation process for a class called IODOrder. Right-click on the Models folder and click New File. Select Objective-C Class. Name the class “IODOrder” and make it a subclass of NSObject.
Make sure the Models folder is selected and then click Create.
Now you have all the classes and files you need set up. Time to get coding!
Setting Up Basic Properties in IODItem
Select IODItem.h. The first thing you're going to do is add the NSCopying protocol to the class.
Protocols are a way of setting up a contract of the methods the class will implement. Basically, if a class implements a certain protocol, then the class would need to implement certain methods that are required or optional, as defined for that particular protocol. To implement the NSCopying protocol, change IODItem.h to look like this:
@interface IODItem : NSObject <NSCopying>
Next, add the properties for the item's attributes. An item will have a name/title, price, and an image file. Add the following properties underneath the previous line. The complete .h file should look like this now:
#import <Foundation/Foundation.h>
@interface IODItem : NSObject <NSCopying>
@property (nonatomic,strong) NSString* name;
@property (nonatomic,assign) float price;
@property (nonatomic,strong) NSString* pictureFile;
@end
Now switch over to IODItem.m and add the synthesizers for the properties by adding the following below the @implementation IODItem line:
@synthesize name;
@synthesize price;
@synthesize pictureFile;
If you build the code right now and look at it, you'll see a warning.
This warning is a reference to the NSCopying protocol that you added above. Remember how I said the protocol might define required methods? Well, the NSCopying protocol requires that -(id)copyWithZone:(NSZone *)zone be implemented. Since you haven't done that, the class is incomplete – hence the warning!
Add the following method to the end of IODItem.m (before the @end):
-(id)copyWithZone:(NSZone *)zone {
IODItem* newItem = [IODItem new];
[newItem setName:[self name]];
[newItem setPrice:[self price]];
[newItem setPictureFile:[self pictureFile]];
return newItem;
}
Poof! No more warning!
All the code does is to create a new IODItem, set the properties to be exactly the same as the existing object, and return the new object instance.
You're also going to set up an initializer method. This is just a quick way to set up the default properties when you initialize an instance. Add the following method to the end of IODItem.m:
- (id)initWithName:(NSString*)inName andPrice:(float)inPrice andPictureFile:(NSString*)inPictureFile {
if (self = [self init]) {
[self setName:inName];
[self setPrice:inPrice];
[self setPictureFile:inPictureFile];
}
return self;
}
Switch back to IODItem.h and add the prototype for the above method to the end of the file (but above the @end):
- (id)initWithName:(NSString*)inName andPrice:(float)inPrice andPictureFile:(NSString*)inPictureFile;
Setting Up Basic Properties in IODOrder
Next, we are going to work on the other class, IODOrder. This class will represent the order and the operations that go along with an order: adding an item, removing an item, calculating the total for the order, and printing out an overview of the order.
Switch to IODOrder.h and before the @interface section, add the following to let the IODOrder class know there is such a class as IODItem.
@class IODItem;
Inside the @interface section, add the following property:
@property (nonatomic,strong) NSMutableDictionary* orderItems;
This is the dictionary that will hold the items ordered by the user. Switch to IODOrder.m and import the IODItem class header at the top of the file:
#import "IODItem.h"
Next synthesise properties below the @implementation IODOrder line:
@synthesize orderItems;
Setting Up Basic Properties in IODViewController
Switch to IODViewController.h to add an instance variable and two properties. Replace the existing "@interface IODViewController : UIViewController" line with the following:
@class IODOrder;
@interface IODViewController : UIViewController {
int currentItemIndex;
}
@property (strong, nonatomic) NSMutableArray* inventory;
@property (strong, nonatomic) IODOrder* order;
The currentItemIndex variable will keep track of which item the user is currently browsing in the inventory. Inventory is pretty self-explanatory; it's an array of the IODItems that we got from the web service. Order is an instance of the IODOrder class that stores the items currently in the user's order.
Switch to the IODViewController.m and do the following:
- Add the imports for IODItem and IODOrder
- Add the @synthesize for the inventory and order properties
- Initialize currentItemIndex to 0 during viewDidLoad
- Set the order property to a new IODOrder instance
When you're done, it should look like this:
#import "IODViewController.h"
#import "IODItem.h" // <---- #1
#import "IODOrder.h" // <---- #1
@implementation IODViewController
// ... Other synthesize statements ...
@synthesize inventory; // <---- #2
@synthesize order; // <---- #2
// ... didReceiveMemoryWarning - not relevant to discussion ...
#pragma mark - View lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
currentItemIndex = 0; // <---- #3
self.order = [IODOrder new]; // <---- #4
}
Give it a build. Everything should run smoothly, with no warnings.
Loading the Inventory
The method retrieveInventoryItems, which you will add shortly, will download and process the inventory items from the web service. This is a class method, not an instance method.
Note: A class method is indicated by the + sign at the beginning of the method definition. An instance method is indicated by a - sign.
Add the following line to the top of IODItem.m, just below the #imports:
#define kInventoryAddress @"http://adamburkepile.com/inventory/"
Note: If you are hosting the web service yourself, change the URL in the above line to point to your endpoint.
Add the following method above the @end in IODItem.m:
+ (NSArray*)retrieveInventoryItems {
// 1 - Create variables
NSMutableArray* inventory = [NSMutableArray new];
NSError* err = nil;
// 2 - Get inventory data
NSArray* jsonInventory = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:kInventoryAddress]]
options:kNilOptions
error:&err];
// 3 - Enumerate inventory objects
[jsonInventory enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary* item = obj;
[inventory addObject:[[IODItem alloc] initWithName:[item objectForKey:@"Name"]
andPrice:[[item objectForKey:@"Price"] floatValue]
andPictureFile:[item objectForKey:@"Image"]]];
}];
// 4 - Return a copy of the inventory data
return [inventory copy];
}
Hey, your first Block! Let's take a closer look at this code to see exactly what it's doing:
- First, we define the array that will hold the return objects and an error pointer.
- We use a regular NSData object to download the data from the web service, and then pass that NSData object into the iOS's new JSON data service to decode the raw data into Objective-C object types (NSArrays, NSDictionaries, NSStrings, NSNumbers, etc).
- Next, we use the enumerateObjectsUsingBlock: method that we discussed earlier to convert the objects from regular NSDictionaries to IODItem class objects. We call the enumerateObjectsUsingBlock: method on the jsonInventory array and enumerate over it with a Block that casts the object passed to the Block as an NSDictionary object, uses that dictionary object to create a new IODItem, and finally adds that new item to the return inventory array.
- Finally, we return the inventory array. Note that we return a copy of the array instead of returning it directly, because we don't want to return a mutable version. The copy method creates an immutable version you can safely return.
Now switch back to the IODItem.h and add the prototype for the method:
+ (NSArray*)retrieveInventoryItems;
Dispatch Queues and Grand Central Dispatch
Another concept that would be useful to learn is dispatch queues. Switch to IODViewController.m and add the following inside the @implementation block, just below the @synthesize statements.
dispatch_queue_t queue;
Then, inside the viewDidLoad method, add this line to the end of the method:
queue = dispatch_queue_create("com.adamburkepile.queue",nil);
The first parameter for the dispatch_queue_create method is the queue name. You can name it however you want, but it has to be unique to the entire system. This is why Apple recommends a reverse DNS-style name.
You need to release the queue when you deallocate the view controller. Even though you're using ARC with this project, ARC doesn't manage dispatch queues, so you need to manually release it. But remember that with ARC enabled, you don't have to call [super dealloc] inside of the dealloc method. So add the following to the end of your code:
-(void)dealloc {
dispatch_release(queue);
}
Now let's put that queue to use. Add the following three lines to viewDidAppear below the existing code:
// 1 - Set initial label text
ibChalkboardLabel.text = @"Loading Inventory...";
// 2 - Get inventory
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
// 3 - Set inventory loaded text
ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
Give it a run.
Something's not quite right, is it? You're using the retrieveInventoryItems method that you defined on IODItem to call the web service, return the inventory items and assign them to the inventory array.
Remember, there was a five-second delay in that PHP web service script from Part One. But when we run the program, it doesn't say “Loading Inventory...” and then wait for five seconds before saying “Inventory Loaded.” It seems to start up and then say “Inventory Loaded” after five seconds without saying “Loading Inventory....”!
The problem is this: the call to the web service is blocking and freezing the main thread and won't allow it to change the label text. If only there was a separate queue you could use for that long operation that didn't interfere with the main thread...
OH WAIT! WE MADE A SEPARATE QUEUE! This is where Grand Central Dispatch and Blocks can help us solve a problem very simply. Using Grand Central Dispatch, we can assign work (in the form of Blocks) to be done on our other queue that is separate and doesn't block the main thread.
Replace sections #2 and #3 of the viewDidAppear method with this:
// 2 - Use queue to fetch inventory and then set label text
dispatch_async(queue, ^{
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
dispatch_async(dispatch_get_main_queue(), ^{
ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
});
});
Note that we use two different blocks here, that have a void return value and no parameters.
Give it another run and everything should work perfectly.
Did you wonder why we made that second call to dispatch_async to set the text label? When you set the label text, you're updating a UI element, and anything that updates anything on the UI has to be executed on the main thread. So we make another call with dispatch_async, but this time we get the main queue and execute our Block on the main queue.
This method of jumping or nesting queues from a background queue to the main queue is quite common when an operation takes a long time and there is a subsequent action which involves updating a user interface element.
Grand Central Dispatch is a complex system that you can't fully appreciate or understand based on the tiny role it plays in this tutorial. If you're interested, I suggest you read the Multithreading and Grand Central Dispatch on iOS for Beginners tutorial on this site.
Adding Helper Methods
You're using the web service to download and store the inventory. Now you're going to set up three helper methods that'll assist you in displaying the stored inventory information to the user.
The first method, findKeyForOrderItem:, will be added to IODOrder.m. This method isn't directly useful, but will be necessary in accessing the item dictionary.
Add the following to the end of IODOrder.m (before the @end):
- (IODItem*)findKeyForOrderItem:(IODItem*)searchItem {
// 1 - Find the matching item index
NSIndexSet* indexes = [[self.orderItems allKeys] indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
IODItem* key = obj;
return [searchItem.name isEqualToString:key.name] &&
searchItem.price == key.price;
}];
// 2 - Return first matching item
if ([indexes count] >= 1) {
IODItem* key = [[self.orderItems allKeys] objectAtIndex:[indexes firstIndex]];
return key;
}
// 3 - If nothing is found
return nil;
}
Let's examine what this function does. But in order to do that, I need to explain why all of this is necessary. The IODOrder object contains a property called orderItems, which is a dictionary of key-values pairs. The key will be an IODItem and the value will be a NSNumber, specifying how many of that particular item has been ordered.
This is all fine in theory, but a little quirk of the NSDictionary class is that when you assign something as a key, it doesn't actually assign the object, it makes a copy of the object and uses that as the key. This means the object you use as the key must conform to the NSCopying protocol (which is why you had to implement NSCopying on IODItem).
The fact that the key in the orderItems dictionary and the IODItem in the inventory array are not technically the same object (even though they have the same properties) means that you can't perform a simple search for the key. Instead, you must compare the name and price of each object to determine if they are the same item. That's what the above function does: it compares the properties of the keys to find one that matches the one being searching for.
With that said, here's what the code does:
- Here you look at all the keys in the orderItems dictionary and use the indexesOfObjectsPassingTest: method to find the keys that match the name and price. This is another example of a Block method. Notice the BOOL after the ^. This is the return type. This particular method works on an array and uses the Block to compare two objects to return the indexes of all the objects that pass any specific test specified by the the Block.
- This simply takes the returned indexes and returns the first one.
- Return nil if a matching key object isn't found.
Don't forget to add the method prototype to the IODOrder.h:
- (IODItem*)findKeyForOrderItem:(IODItem*)searchItem;
Now switch to IODViewController.m and add the following method to the end of the file:
- (void)updateCurrentInventoryItem {
if (currentItemIndex >= 0 && currentItemIndex < [self.inventory count]) {
IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
ibCurrentItemLabel.text = currentItem.name;
ibCurrentItemImageView.image = [UIImage imageNamed:[currentItem pictureFile]];
}
}
Using currentItemIndex and the inventory array, this method sets the displayed name and picture for the current inventory item.
Still in the IODViewController.m, add:
- (void)updateInventoryButtons {
if (!self.inventory || [self.inventory count] == 0) {
ibAddItemButton.enabled = NO;
ibRemoveItemButton.enabled = NO;
ibNextItemButton.enabled = NO;
ibPreviousItemButton.enabled = NO;
ibTotalOrderButton.enabled = NO;
} else {
if (currentItemIndex <= 0) {
ibPreviousItemButton.enabled = NO;
} else {
ibPreviousItemButton.enabled = YES;
}
if (currentItemIndex >= [self.inventory count]-1) {
ibNextItemButton.enabled = NO;
} else {
ibNextItemButton.enabled = YES;
}
IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
if (currentItem) {
ibAddItemButton.enabled = YES;
} else {
ibAddItemButton.enabled = NO;
}
if (![self.order findKeyForOrderItem:currentItem]) {
ibRemoveItemButton.enabled = NO;
} else {
ibRemoveItemButton.enabled = YES;
}
if ([order.orderItems count] == 0) {
ibTotalOrderButton.enabled = NO;
} else {
ibTotalOrderButton.enabled = YES;
}
}
}
This is the longest of the three helper methods, but it's quite simple when you look at it. This method looks at the various states the program can be in and determines if the buttons should be enabled or disabled.
For example, if the currentItemIndex is 0, the previous item button is disabled because you can't go back farther than the first item. If the orderItems count is 0, then the total order button is disabled, because there's nothing to calculate the total for.
Add the prototypes for these two methods to IODViewController.h:
- (void)updateCurrentInventoryItem;
- (void)updateInventoryButtons;
All right! Equipped with these helper methods, it's time to make some magic happen. Go back to viewDidAppear in IODViewController.m and add the following above section #1:
// 0 - Update buttons
[self updateInventoryButtons];
Then, replace section #2 with the following:
// 2 - Use queue to fetch inventory and then update UI
dispatch_async(queue, ^{
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateInventoryButtons];
[self updateCurrentInventoryItem];
ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
});
});
Build and run.
HEY! Nom nom, Hamburger... I'd like to see some other food though, so let's get those other buttons working.
The ibaLoadNextItem: and ibaLoadPreviousItem: methods have already been stubbed out in IODViewController when you created the action in the storyboard. So let's add the following code to the stubs to implement those methods in IODViewController.m:
- (IBAction)ibaLoadPreviousItem:(id)sender {
currentItemIndex--;
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
- (IBAction)ibaLoadNextItem:(id)sender {
currentItemIndex++;
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
With the aid of the helper methods you created above, switching items is a simple matter of changing the currentItemIndex and refreshing the onscreen information. What could be easier? Now you have an entire buffet of food to choose from!
Compile and test how easy it is to flip through the whole menu of yummy food.
Add and Remove Current Item
Unfortunately, you have a menu but the waiter isn't taking orders. Or, in other words, the add/remove item buttons don't work. Time to change that.
You need another helper method in the IODOrder class, so switch over to IODOrder.m and add the following method:
- (NSMutableDictionary *)orderItems{
if (!orderItems) {
orderItems = [NSMutableDictionary new];
}
return orderItems;
}
This is simply the getter for the orderItems property. If orderItems has been assigned something, it returns that object. If it hasn't been assigned anything, it creates a new dictionary and assigns that to orderItems, and then returns it.
Next you're going to work on the orderDescription method. This method will provide the string used when the app prints on the chalkboard. Add the following code to IODOrder.m:
- (NSString*)orderDescription {
// 1 - Create description string
NSMutableString* orderDescription = [NSMutableString new];
// 2 - Sort the order items by name
NSArray* keys = [[self.orderItems allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
IODItem* item1 = (IODItem*)obj1;
IODItem* item2 = (IODItem*)obj2;
return [item1.name compare:item2.name];
}];
// 3 - Enumerate items and add item name and quantity to description
[keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
IODItem* item = (IODItem*)obj;
NSNumber* quantity = (NSNumber*)[self.orderItems objectForKey:item];
[orderDescription appendFormat:@"%@ x%@\n", item.name, quantity];
}];
// 4 - Return order description
return [orderDescription copy];
}
I'll break this down a little for you:
- This is the string for the order description. Each item in the order will be appended to this string.
- This chunk of code takes an array composed of the keys in the orderItems dictionary and uses a Block method, sortedArrayUsingComparator:, to sort those keys by name.
- This code then uses that sorted array of keys to call that same old enumerateObjectsUsingBlock: method that by now you know and love. For each key, you convert it to an IODItem, get the value (the quantity in the order), and add that string to the orderDescription string.
- Finally, you return the orderDescription string, but again you return a copy of it so that it's an immutable version.
Switch to IODOrder.h and add the prototypes for those two methods:
- (NSMutableDictionary *)orderItems;
- (NSString*)orderDescription;
Now that you can get the current order string from the order object, switch back to IODViewController.m and add a method to call it. You can add this method to the end of the file.
- (void)updateOrderBoard {
if ([order.orderItems count] == 0) {
ibChalkboardLabel.text = @"No Items. Please order something!";
} else {
ibChalkboardLabel.text = [order orderDescription];
}
}
This method looks at the number of items in the order. If the number of items is zero, it returns a static string indicating that there are no items in the order. Otherwise, the method uses the orderDescription method defined in IODOrder to display a string of all the items in the order and their quantities.
Add the prototype for this method to IODViewController.h:
- (void)updateOrderBoard;
Now that you can update the board with the current order, do so by replacing section #2 of the viewDidAppear method in IODViewController.m:
// 2 - Use queue to fetch inventory and then then update UI
dispatch_async(queue, ^{
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateOrderBoard]; // <---- Add
[self updateInventoryButtons];
[self updateCurrentInventoryItem];
ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
});
});
}
I realize this is a bit pointless since you just overwriting it with the initial text a couple of lines down, but for sake of consistency, it's not a bad idea.
The next method you're going to implement will add an item to the order. Switch to IODOrder.m and add this method:
- (void)addItemToOrder:(IODItem*)inItem {
// 1 - Find item in order list
IODItem* key = [self findKeyForOrderItem:inItem];
// 2 - If the item doesn't exist, add it
if (!key) {
[self.orderItems setObject:[NSNumber numberWithInt:1] forKey:inItem];
} else {
// 3 - If item exists, update the quantity
NSNumber* quantity = [self.orderItems objectForKey:key];
int intQuantity = [quantity intValue];
intQuantity++;
// 4 - Update order items list with new quantity
[self.orderItems removeObjectForKey:key];
[self.orderItems setObject:[NSNumber numberWithInt:intQuantity] forKey:key];
}
}
Here's the step-by-step explanation:
- You use the method you previously created to find the key for the orderItem dictionary entry. Remember, if the object isn't found, it simply returns nil.
- If the object wasn't found in the order at this point, add the item to the order with a quantity of 1.
- If the object was found, we read the quantity value, store it, and increment it.
- Finally, we remove the the original entry and insert a new version with the updated quantity value.
The removeItemFromOrder: method is very much the same as the addItemToOrder: method. Add the following code to IODOrder.m:
- (void)removeItemFromOrder:(IODItem*)inItem {
// 1 - Find the item in order list
IODItem* key = [self findKeyForOrderItem:inItem];
// 2 - We remove the item only if it exists
if (key) {
// 3 - Get the quanity and decrement by one
NSNumber* quantity = [[self orderItems] objectForKey:key];
int intQuantity = [quantity intValue];
intQuantity--;
// 4 - Remove object from array
[[self orderItems] removeObjectForKey:key];
// 5 - Add a new object with updated quantity only if quantity > 0
if (intQuantity > 0)
[[self orderItems] setObject:[NSNumber numberWithInt:intQuantity] forKey:key];
}
}
Do note that when we remove an item from the order, we only need to do something if the object is found in the order. If the item is found, we read the value, decrement it, remove the dictionary object, and reinsert the object with the new quantity if the quantity is greater than 0.
Switch to the IODOrder.h and add the prototypes:
- (void)addItemToOrder:(IODItem*)inItem;
- (void)removeItemFromOrder:(IODItem*)inItem;
Now we can switch to IODViewController.m and add code to the add and remove item stubs to call the newly created helper methods:
- (IBAction)ibaRemoveItem:(id)sender {
IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
[order removeItemFromOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
- (IBAction)ibaAddItem:(id)sender {
IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
[order addItemToOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
For both of these methods, all we do is get the current item from the inventory array, pass that object to the addItemToOrder: or removeItemFromOrder: method that we defined on IODOrder, and update the UI with our helper methods.
Give it another build and run. You should now see that you can add items to the order and that the chalkboard updates to display your order.
UIAnimation
Let's go back and add a little visual flair with another block method. Replace the ibaRemoveItem: and ibaAddItemMethod: code with the following:
- (IBAction)ibaRemoveItem:(id)sender {
IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
[order removeItemFromOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
UILabel* removeItemDisplay = [[UILabel alloc] initWithFrame:ibCurrentItemImageView.frame];
[removeItemDisplay setCenter:ibChalkboardLabel.center];
[removeItemDisplay setText:@"-1"];
[removeItemDisplay setTextAlignment:UITextAlignmentCenter];
[removeItemDisplay setTextColor:[UIColor redColor]];
[removeItemDisplay setBackgroundColor:[UIColor clearColor]];
[removeItemDisplay setFont:[UIFont boldSystemFontOfSize:32.0]];
[[self view] addSubview:removeItemDisplay];
[UIView animateWithDuration:1.0
animations:^{
[removeItemDisplay setCenter:[ibCurrentItemImageView center]];
[removeItemDisplay setAlpha:0.0];
} completion:^(BOOL finished) {
[removeItemDisplay removeFromSuperview];
}];
}
- (IBAction)ibaAddItem:(id)sender {
IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
[order addItemToOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
UILabel* addItemDisplay = [[UILabel alloc] initWithFrame:ibCurrentItemImageView.frame];
[addItemDisplay setText:@"+1"];
[addItemDisplay setTextColor:[UIColor whiteColor]];
[addItemDisplay setBackgroundColor:[UIColor clearColor]];
[addItemDisplay setTextAlignment:UITextAlignmentCenter];
[addItemDisplay setFont:[UIFont boldSystemFontOfSize:32.0]];
[[self view] addSubview:addItemDisplay];
[UIView animateWithDuration:1.0
animations:^{
[addItemDisplay setCenter:ibChalkboardLabel.center];
[addItemDisplay setAlpha:0.0];
} completion:^(BOOL finished) {
[addItemDisplay removeFromSuperview];
}];
}
The above might seem like a lot of code but it's really quite simple. The first new code segment we added just creates a UILabel and sets its properties. The second segment is an animation that moves the label that we just created. This is an example of the Block UIView animation system that we outlined at the beginning of this tutorial.
Compile and run and you'll see a nifty animation which shows the items being added and removed each time you tap the "+1" or "-1" buttons.
Getting the Total
The last helper method we are going to add to IODOrder.m is the method to total the order and return the result.
- (float)totalOrder {
// 1 - Define and initialize the total variable
__block float total = 0.0;
// 2 - Block for calculating total
float (^itemTotal)(float,int) = ^float(float price, int quantity) {
return price * quantity;
};
// 3 - Enumerate order items to get total
[self.orderItems enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
IODItem* item = (IODItem*)key;
NSNumber* quantity = (NSNumber*)obj;
int intQuantity = [quantity intValue];
total += itemTotal(item.price, intQuantity);
}];
// 4 - Return total
return total;
}
Let's go through the above code step-by-step:
- We define and initialize the variable that will accumulate the total. Note the __block keyword. We will be using this variable inside of a Block. If we do not use the __block keyword, the Block we create below would create a const copy of this variable and use that when referenced inside the Block, meaning we would not be able to change the value inside of the Block. By adding this keyword we are able to be read from AND write to the variable inside of the Block.
- Then, we define a Block variable and assign it a Block that simply takes a price and a quantity and returns the item total based on price and quantity.
- This code segment goes over every object in the orderItems dictionary using a Block method, enumerateKeysAndObjectsUsingBlock: and uses the previous Block variable to find the total for each item for the quantity ordered and then adds that to the grand total (which is why we needed the __block keyword on the total variable since it is being modified inside of a Block).
- Once we are done calculating the total for all the items, we simply return the calculated total.
Go back to IODOrder.h and add the prototypes:
- (float)totalOrder;
The last thing to do is to add the total calculation functionality to the app. All the heavy work will be done by the totalOrder method and so, all we have to do is show the calculated total to the user when they hit the total button and trigger the ibaCalculateTotal: action. So fill in the ibaCalculateTotal: stub in IODViewController.m with the following:
- (IBAction)ibaCalculateTotal:(id)sender {
float total = [order totalOrder];
UIAlertView* totalAlert = [[UIAlertView alloc] initWithTitle:@"Total"
message:[NSString stringWithFormat:@"$%0.2f",total]
delegate:nil
cancelButtonTitle:@"Close"
otherButtonTitles:nil];
[totalAlert show];
}
This just gets the total, creates a simple alert view, and shows it to the user.
That's it! Give it a final build and run, and maybe even grab a burger to celebrate! :]
Useful Blocks Cheat Sheet
Before you go, I wanted to let you know about a few block methods that you might find useful.
NSArray
- enumerateObjectsUsingBlock - Probably the Block method I use the most, it basically is a simpler, cleaner foreach.
- enumerateObjectsAtIndexes:usingBlock: - Same as enumerateObjectsUsingBlock: except you can enumerate a specific range of items in the array instead of all the items. The range of items to enumerate is passed via the indexSet parameter.
- indexesOfObjectsPassingTest: - The Block returns an indexset of the the objects that pass a test specified by the Block. Useful for looking for a particular group of objects.
NSDictionary
- enumerateKeysAndObjectsUsingBlock: - Enumerates through a dictionary, passing the Block each key and object.
- keysOfEntriesPassingTest: - Returns a set of the keys corresponding to objects that pass a test specified by the Block.
UIView
- animateWithDuration:animations: - UIViewAnimation Block, useful for simple animations.
- animateWithDuration:completion: - Another UIViewAnimation Block, this version adds a second Block parameter for callback code when the animation code has completed.
Grand Central Dispatch
- dispatch_async - This is the main function for async GCD code.
Creating Your Own Blocks
Also, sometimes you might want to create your own methods that take blocks. Here's some code snippets showing you how you can do that:
// Here's a method that takes a block
- (void)doMathWithBlock:(int (^)(int, int))mathBlock {
self.label.text = [NSString stringWithFormat:@"%d", mathBlock(3, 5)];
}
// Calling that method with a block
- (IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
Since a block is just an Objective-C object, you can store it in a property so you can call it later. This is useful if you want to call the method after some asynchronous task has completed, such as a network task. Here's an example:
// Declare property
@property (strong) int (^mathBlock)(int, int); // Use copy if not using ARC
// Synthesize property
@synthesize mathBlock = _mathBlock;
// Store block so you can call it later
- (void)doMathWithBlock:(int (^)(int, int))mathBlock {
self.mathBlock = mathBlock;
}
// Calling that method with a block
- (IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
// Later on...
- (IBAction)button2Tapped:(id)sender {
self.label.text = [NSString stringWithFormat:@"%d", self.mathBlock(3, 5)];
}
Finally, you can simplify your syntax a bit by using typedefs. Here's the previous example cleaned up a bit with a typedef for the block:
// Create typedef for block
typedef int (^MathBlock)(int, int);
// Create property using typedef
@property (strong) MathBlock mathBlock;
// Synthesize property
@synthesize mathBlock = _mathBlock;
// Method that stores block for use later
- (void)doMathWithBlock:(MathBlock) mathBlock {
self.mathBlock = mathBlock;
}
// Calling that method with a block
- (IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
// Later on...
- (IBAction)button2Tapped:(id)sender {
self.label.text = [NSString stringWithFormat:@"%d", self.mathBlock(3, 5)];
}
Blocks and Autocompletion
One final tip. When you're using a method that takes a block in Xcode, it can autocomplete the block for you, saving yourself time and syntax errors.
For example, type this into Xcode:
NSArray * array;
[array enum
At this point the autocompletion routine will find enumerateObjectsUsingBlock - hit enter to auto-complete that method name. Then hit enter again to auto-complete the block, and it will put in this:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
code
}
You can fill in your code and close out the method call, and viola - much easier than typing everything in!
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// Do something
}];
Where to Go From Here?
The final completed project code can be downloaded here. If you are familiar with git, I also have the project hosted here at github, with commits at each step if you get stuck at a step.
Hopefully in creating this simple app, you've been able to see the power and simplicity that Blocks add to your dev toolkit, and gotten some ideas of how you can use them in your own projects.
If this is your first exposure to blocks, you've taken a pretty big step. With more and more apps needing asynchronous, networked, and multi-threaded code, blocks will definitely be considered required learning.
There's still more to learn about blocks - specifically in terms of variable capture and the __block keyword. More information about blocks and Grand Central Dispatch can be found here:
Thanks for following along with me on my first (of hopefully many) tutorial for this site, and I hope to hear from you in the forums!
This is a blog post by iOS Tutorial Team member Adam Burkepile, a full-time Software Consultant and independent iOS developer. Check out his latest app Pocket No Agenda, or follow him on Twitter.