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 .

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 6 of this article. Click here to view the first page.

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:

  1. 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.
  2. This simply takes the returned indexes and returns the first one.
  3. 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.