Chapter    3

Table and Collection View Recipes

All day, every single day, we receive information. Whether in the form of video, radio, music, e-mails, 140-character messages, or even sights and sounds, there is always new data to acquire and process. As developers, we work to create and manage the medium between this information and the end-users through data organization and display. We must be able to take the immense stream of information available and process it down to simple, concise pieces that our specific audience will be interested in. On top of this, we also have to make our data look visually appealing, while still maintaining efficiency and organization.

In iOS development there are two great tools for achieving these goals: UITableView with its well-known user interface that has pretty much become what users expect from data-based apps, and the brand new UICollectionView, which brings multi-column support to the table. Throughout this chapter, we focus on the step-by-step methodology for creating, implementing, and customizing these useful tools.

Recipe 3-1: Creating an Ungrouped Table

You can use two kinds of UITableView in iOS: the grouped table and the ungrouped table. Your use of one or the other will depend on the requirements of your application, but we start here by focusing on an ungrouped table due to its ease of implementation.

Setting Up the Application

To build a fully functional and customizable UITableView-based application, you start from the ground up with an empty application and end up with a useful table to display information about various countries. In Xcode, make a new project, and select the Empty Application template. This gives you only an application delegate, from which you can build all your view controllers.

You will be using a single project throughout this entire chapter, so rather than naming projects by recipe name, give your project whichever name you prefer. (We chose “Countries” because the application displays information about different countries.) Also, be sure to uncheck the Use Core Data option because we won’t be using it in this chapter. However, and as always in this book, leave the Use Automatic Reference Counting option checked. Figure 3-1 shows these options.

9781430245995_Fig03-01.jpg

Figure 3-1.  Options for the Countries project

Because you started with an empty application, you begin by making your main view controller, which contains your table view.

Create a new file using the Objective-C class template. On the next screen, enter MainTableViewController as the class name and select UIViewController as the subclass. It’s important that you also check the With XIB for user interface option so that Xcode creates a user interface file for your view controller.

Note  Some may find it more convenient to create a subclass of UITableViewController, as you are immediately given a UITableView, as well as some of the methods required to use it. The downside of this method is that the UITableView given in the controller’s .xib file is more difficult to configure and reframe. For this reason, you are using a UIViewController subclass, and you will simply add in your UITableView, and its methods, yourself.

Now, select the MainTableViewController.xib file to bring up Interface Builder. Continue by dragging a table view from the Object library into your view. Rather than making the table take up the entire view, shrink it down a bit and have a 20-point padding around it. Select the main view, and change the background color to a light gray so that you can differentiate it from your UITableView. This results in the display shown in Figure 3-2.

9781430245995_Fig03-02.jpg

Figure 3-2.  A table view with a 20-point padding around it

Now switch to the MainViewController.m file and set the title in the viewDidLoad method, as follows:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.title = @"Countries";
}

The title appears in the navigation bar that you set up next. This is done in the application delegate, so switch to your AppDelegate.h file and add the following code:

//
//  AppDelegate.h
//  Countries
//
#import <UIKit/UIKit.h>
#import "MainTableViewController.h"

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, strong) UINavigationController *navigationController;
@property (nonatomic, strong) MainTableViewController *tableViewController;
@end

Note  Since iOS 6 you no longer need to @synthesize your properties. The new compiler creates getters and setters automatically if they haven’t been declared explicitly. Also, in iOS 6 there’s no need for setting your properties to nil in the viewDidUnload method; yet another scaffolding responsibility has been lifted off our shoulders, freeing up more time to spend on more important creative coding endeavors. Cheers to that!

Now switch to AppDelegate.m and add the following code to the application:didFinishLaunchingWithOptions: method:

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];

    self.tableViewController = [[MainTableViewController alloc] init];
    self.navigationController = [[UINavigationController alloc]

    initWithRootViewController:self.tableViewController];
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    return YES;
}

The application skeleton is now complete. It has a navigation controller with your MainTableViewController as the root view controller. When running the project in the simulator, you should see a screen like the one in Figure 3-3.

9781430245995_Fig03-03.jpg

Figure 3-3.  Basic application with an empty UITableView

Adding a Model for Countries

You use an array to store the information used to display your table’s information. Declare it a property of your view controller, with the type NSMutableArray and the name countries, as shown in the following code.

//
//  MainTableViewController.h
//  Countries
//

#import <UIKit/UIKit.h>

@interface MainTableViewController : UIViewController

@property (strong, nonatomic) NSMutableArray *countries;

@end

In the array you will store objects representing countries, so let’s create a model for that. Create a new file as before, using the Objective-C class template. Name your new class Country, and make sure that it is a subclass of NSObject.

You’ll store four pieces of information in your Country class: name, capital city, motto, and a UIImage that contains the country’s flag. Define these properties in your Country.h, as follows:

//
//  Country.h
//  Countries
//

#import <Foundation/Foundation.h>

@interface Country : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *capital;
@property (nonatomic, strong) NSString *motto;
@property (nonatomic, strong) UIImage *flag;

@end

Now that your model is set up, you can return to your view controller. The compiler needs to access the methods of the new Country class that you have just set up, so add the following import statement to MainTableViewController.h.

#import "Country.h"

Now before you can proceed to create the test data, make sure you have downloaded the image files for the flags that you will be using for the countries you add. In this recipe you use flags of the United States, England (as opposed to the UK), Scotland, France, and Spain. We downloaded some public domain flag images from Wikipedia; United States, France, and Spain from http://en.wikipedia.org/wiki/Gallery_of_sovereign-state_flags , and England and Scotland from http://commons.wikimedia.org/wiki/Flags_of_formerly_independent_states . An image size of around 200 pixels is good enough for your purposes.

Caution  Whenever you are working with images, watch carefully for any and all copyright issues. Public domain images, such as those used here from Wikipedia, are free to use and fairly easy to find.

After you have the files all downloaded and visible in the Finder, select and drag them into your project in Xcode under Supporting Files. A dialog appears with options for adding the files to your project. Make sure that the option labeled Copy items into destination group’s folder (if needed) is checked, as in Figure 3-4.

9781430245995_Fig03-04.jpg

Figure 3-4.  Dialog for adding files; make sure the first option is checked

Set up your test data, the five countries mentioned earlier, in your viewDidLoad method, as follows:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.title = @"Countries";
    Country *usa = [[Country alloc] init];
    usa.name = @"United States of America";
    usa.motto = @"E Pluribus Unum";
    usa.capital = @"Washington, D.C.";
    usa.flag = [UIImage imageNamed:@"usa.png"];

    Country *france = [[Country alloc] init];
    france.name = @"French Republic";
    france.motto = @"Liberté, Égalité, Fraternité";
    france.capital = @"Paris";
    france.flag = [UIImage imageNamed:@"france.png"];

    Country *england = [[Country alloc] init];
    england.name = @"England";
    england.motto = @"Dieu et mon droit";
    england.capital = @"London";
    england.flag = [UIImage imageNamed:@"england.png"];

    Country *scotland = [[Country alloc] init];
    scotland.name = @"Scotland";
    scotland.motto = @"In My Defens God Me Defend";
    scotland.capital = @"Edinburgh";
    scotland.flag = [UIImage imageNamed:@"scotland.png"];

    Country *spain = [[Country alloc] init];
    spain.name = @"Kingdom of Spain";
    spain.motto = @"Plus Ultra";
    spain.capital = @"Madrid";
    spain.flag = [UIImage imageNamed:@"spain.png"];

    self.countries =
        [NSMutableArray arrayWithObjects:usa, france, england, scotland, spain, nil];

}

Displaying Data in a Table View

To display your test data in your table view you need a way to reference it from your code. So go ahead and create an outlet named countriesTableView.

Your table view communicates with your program through two protocols: UITableViewDelegate and UITableViewDataSource. Make your view controller the delegate for both these protocols, so add them to its header, as follows:

//
//  MainTableViewController.h
//  Countries
//

#import < UIKit/UIKit.h>
#import "Country.h"

@interface MainTableViewController : UIViewController < UITableViewDelegate,
                                                      UITableViewDataSource>

@property (weak, nonatomic) IBOutlet UITableView *countriesTableView;
@property (strong, nonatomic) NSMutableArray *countries;

@end

The next step is to connect your view controller to the table view. Switch to MainTableViewController.m and set the table view’s delegate and dataSource properties in the viewDidLoad method, as follows:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.title = @"Countries";
    self.countriesTableView.delegate = self;
    self.countriesTableView.dataSource = self;
    Country *usa = [[Country alloc] init];
    usa.name = @"United States of America";
    usa.motto = @"E Pluribus Unum";
    usa.capital = @"Washington, D.C.";
    usa.flag = [UIImage imageNamed:@"usa.png"];

    // ...
}

For the sake of organization, all the methods that a UITableView can call are split into two groups: delegate methods and data source methods. Delegate methods are used to handle any kind of visual elements of the UITableView, such as the row height of cells. Data source methods, on the other hand, deal with the information displayed in the UITableView, such as the configuration of any given cell’s information.

To create an ungrouped UITableView, you must correctly implement two main methods.

First, you need to specify how many rows will be displayed in the table view. This is done via the tableView:numberOfRowsInSection: method. Your table view has only one section, because it’s ungrouped, so there is no need to consult the section parameter. All you need to do is to return the number of countries in your array:

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.countries count];
}

Second, you must create a method to specify how the UITableView’s cells are configured using the tableView:cellForRowAtIndexPath: method. Following is a generic implementation of this method, which you modify for your data:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        cell.textLabel.font = [UIFont systemFontOfSize:19.0];
        cell.detailTextLabel.font = [UIFont systemFontOfSize:12];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"Cell %i", indexPath.row];

    return cell;
}

If you run your application now, you will see that your table view has five cells, one for each entry in your countries array. Each cell, as Figure 3-5 shows, has a generic title (Cell 0, Cell 1, Cell 2, etc.) and a disclosure accessory indicator.

9781430245995_Fig03-05.jpg

Figure 3-5.  Your app displaying five cells with a generic text and a disclosure accessory indicator

Because you haven’t implemented any functionality for the accessory views yet, nothing happens when you tap the cells. You take care of that, as well as customizing the look and content of the cells in a minute, but first here’s a note on cell reuse.

A Note on Cached Cells

The previous code deserves some explanation. A table view in iOS tries to save up on memory and time by reusing cells that are currently not in view of the user. It takes a cell that has been scrolled out of sight and reuses it to display another cell that has become visible.

However, it’s up to you to make the reuse scheme work. First you must define the different types of cells your table view supports (that is, cells that share the same look and components). Each such cell type is identified by a reuse identifier of your choice.

The second thing your app must do is to call dequeueReusableCellWithIdentifier: method to see whether there is a free cell to reuse before you allocate a new one. In the previous sample, you can see that you first attempt to dequeue a reusable cell. If none are available (that is, if cell is nil), then you create a new cell and give it a generic setup that can be reused for all your cells. Then, no matter whether the cell was dequeued or created, you update the text to the appropriate value.

Configuring the Cells

Now that your application is up, running, and displaying some kind of information, you can work on your specific implementation.

To configure your cells to properly fit your data, the first thing you have to do is change the display style of your rows. Change the allocation/initialization line in your tableView:cellForRowAtIndexPath: method to the following:

cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];

There are four different UITableViewCell styles that you can use, each with a slightly different display:

  • UITableViewCellStyleDefault: Only one label, as shown in Figure 3-5.
  • UITableViewCellStyleSubtitle: Just like the Default style, but with a subtitle line underneath the main text.
  • UITableViewCellStyleValue1: Two text lines, with the primary line on the left side of the cell and the secondary detail text label on the right.
  • UITableViewCellStyleValue2: Two text lines with the focus on the detail text label.

Next, you can actually set the cell’s text label to be the name of the country, rather than simply the count of the cell. Adjust the setting of the cell.textLabel.text property to the following:

Country *item = [self.countries objectAtIndex:indexPath.row];
cell.textLabel.text = item.name;

You can set the subtitle of the text very similarly using the detailTextLabel property of the cell. Set it to the capital of the country.

cell.detailTextLabel.text = item.capital;

The UITableViewCell class also has a property called imageView, which, when given an image, places it to the left of the title label. Implement this action by adding the following line to your cell configuration:

cell.imageView.image = item.flag;

You’ll probably notice that if you run your program now, all your flags will appear, but with varying aspect ratios, making your view look less professional. Setting the frame of the cell’s imageView will not fix this problem, so here is a quick solution.

First, in your view controller implementation file, define a class method that draws a UIImage in a given size, as follows:

+ (UIImage *)scale:(UIImage *)image toSize:(CGSize)size
{
    UIGraphicsBeginImageContext(size);
    [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return scaledImage;
}

Place this method’s handler in your view controller’s private @interface declaration to avoid any potential compiler problems. The private @interface declaration is where you put your private method declarations; it resides at the top of your view controller’s implementation file, as shown in the following code:

//
//  MainTableViewController.m
//  Countries
//

#import "MainTableViewController.h"

@interface MainTableViewController ()

+ (UIImage *)scale:(UIImage *)image toSize:(CGSize)size;

@end

@implementation MainTableViewController

// ...

// Implementation of the scale method goes here
+ (UIImage *)scale:(UIImage *)image toSize:(CGSize)size
{
    // ...
}

@end

Then, you can adjust the image setting lines of code to utilize this method.

cell.imageView.image =
    [MainTableViewController scale: item.flag toSize:CGSizeMake(115, 75)];

After all these configurations, the resulting tableView:cellForRowAtIndexPath: method looks like the following:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        cell.textLabel.font = [UIFont systemFontOfSize:19.0];
        cell.detailTextLabel.font = [UIFont systemFontOfSize:12];
    }
    Country *item = [self.countries objectAtIndex:indexPath.row];
    cell.textLabel.text = item.name;
    cell.detailTextLabel.text = item.capital;
    cell.imageView.image =
        [MainTableViewController scale: item.flag toSize:CGSizeMake(115, 75)];
    return cell;
}

Build and run your application; it should resemble Figure 3-6, complete with country information and flag images.

9781430245995_Fig03-06.jpg

Figure 3-6.  Your table populated with country information

A Note on Rounded Corners

Whenever you look at any well-made iOS application, you will probably notice that almost every single element has its corners rounded. This is one of those small details that most people don’t notice but that dramatically improves the visual quality of an application. Fortunately, it’s actually fairly simple to implement with just two steps.

First, import the Quartz Core API to your view controller’s header file:

#import <QuartzCore/QuartzCore.h>

Once you’ve done that, you can access the layer property of any class that inherits from UIView, which has a cornerRadius property that can be set. Here you’ll go ahead and round the corners on your UITableView by adding the following line to your viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.title = @"Countries";
    self.countriesTableView.delegate = self;
    self.countriesTableView.dataSource = self;
    self.countriesTableView.layer.cornerRadius = 8.0;
    // ...
}

When you run your app now, it resembles Figure 3-7.

9781430245995_Fig03-07.jpg

Figure 3-7.  Your table view with rounded corners

Implementing the Accessory Views

So now that you have a nice looking table with your five countries, you can work on extending beyond the basic functionality of the table view. First, you’ll focus on the most straightforward ability, to act upon the selection of a specific row.

For the purpose of this recipe, you will build your application in such a way that upon the selection of a row, a separate view controller presents that will display all the known information about the selected country.

Start by creating a new view controller like the one you did in the beginning of this recipe, by using the Objective-C class template and UIViewController as parent class. Name the new class CountryDetailsViewController and make sure the With XIB for user interface option is checked.

Construct this controller’s view in its .xib file to resemble the one shown in Figure 3-8 by using a combination of labels, text fields, and an image view. To enhance the look a little, we added a slight shadow to the “Country Name” label using the Attribute inspector.

9781430245995_Fig03-08.jpg

Figure 3-8.  CountryDetailsViewController’s .xib file and configuration

Create outlets for the components that you’ll be changing dynamically (that is, the country label, the image view, and the two text fields). Use the following respective property names:

  • nameLabel
  • capitalTextField
  • mottoTextField
  • flagImageView

You need to be able to manipulate the behavior of the two text fields. To allow your view controller to respond to events from these text fields, add the UITextFieldDelegate protocol to its header.

@interface CountryDetailsViewController : UIViewController <UITextFieldDelegate>

To make your view controller as generic as possible, give it a property of your Country class to hold the currently displayed data. This way, you simply populate your view with the necessary data, and if desired, you could even make it possible to easily repopulate with different data without changing views. Add an import statement for the Country class.

#import "Country.h"

Declare the property as follows:

@property (strong, nonatomic) Country *currentCountry;

Your detailed view controller needs a way to tell whoever invoked it that it’s finished and should be removed from view. The convention in iOS is to set up a custom protocol and a delegate property for that purpose. So make the following additions to your CountryDetailsViewController.h file:

//
//  CountryDetailsViewController.h
//  Countries
//

#import <UIKit/UIKit.h>
#import "Country.h"

// Forward declaration needed for the protocol to use
// the CountryDetailsViewController type
@class CountryDetailsViewController;

@protocol CountryDetailsViewControllerDelegate <NSObject>
-(void)countryDetailsViewControllerDidFinish:(CountryDetailsViewController *)sender;
@end

@interface CountryDetailsViewController : UIViewController <UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UIImageView *flagImageView;
@property (weak, nonatomic) IBOutlet UITextField *capitalTextField;
@property (weak, nonatomic) IBOutlet UITextField *mottoTextField;

@property (strong, nonatomic) Country *currentCountry;
@property (strong, nonatomic) id <CountryDetailsViewControllerDelegate> delegate;

@end

Now, switch your focus to the implementation file of your details view controller. There’s plenty to be done there as well, so let’s start by adding a method to populate the view.

-(void)populateViewWithCountry:(Country *)country
{
    self.currentCountry = country;
    self.flagImageView.image = country.flag;
    self.nameLabel.text = country.name;
    self.capitalTextField.text = country.capital;
    self.mottoTextField.text = country.motto;
}

You will want this method to be called after your view is loaded, but right before your view is displayed, which is when viewWillAppear:animated: is invoked. So add that delegate method to your detailed view controller, as follows:

-(void)viewWillAppear:(BOOL)animated
{
    [self populateViewWithCountry:self.currentCountry];
}

Next, let’s consider the text fields. You want to dismiss the keyboard when the user is done editing, so you should implement the textFieldShouldReturn: delegate method.

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return NO;
}

For the foregoing delegate method to be called, you need to connect your view controller to the delegate properties of the text fields. Do this in the viewDidLoad method.

self.mottoTextField.delegate = self;
self.capitalTextField.delegate = self;

Because you are allowing the user to make changes to your data, you should include a button to revert to the original data to cancel edits. Add this to the right side of your navigation bar by adding the following code to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.mottoTextField.delegate = self;
    self.capitalTextField.delegate = self;

    UIBarButtonItem *revertButton =
        [[UIBarButtonItem alloc] initWithTitle:@"Revert"
                                         style:UIBarButtonItemStyleBordered
                                        target:self
                                        action:@selector(revert)];
    self.navigationItem.rightBarButtonItems =
        [NSArray arrayWithObject:revertButton];

}

The revert selector that you specified as your revertButton’s action is easily implemented. It should merely repopulate the view with the data from the currentCountry property:

-(void)revert
{
    [self populateViewWithCountry:self.currentCountry];
}

The last thing you need to do is implement functionality to save any changes to the given Country upon returning to your MainTableViewController. You implement the method viewWillDisappear:animated: to do this.

-(void)viewWillDisappear:(BOOL)animated
{
    // End any editing that may be in progress at this point
    [self.view.window endEditing: YES];

    // Update the country object with the new values
    self.currentCountry.capital = self.capitalTextField.text;
    self.currentCountry.motto = self.mottoTextField.text;
    [self.delegate countryDetailsViewControllerDidFinish:self];
}

The detailed view controller is finished for now; switch back to the header file of your MainTableViewController and add the CountryDetailsDelegate protocol that you created to the header. You need to import the class you created first.

#import "CountryDetailsViewController.h"

To make your implementation of the CountryDetailsViewController delegate method easier, you want to create an instance variable that refers to the index path of whichever row was selected, so that you can save processing power by refreshing only that row. After you add the variable of type NSIndexPath, called selectedIndexPath, your header file should now look as follows, with recent changes marked in bold:

//
//  MainTableViewController.h
//  Countries
//

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import "Country.h"
#import "CountryDetailsViewController.h"

@interface MainTableViewController : UIViewController <UITableViewDelegate,
    UITableViewDataSource, CountryDetailsViewControllerDelegate>
{
    NSIndexPath *selectedIndexPath;
}

@property (weak, nonatomic) IBOutlet UITableView *countriesTableView;
@property (strong, nonatomic) NSMutableArray *countries;
@end

You can now implement the CountryDetailsViewController’s delegate. Switch to MainTableViewController.m and add the delegate method as follows:

-(void)countryDetailsViewControllerDidFinish:(CountryDetailsViewController *)sender
{
    if (selectedIndexPath)
    {
        [self.countriesTableView beginUpdates];
        [self.countriesTableView reloadRowsAtIndexPaths:
                [NSArray arrayWithObject:selectedIndexPath] withRowAnimation:UITableViewRowAnimationNone];
        [self.countriesTableView endUpdates];
    }
    selectedIndexPath = nil;
}

The beginUpdates and endUpdates methods, though somewhat unnecessary here, are very useful for reloading data in a table view. They specify that any calls to reload data in between those calls should be animated. Because all your reloading of data occurs while the UITableView is offscreen, it is not quite necessary, but it does not harm your application.

Finally, to actually act on the selection of a given row in a UITableView, all you need to do is implement the UITableView’s delegate method tableView:didSelectRowAtIndexPath:

-(void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    selectedIndexPath = indexPath;

    Country *chosenCountry = [self.countries objectAtIndex:indexPath.row];
    CountryDetailsViewController *detailedViewController =
        [[CountryDetailsViewController alloc] init];
    detailedViewController.delegate = self;
    detailedViewController.currentCountry = chosenCountry;

    [self.navigationController pushViewController:detailedViewController animated:YES];
}

The UITableView class also has multiple other methods for dealing with the selection or deselection of a row, including tableView:willSelectRowAtIndexPath: (which is called before its -tableView:didSelectRowAtIndexPath counterpart), as well as tableView:willDeselectRowAtIndexPath: and tableView:didDeselectRowAtIndexPath:. Using these four delegate methods, you can fully customize the behavior of a UITableView to fit any application.

When running this project now, you can view and edit country information, as in Figure 3-9.

9781430245995_Fig03-09.jpg

Figure 3-9.  The resulting display of your CountryDetailsViewController

Enhanced User Interaction

When you’re dealing with applications that focus on UITableViews, you often want to allow the user to access multiple views from the same table. For example, the Phone application on an iPhone has a voicemail tab, which displays a UITableView containing the various voicemails left on the phone. The user can then either play the voicemail by selecting a row from the table or view the contact information of the original caller by selecting a smaller blue button on the right side of the row. You can implement a similar behavior by implementing another UITableView delegate method.

First, you must change the type of “accessory” of the cells in your UITableView. This refers to the icon displayed on the far right side of any given row. In your tableView:cellForRowAtIndexPath: method, find the following line:

cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

Change this value to UITableViewCellAccessoryDetailDisclosureButton. This gives you the blue button that can respond to touches. The four possible values for this property are as follows:

  • UITableViewCellAccessoryNone: Specifies a lack of accessory.
  • UITableViewCellAccessoryDisclosureIndicator: Adds a gray arrow on the right side of a row, as you have been using until now.
  • UITableViewCellAccessoryDetailDisclosureButton: Your most recent choice, which specifies an interaction-enabled button.
  • UITableViewCellAccessoryCheckmark: Adds a checkmark to a given row; this is especially useful in conjunction with the tableView:didSelectRowAtIndexPath: method to add and remove check marks from a list as you find necessary.

Note  Whereas these four available accessory types are pretty useful and cover almost any generic use, it’s certainly easy to think of a reason to want something entirely different over on the right side of your row. You can easily customize a UITableViewCell’s accessory through the accessoryView property to be any other UIView subclass.

Now that you turned your accessory into a button, it is actually incredibly easy to implement an action to handle this interaction. You implement another UITableView delegate method, tableView:accessoryButtonTappedForRowWithIndexPath:. For your testing purposes, make this action exactly the same as that of a row selection, with an extra NSLog(), although it should be very easy to see how you could implement different behavior.

-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    selectedIndexPath = indexPath;

    Country *chosenCountry = [self.countries objectAtIndex:indexPath.row];
    CountryDetailsViewController *detailedViewController =
        [[CountryDetailsViewController alloc] init];
    detailedViewController.delegate = self;
    detailedViewController.currentCountry = chosenCountry;

    NSLog(@"Accessory Button Tapped");
    [self.navigationController pushViewController:detailedViewController animated:YES];
}

When you run this app, tapping the accessory buttons should run your newest functionalities, as shown in Figure 3-10.

9781430245995_Fig03-10.jpg

Figure 3-10.  Your UITableView with detail-disclosure buttons responding to events

A Note on Cell View Customization

Just as with the accessory view, several other parts of a UITableViewCell are customizable by way of their views. The UITableViewCell class includes several properties for other views that you can edit, including the following:

  • imageView: The UIImageView to the left of the textLabel in a cell, as shown by your flags in the previous example; if no image is given to this view, then the cell will appear as if the UIImageView did not exist (as opposed to a blank UIImageView taking up space).
  • contentView: The main UIView of the UITableViewCell, which includes all the text; you may want to customize this to implement a more powerful or versatile UITableViewCell.
  • backgroundView: A UIView set to nil in plain-style tables (like you have used so far), and otherwise for grouped tables; this view appears behind all other views in the table, so it is great for specifically customizing the visual display of the cell.
  • selectedBackgroundView: This UIView is inserted above the backgroundView but behind all other views when a cell is selected. It can also be easily given an alpha animation (fading opacity in or out) by use of the -setSelected:animated: action.
  • multipleSelectionBackgroundView: This UIView acts just like the selectedBackgroundView but is used for when a UITableView is enabled to allow the selection of multiple rows.
  • accessoryView: As discussed earlier, this allows you to create entirely different views for a row’s accessory, so you could implement your own custom display and behavior beyond the preset values.
  • editingAccessoryView: This is similar to the accessoryView property but specifically for when a UITableView is in “editing” mode, which you will see in detail soon.

Although most developers stick to the pretty generic UITableView, because it fits well with the iOS design theme, if you look around you can find some pretty creative implementations using custom views. All this extra customization may add a lot of development time to your project, but a high-quality, custom UITableView certainly stands out in an application for its uniqueness.

Recipe 3-2: Editing a UITableView

If you look at almost any UITableView in an application you commonly use, such as your device’s music player, you’ll probably notice that you can edit the table in some way. In your Music application, you can swipe across a row to reveal a Delete button, which when tapped will remove the item in question; in your Mail application, you can press the Edit button in the upper-rightcorner to allow the selection of multiple messages for deletion, movement, and other functions. Both of these functionalities are based on the concept of editing a UITableView.

The first thing you can look at is the idea of putting your UITableView into editing mode, because for your users to use your editing functionality, they need to access it. Do this by adding an Edit button to the top-right corner of your view. This is surprisingly easy to do by adding the following line to the viewDidLoad method of your main table view controller:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.title = @"Countries";
    self.countriesTableView.delegate = self;
    self.countriesTableView.dataSource = self;
    self.countriesTableView.layer.cornerRadius = 8.0;
    self.navigationItem.rightBarButtonItem = self.editButtonItem;

    // ...
}

This editButtonItem property is not actually a property that you need to define, as it is preset for all UIViewController subclasses. The great thing about this button is that it is programmed not only to call a specific method already but also to toggle its text between Edit and Done.

The editButtonItem by default is set to call the method setEditing:animated: for which you create a simple implementation.

-(void)setEditing:(BOOL)editing animated:(BOOL)animated
{
    [super setEditing:editing animated:animated];
    [self.countriesTableView setEditing:editing animated:animated];
}

The main ideas of this method are simple: first you call the super method, which handles the toggling of the button’s text, and then you set the editing mode of your UITableView according to the parameters given.

At this point, your application’s Edit button triggers the editing mode of the UITableView, allowing you to reveal Delete buttons for any given row. However, because you haven’t actually implemented any behavior for these buttons, you can’t actually delete any rows from your table yet. To do this, you must first implement one more delegate method, tableView:commitEditingStyle:forRowAtIndexPath:.

Following is a pretty basic implementation of the method that you’ll start with:

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        Country *deletedCountry = [self.countries objectAtIndex:indexPath.row];
        [self.countries removeObject:deletedCountry];

        [countriesTableView
            deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
            withRowAnimation:UITableViewRowAnimationAutomatic];
    }
}

It is important that you make sure to delete the actual piece of data from your model before removing the row(s) from your UITableView, like in the previous recipe, when you first deleted a country from the array and then removed its table view row. If you don’t do it in that order, your application may throw an exception.

Now, when you run your app, you can tap the Edit button to put your UITableView into editing mode, resembling Figure 3-11.

9781430245995_Fig03-11.jpg

Figure 3-11.  Your UITableView in editing mode, with functionality for removing rows

UITableView Row Animations

In the method you just added, you specified a specific animation type to be performed on the deletion of a row, called UITableViewRowAnimationAutomatic. The parameter that accepts this value has various other preset values with which you can customize the visual behavior of your rows, including the following:

  • UITableViewRowAnimationBottom
  • UITableViewRowAnimationFade
  • UITableViewRowAnimationLeft
  • UITableViewRowAnimationMiddle
  • UITableViewRowAnimationNone
  • UITableViewRowAnimationRight
  • UITableViewRowAnimationTop

The animation type that you choose won’t make any significant difference in how your application performs, but it can certainly change how an application looks and feels to the user. It’s best to play around with these and see which animation looks best in your application.

At this point, your method should now be able to handle the deletion of rows from your table. Because you wrote your program to re-create your data every time the application runs, it should be pretty easy to test this. When you are about to delete a row from a table, your table resembles Figure 3-12.

9781430245995_Fig03-12.jpg

Figure 3-12.  Deleting a row from a table

But Wait, There’s More!

Deletion is not the only kind of editing that can occur in a UITableView. Although not used quite as often, iOS includes functionality to allow rows to be created and inserted with the same method with which they were deleted.

The default editing style for any row in a UITableView is UITableViewCellEditingStyleDelete, so to implement row insertion, you need to change this. For fun, you will give every other row an “insertion” editing style by implementing the tableView:editingStyleForRowAtIndexPath: method.

-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ((indexPath.row % 2) == 1)
    {
        return UITableViewCellEditingStyleInsert;
    }
    return UITableViewCellEditingStyleDelete;
}

Just as before, you need to specify the behavior to be followed upon the selection of an Insertion button. You add a case to your tableView:commitEditingStyle:forRowAtIndexPath: so the method now looks as follows:

-(void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath

{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        Country *deletedCountry = [self.countries objectAtIndex:indexPath.row];
        [self.countries removeObject:deletedCountry];

        [countriesTableView
            deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
            withRowAnimation:UITableViewRowAnimationAutomatic];
    }
    else if (editingStyle == UITableViewCellEditingStyleInsert)
    {
        Country *copiedCountry = [self.countries objectAtIndex:indexPath.row];
        Country *newCountry = [[Country alloc] init];
        newCountry.name = copiedCountry.name;
        newCountry.flag = copiedCountry.flag;
        newCountry.capital = copiedCountry.capital;
        newCountry.motto = copiedCountry.motto;

        [self.countries insertObject:newCountry atIndex:indexPath.row + 1];

        [self.countriesTableView insertRowsAtIndexPaths:
                [NSArray arrayWithObject:[NSIndexPath indexPathForRow:indexPath.row + 1

                                          inSection:indexPath.section]]
                withRowAnimation:UITableViewRowAnimationRight];
    }

}

You can see that you have chosen a pretty easy implementation for insertion. All you have done is to insert a copy of the selected row. You should note that by changing the index values in this method, you could easily insert objects into nearly any row in the table; it is not necessary to insert into only the following row.

As with the deletion, you must make sure that your data model is updated before your table view, so you add the new Country to your array before you insert the new row into your UITableView.

When running your app and editing your table, you can see both deletion and insertion buttons, as in Figure 3-13.

9781430245995_Fig03-13.jpg

Figure 3-13.  Editing a UITableView with insertion or deletion

You can use two other UITableViewdelegate methods in combination with editing to further customize your application’s behavior. We’ll just mention them quickly here before closing this recipe and going on with reordering of table views.

  • The tableView:willBeginEditingRowAtIndexPath: method allows you to get a kind of “first look” at whichever row was selected for editing, and act accordingly.
  • The tableView:didEndEditingRowAtIndexPath: method can be used as a completion block, in that you can specify any actions you deem necessary to be performed with a row, but only after you have completed a row’s editing.

Recipe 3-3: Reordering a UITableView

Now that we have covered deletion and insertion of rows, the next logical step in terms of functionality of a table would be to make it so that you can move around your rows. This is actually pretty simple to incorporate given how you have set up your application.

First you have to specify which of your rows are allowed to move. You do this in the tableView:canMoveRowAtIndexPath: delegate method:

-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}

We took the easy way out of this by simply making all the rows editable, but you can of course change this depending on your application.

Now you simply need to implement a delegate to update your data model on the successful movement of a row:

-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:
(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    [self.countries exchangeObjectAtIndex:sourceIndexPath.row
        withObjectAtIndex:destinationIndexPath.row];
    [self.countriesTableView reloadData];
}

Just as with insertion, you must make sure to correct your array to match the reordering, but the UITableView handles the actual swapping of rows automatically.

For extra control over the reordering of the table, you can implement an extra method called tableView:targetIndexPathForMoveFromRowAtIndexPath:. This delegate method is called every time a cell is dragged over another cell as a possible movement, and its normal use is for “retargeting” a destination row. In this way, you can check the proposed destination and either confirm the proposed move or reject it and return a different destination.

Although you haven’t implemented functionality to confirm or reject your proposed movements, your application can now successfully move and reorder your rows in addition to your previous deletion and copying functionalities, as in Figure 3-14.

9781430245995_Fig03-14.jpg

Figure 3-14.  Your table with a reordering of cells feature

Recipe 3-4: Creating a Grouped UITableView

Now that you have nearly completely gone through all the basics of using an ungrouped UITableView, you can adjust your application to consider a “grouped” approach. All the functionalities you implemented with an ungrouped table also apply to a grouped one, so you will not have to make a great deal of changes.

The absolute first thing you need to do to use a grouped table is to switch the “style” of the UITableView from “plain” to “grouped.” The easiest way to do this is in your view controller’s .xib file by selecting your UITableView and changing the style in the Attribute inspector, resulting in a display similar to the one in Figure 3-15.

9781430245995_Fig03-15.jpg

Figure 3-15.  Configuring a “grouped” UITableView

While this is the only thing necessary to change the style of your table, the problem is that until now, your data model has been formatted for an ungrouped style. You don’t have your data grouped at all. To remedy this problem, you will change the organization with which your data are stored.

Rather than having one array containing all five of your countries, you will separate your countries into their groups, with each group being an NSMutableArray, and then put these arrays into a larger NSMutableArray. (Although a better practice would be to make these immutable, we have chosen a mutable version to make editing your data model from the table a simpler process.)

For your application, divide your five Country objects into two categories: one of countries in the United Kingdom and one of all the others.

First, you need to create two more NSMutableArrays to be your subarrays, so add these two properties to MainTableViewController.h. You will end up with a total of three NSMutableArray properties.

@property (strong, nonatomic) NSMutableArray *countries;
@property (strong, nonatomic) NSMutableArray *unitedKingdomCountries;
@property (strong, nonatomic) NSMutableArray *nonUKCountries;

Now  change your viewDidLoad method to accommodate this change. Delete the following line from this method:

self.countries =
    [NSMutableArray arrayWithObjects:usa, france, england, scotland, spain, nil];

Now replace that line with the following to properly organize your countries:

    self.unitedKingdomCountries = [NSMutableArray arrayWithObjects:england, scotland, nil];
    self.nonUKCountries = [NSMutableArray arrayWithObjects:usa, france, spain, nil];
    self.countries = [NSMutableArray arrayWithObjects:self.unitedKingdomCountries, self.nonUKCountries, nil];

Now comes the slightly tricky part where you have to make sure all your data source and delegate methods are adjusted to your new format. First, you have to include a retrieval of the group’s array, and then you have to retrieve a specific country from there in each method. First, change your tableView:cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
        cell.textLabel.font = [UIFont systemFontOfSize:19.0];
        cell.detailTextLabel.font = [UIFont systemFontOfSize:12];
    }

    NSArray *group = [self.countries objectAtIndex:indexPath.section];
    Country *item = [group objectAtIndex:indexPath.row];
    cell.textLabel.text = item.name;
    cell.detailTextLabel.text = item.capital;
    cell.imageView.image =
        [MainTableViewController scale: item.flag toSize:CGSizeMake(115, 75)];

    return cell;
}

Up next is tableView:numberOfRowsInSection:.

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSArray *group = [self.countries objectAtIndex:section];
    return [group count];

}

Here is tableView:didSelectRowAtIndexPath:.

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    selectedIndexPath = indexPath;

    NSArray *group = [self.countries objectAtIndex:indexPath.section];
    Country *chosenCountry = [group objectAtIndex:indexPath.row];
    CountryDetailsViewController *detailedViewController =
        [[CountryDetailsViewController alloc] init];
    detailedViewController.delegate = self;
    detailedViewController.currentCountry = chosenCountry;

    [self.navigationController pushViewController:detailedViewController animated:YES];
}

And the same change in tableView:accessoryButtonTappedForRowWithIndexPath:.

-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    selectedIndexPath = indexPath;

    NSArray *group = [self.countries objectAtIndex:indexPath.section];
    Country *chosenCountry = [group objectAtIndex:indexPath.row];
    CountryDetailsViewController *detailedViewController =
        [[CountryDetailsViewController alloc] init];
    detailedViewController.delegate = self;
    detailedViewController.currentCountry = chosenCountry;

    NSLog(@"Accessory Button Tapped");
    [self.navigationController pushViewController:detailedViewController animated:YES];
}

For the tableView:moveRowAtIndexPath:toIndexPath: method, you will make a quick assumption that you are moving only rows that are in the same section to make your coding easier. Notice when you run the application later that this actually works well. As with your current implementation, the UITableView does not allow a Country to switch groups, just as expected in this particular application. For an application where it may be reasonable to have objects change groups, include code to do so accordingly.

-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:
(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    //Assume same Section
    NSMutableArray *group = [self.countries objectAtIndex:sourceIndexPath.section];
    if (destinationIndexPath.row < [group count])
    {
        [group exchangeObjectAtIndex:sourceIndexPath.row
            withObjectAtIndex:destinationIndexPath.row];
    }

    [self.countriesTableView reloadData];
}

The last method you must fix is tableView:commitEditingStyle:forRowAtIndexPath:, which looks like so:

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        NSMutableArray *group = [self.countries objectAtIndex:indexPath.section];
        Country *deletedCountry = [group objectAtIndex:indexPath.row];
        [group removeObject:deletedCountry];

        [countriesTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    }
    else if (editingStyle == UITableViewCellEditingStyleInsert)
    {
        NSMutableArray *group = [self.countries objectAtIndex:indexPath.section];
        Country *copiedCountry = [group objectAtIndex:indexPath.row];
        Country *newCountry = [[Country alloc] init];
        newCountry.name = copiedCountry.name;
        newCountry.flag = copiedCountry.flag;
        newCountry.capital = copiedCountry.capital;
        newCountry.motto = copiedCountry.motto;

        [group insertObject:newCountry atIndex:indexPath.row + 1];

        [self.countriesTableView insertRowsAtIndexPaths:
            [NSArray arrayWithObject:[NSIndexPath indexPathForRow:indexPath.row + 1
             inSection:indexPath.section]]
             withRowAnimation:UITableViewRowAnimationRight];
    }
}

Finally, because you did switch your UITableView over to a grouped style, you need to implement just two extra methods to ensure correct functionality.

First, you need to specify how many sections your UITableView will have with the following method:

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.countries count];
}

Second, you should specify headers for each section, which will basically be the titles for your groups. Because you already know how your data is formatted, this is pretty easy to do.

-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    if (section == 0)
    {
        return @"United Kingdom Countries";
    }
    return @"Non-United Kingdom Countries";
}

If your data model was more complicated, you would probably want to have the names of your groups stored somewhere with the groups themselves. Using an NSDictionary would be a particularly good way to use this by making the headers, as strings, the keys for your NSArray group objects.

The UITableViewDelegate protocol also includes a method that allows the developer to customize the text displayed in a Delete button when editing a UITableView. This method is entirely optional and varies in its use based on the needs of any given application.

-(NSString *)tableView:(UITableView *)tableView
titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath

{
    return NSLocalizedString(@"Remove", @"Delete");
}

After all these changes, running your app should result in a view similar to the one in Figure 3-16.

9781430245995_Fig03-16.jpg

Figure 3-16.  Your application with grouped items and section headers

As a final embellishment for your table, you can also add footers to your sections. These work just like headers, but, as you might guess, they appear on the bottom of your groups. Here’s a quick method to add footers to your UITableView.

-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
    if (section == 0)
        return @"United Kingdom Countries";
    return @"Non-United Kingdom Countries";
}

In keeping with all the other customizable parts of a UITableView, these headers and footers are also easily customized beyond a simple NSString. If you use the methods tableView:viewForHeaderInSection: and tableView:viewForFooterInSection:, you can programmatically create your own subview to be used as a header or footer, allowing for full control over your UITableView’s display.

At this point, you now have a fully functional grouped UITableView, complete with all the same abilities as your ungrouped one! Figure 3-17shows the final result of your setup.

9781430245995_Fig03-17.jpg

Figure 3-17.  Your completed grouped UITableView with both headers and footers

Recipe 3-5: Registering a Custom Cell Class

Let’s for a minute return to the method that’s responsible for creating and initializing a given table view cell. For reference, here’s the implementation from the previous recipes:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                       reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
        cell.textLabel.font = [UIFont systemFontOfSize:19.0];
        cell.detailTextLabel.font = [UIFont systemFontOfSize:12];
    }
    NSArray *group = [self.countries objectAtIndex:indexPath.section];
    Country *item = [group objectAtIndex:indexPath.row];
    cell.textLabel.text = item.name;
    cell.detailTextLabel.text = item.capital;
    cell.imageView.image =
        [MainTableViewController scale: item.flag toSize:CGSizeMake(115, 75)];
    return cell;
}

The code follows a common implementation pattern of the tableView:cellForRowAtIndexPath: method and it does its job well. However, there are a couple of problems with it. For one thing, it’s quite long and it’s not obvious from a quick glance what it does. A more serious problem is that it’s not particularly reusable; if you create another application and want similar-looking table view cells, your only option is to copy and paste the foregoing code into the other project.

A better solution would be to make a custom table view cell class of your own so that you can reuse it between projects, or even within one project if it contains several table views. A custom class could also make the setup code significantly simpler and more self-explanatory. Recipe 3-5 shows you how to change the Country project’s current implementation of the tableView:cellForRowAtIndexPath: method into one that utilizes a custom table view cell class.

Creating a Custom Table View Cell Class

Start by creating a new class using the Objective-C class template. Name the new class CountryCell and make it a subclass of UITableViewCell. Now, open CountryCell.h and add a country property to the class, like so:

//
//  CountryCell.h
//  Countries
//

#import <UIKit/UIKit.h>
#import "Country.h"

@interface CountryCell : UITableViewCell

@property (strong, nonatomic) Country *country;

@end

Now, switch to the CountryCell.m file. The designated initializer of table view cells is the initWithStyle:reuseIdentifier: method. Override this method and provide the initialization that is common for all country cellsthat is, cell style, accessory type, and the fonts of the two labels.

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:UITableViewCellStyleSubtitle
            reuseIdentifier:reuseIdentifier];
    if (self)
    {
        // Initialization code
        self.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
        self.textLabel.font = [UIFont systemFontOfSize:19.0];
        self.detailTextLabel.font = [UIFont systemFontOfSize:12];

    }
    return self;
}

Next, we’re going to implement a special setter method for the country property that updates the parts of the cell that are different for each country. These are the text label, the detailed text label, and the flag image. You implement the setter as follows:

- (void)setCountry:(Country *)country
{
    if (country != _country)
    {
        _country = country;
        self.textLabel.text = _country.name;
        self.detailTextLabel.text = _country.capital;
        self.imageView.image =
            [CountryCell scale: _country.flag toSize:CGSizeMake(115, 75)];
    }
}

If you try to compile the code now it will fail because it doesn’t recognize the scale:toSize: class method, which is currently declared in MainTableViewController. In a real scenario you’d probably want to move the method to some kind of helper class that is shared throughout your application, but for the purpose of this recipe it’s sufficient to move it from MainTableViewController into your CountryCell class.

Your complete implementation file should now resemble the code that follows.

//
//  CountryCell.m
//  Countries
//

#import "CountryCell.h"

@implementation CountryCell

@synthesize country = _country;

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
    if (self)
    {
        // Initialization code
        self.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
        self.textLabel.font = [UIFont systemFontOfSize:19.0];
        self.detailTextLabel.font = [UIFont systemFontOfSize:12];
    }
    return self;
}
+ (UIImage *)scale:(UIImage *)image toSize:(CGSize)size
{
    UIGraphicsBeginImageContext(size);
    [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return scaledImage;
}
- (void)setCountry:(Country *)country
{
    if (country != _country)
    {
        _country = country;
        self.textLabel.text = _country.name;
        self.detailTextLabel.text = _country.capital;
        self.imageView.image =
            [CountryCell scale: _country.flag toSize:CGSizeMake(115, 75)];
    }
}

@end

Your custom table view cell class is now ready to be used from your table view controller.

Registering Your Cell Class

Switch to MainTableViewController.m and add the following line to its viewDidLoad method. To make it compile, you also need to import CountryCell.h.

#import "MainTableViewController.h"
#import "CountryCell.h"

@implementation MainTableViewController

// ...

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.title = @"Countries";
    self.countriesTableView.delegate = self;
    self.countriesTableView.dataSource = self;
    self.countriesTableView.layer.cornerRadius = 8.0;
    self.navigationItem.rightBarButtonItem = self.editButtonItem;

    [self.countriesTableView registerClass:CountryCell.class
        forCellReuseIdentifier:@"CountryCell"];


    // ...
}

The preceding code registers your CountryCell class with the table view. This uses a new feature of iOS 6 that changes the semantics of the dequeueReusableCellWithIdentifier: method a little. The new behavior of that method is that if a suitable cached cell object could not be found, a new cell is created if a registered class with the given identifier exists.

It’s now time to reap the benefits of your changes and implement the tableView:cellForRowAtIndexPath: method, which at this point can be shrunk into only four lines of code:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CountryCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CountryCell"];
    NSArray *group = [self.countries objectAtIndex:indexPath.section];
    cell.country = [group objectAtIndex:indexPath.row];
    return cell;
}

If you build and run your code now, it should work just like before. But your code is a little bit better encapsulated and better prepared for reuse.

Recipe 3-6: Creating a Flag Picker Collection View

One of the great new features of iOS 6 is the Collection View. It has evolved from the good old Table View but not to be its replacement, but rather as a natural complement. Unlike Table Views, which display data in a single column and have a lot of built-in functionality based on that layout, Collection Views provide a total control of how items are laid out but offer less built-in functions. It offers more possibilities but at the price of more work on the behalf of the developer.

The exceptional flexibility of the Collection View comes from a total separation between the view and its layout. What this means is that you can get full control of the layout of items by providing a custom Layout object. However, Apple provides a ready to use Layout class that will work in most situations. It provides a basic multicolumn layout that expands in one direction (that is, either supports horizontal or vertical scrolling).

In this recipe we’ll show you how to setup a Collection View with this built-in Layout class, called UICollectionViewFlowLayout. You’ll use it to create a picker view in which a user can browse a collection of flags and select one of them.

Setting Up the Application

Because this app displays a collection of flags, you should start by gathering some flag images for your test data. Download as many flag images as you like from http://en.wikipedia.org/wiki/Gallery_of_sovereign-state_flags , but to make sense there should be at least 15 of them from several continents. As a reference, we downloaded the following flags:

  • African flags: Ghana, Kenya, Morocco, Mozambique, Rwanda, and South Africa
  • Asian flags: China, India, Japan, Mongolia, Russia, and Turkey
  • Australasian flags: Australia and New Zealand
  • European flags: France, Germany, Iceland, Ireland, Italy, Malta, Poland, Spain, Sweden, and United Kingdom
  • North American flags: Canada, Mexico, and United States
  • South American flags: Argentina, Brazil, and Chile

Tip  To keep the app size down, download the 200-pixel PNG format of the flags. This format and size is found by clicking the flag image on the Wikipedia flag gallery, which takes you to a page with links to the available sizes and formats for the flag image. It’s also recommended that you change their file names to contain only the name of the country, for example, France.png.

Now, create a new single-view application and add the flag images to the project. An easy way to do this is to gather the flag images in a folder and drag it onto the Project Navigator. It may be a good idea to create a new group folder to host the files, preferably in the Supporting Files folder as shown in Figure 3-18.

9781430245995_Fig03-18.jpg

Figure 3-18.  An application with flag resource images in a group folder of their own

The next thing you’re going to do is to setup a simple user interface that displays a big flag and the name of the country it belongs to. A button below the flag will allow the user to select a different flag using the Flag Picker that you’ll soon build. Select the ViewController.xib file to bring up Interface Builder, add a label, an image view and a button to the view in such a way that it resembles Figure 3-19. Initialize the image view with one of your flag images by setting the image view’s Image attribute in the Attribute Inspector.

9781430245995_Fig03-19.jpg

Figure 3-19.  A simple user interface that displays a country name and its flag

Now, since you’ll change the content of both the label and the image view at runtime, you need outlets to reference them from you code. Create these outlets and name them countryLabel and flagImageView, respectively. Similarly, create an action named pickFlag for when the user taps the button.

Your ViewController.h file should now resemble this code:

//
//  ViewController.h
//  Flag Picker Collection View Example
//

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet UILabel *countryLabel;
@property (weak, nonatomic) IBOutlet UIImageView *flagImageView;

- (IBAction)pickFlag:(id)sender;

@end

You’re now going to leave the main user interface for a while and instead turn to implement the Flag Picker View Controller that will be displayed from the pickFlag: action method. But before you can do that you need to create a simple data model that you can use to transfer data between the picker and the main view.

Creating a Data Model

In this recipe you’re going to setup a really simple model to hold the data. You’re going to create a class that holds an image of a flag and the name of the country it belongs to. Create a new Objective-C class named Flag and with NSObject as its parent. Then, in Flag.h, add the following code to declare properties and an initialization method for the class:

//
//  Flag.h
//  Flag Picker Collection View Example
//

#import <Foundation/Foundation.h>

@interface Flag : NSObject

@property (strong, nonatomic)NSString *name;
@property (strong, nonatomic)UIImage *image;
- (id)initWithName:(NSString *)name imageName:(NSString *)imageName;


@end

Now, switch to Flag.m for the implementation of the initialization method:

//
//  Flag.m
//  Flag Picker Collection View Example
//

#import "Flag.h"

@implementation Flag

- (id)initWithName:(NSString *)name imageName:(NSString *)imageName
{
    self = [super init];
    if (self) {
        self.name = name;
        NSString *imageFile = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
        self.image = [[UIImage alloc] initWithContentsOfFile:imageFile];
    }
    return self;
}


@end

Note that the initWithName:imageName: method loads the image resource file into memory. In a real scenario you’d probably want to use lazy initialization in a custom property getter to defer the loading until the image is actually requested. But for this recipe, loading the flag file on creation is fine.

You’re now ready to move on and start implementing the Flag Picker.

Building the Flag Picker

When the user taps the Change Flag button of the user interface, what you’re going to do is display a collection of flags for the user to choose between. This is the perfect job for a Collection View so let’s set up one.

First you need a new view controller to handle the Collection View so go ahead and create a new subclass of UICollectionViewController. Name the new class FlagPickerViewController. You do not need an .xib file to handle its user interface so make sure the option With XIB for user interface is unchecked.

Now with the new class in place you can go on and set up the delegation pattern to use to notify the main view that a flag has been picked. Go to the header file of the new class and add the following code:

//
//  FlagPickerViewController.h
//  Flag Picker Collection View Example
//

#import <UIKit/UIKit.h>
#import "Flag.h"

@class FlagPickerViewController;

@protocol FlagPickerViewControllerDelegate <NSObject>

-(void)flagPicker:(FlagPickerViewController *)flagPicker didPickFlag:(Flag *)flag;

@end


@interface FlagPickerViewController : UICollectionViewController

- (id)initWithDelegate:(id <FlagPickerViewControllerDelegate>)delegate;

@property (weak, nonatomic)id <FlagPickerViewControllerDelegate > delegate;


@end

You also need some instance variables to hold the available flags. Because you are going to group the flags according to which continent they stem from, you need six arrays:

//
//  FlagPickerViewController.h
//  Flag Picker Collection View Example
//

// ...

@interface FlagPickerViewController : UICollectionViewController
{
@private
    NSArray *africanFlags;
    NSArray *asianFlags;
    NSArray *australasianFlags;
    NSArray *europeanFlags;
    NSArray *northAmericanFlags;
    NSArray *southAmericanFlags;


}

- (id)initWithDelegate:(id <FlagPickerViewControllerDelegate>)delegate;

@property (weak, nonatomic)id <FlagPickerViewControllerDelegate> delegate;

@end

Now, switch to the FlagPickerViewController.m file and add the following implementation for the initialization method:

//
//  FlagPickerViewController.m
//  Flag Picker Collection View Example
//

#import "FlagPickerViewController.h"

@implementation FlagPickerViewController

- (id)initWithDelegate:(id <FlagPickerViewControllerDelegate>)delegate
{
    UICollectionViewFlowLayout *layout =
        [[UICollectionViewFlowLayout alloc] init];
    self = [super initWithCollectionViewLayout:layout];
    if (self)
    {
        self.delegate = delegate;
    }
    return self;
}


// ...

@end

As you can see from the preceding code, the method creates a layout object to handle the positioning of the items. We’re using the built-in UICollectionViewFlowLayout that provides a simple multicolumn layout that flows in one direction (horizontally by default). The method also sets the delegate property that you will use later to notify the invoker that a selection has been made.

Next create the collection of available flags. Find the viewDidLoad method and add the following code. (Note that you should adjust the code according to which flags you actually downloaded and imported into your project.)

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.

    africanFlags = [NSArray arrayWithObjects:
        [[Flag alloc] initWithName:@"Ghana" imageName:@"Ghana"],
        [[Flag alloc] initWithName:@"Kenya" imageName:@"Kenya"],
        [[Flag alloc] initWithName:@"Morocco" imageName:@"Morocco"],
        [[Flag alloc] initWithName:@"Mozambique" imageName:@"Mozambique"],
        [[Flag alloc] initWithName:@"Rwanda" imageName:@"Rwanda"],
        [[Flag alloc] initWithName:@"South Africa" imageName:@"South_Africa"],
        nil];

    asianFlags = [NSArray arrayWithObjects:
        [[Flag alloc] initWithName:@"China" imageName:@"China"],
        [[Flag alloc] initWithName:@"India" imageName:@"India"],
        [[Flag alloc] initWithName:@"Japan" imageName:@"Japan"],
        [[Flag alloc] initWithName:@"Mongolia" imageName:@"Mongolia"],
        [[Flag alloc] initWithName:@"Russia" imageName:@"Russia"],
        [[Flag alloc] initWithName:@"Turkey" imageName:@"Turkey"],
        nil];

    australasianFlags = [NSArray arrayWithObjects:
        [[Flag alloc] initWithName:@"Australia" imageName:@"Australia"],
        [[Flag alloc] initWithName:@"New Zealand" imageName:@"New_Zealand"],
        nil];

    europeanFlags = [NSArray arrayWithObjects:
        [[Flag alloc] initWithName:@"France" imageName:@"France"],
        [[Flag alloc] initWithName:@"Germany" imageName:@"Germany"],
        [[Flag alloc] initWithName:@"Iceland" imageName:@"Iceland"],
        [[Flag alloc] initWithName:@"Ireland" imageName:@"Ireland"],
        [[Flag alloc] initWithName:@"Italy" imageName:@"Italy"],
        [[Flag alloc] initWithName:@"Poland" imageName:@"Poland"],
        [[Flag alloc] initWithName:@"Russia" imageName:@"Russia"],
        [[Flag alloc] initWithName:@"Spain" imageName:@"Spain"],
        [[Flag alloc] initWithName:@"Sweden" imageName:@"Sweden"],
        [[Flag alloc] initWithName:@"Turkey" imageName:@"Turkey"],
        [[Flag alloc] initWithName:@"United Kingdom" imageName:@"United_Kingdom"],
        nil];

    northAmericanFlags = [NSArray arrayWithObjects:
        [[Flag alloc] initWithName:@"Canada" imageName:@"Canada"],
        [[Flag alloc] initWithName:@"Mexico" imageName:@"Mexico"],
        [[Flag alloc] initWithName:@"United States" imageName:@"United_States"],
        nil];

    southAmericanFlags = [NSArray arrayWithObjects:
        [[Flag alloc] initWithName:@"Argentina" imageName:@"Argentina"],
        [[Flag alloc] initWithName:@"Brazil" imageName:@"Brazil"],
        [[Flag alloc] initWithName:@"Chile" imageName:@"Chile"],
        nil];

}

Collection Views follow the same data pattern as Table Views, meaning that they allow data to be grouped into sections. As you’ll see, the delegate methods to notify the Collection View how many sections there are and how many items they contain are very similar to the ones used for Table Views. Add the following two methods to provide that data:

//
//  FlagPickerViewController.m
//  Flag Picker Collection View Example
//

// ...

@implementation FlagPickerViewController

// ...

-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return 6;
}

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    switch (section) {
        case 0:
            return africanFlags.count;
        case 1:
            return asianFlags.count;
        case 2:
            return australasianFlags.count;
        case 3:
            return europeanFlags.count;
        case 4:
            return northAmericanFlags.count;
        case 5:
            return southAmericanFlags.count;

        default:
            return 0;
    }
}


@end

Note  As you may have noticed, we did not explicitly assign a data source delegate for the Collection View. The Collection View Controller class handles this automatically; if you don’t provide a specific delegate object it will assign itself to the task. This is true for both the UICollectionViewDelegate and the UICollectionViewDataSource properties of the Collection View.

Now you have set up the Collection View so that it knows how many sections and how many items it should display. The next step is to let the Collection View know how to display the items. This is done by creating and registering cell views and so called supplementary view.

Defining the Collection View Interface

A Collection View delegates the actual displaying of items and section specific details to views provided by you. These views are called Cell Views and Supplementary Views. Supplementary Views are things like section headers and footers, while Cells are the individual items. It’s your job to define these views and register them with the Collection View.

You’re going to set up these cells programmatically, and you’ll start with the Cell View. It should contain a thumbnail image of a flag and a small label displaying the country name. Start by creating a new UICollectionViewCell subclass with the name FlagCell. Then add the following property declarations to the header file of the new class:

//
//  FlagCell.h
//  Flag Picker Collection View Example
//

#import <UIKit/UIKit.h>

@interface FlagCell : UICollectionViewCell

@property (strong, nonatomic) UILabel *nameLabel;
@property (strong, nonatomic) UIImageView *flagImageView;


@end

In the implementation file, add the following initialization code to the initWithFrame: method. The code basically does two things: it creates and adds a label and an image view to the content view of the cell, and it change the color of the background view that’s displayed when the cell is highlighted:

//
//  FlagCell.m
//  Flag Picker Collection View Example
//

#import "FlagCell.h"

@implementation FlagCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.nameLabel =
            [[UILabel alloc] initWithFrame:CGRectMake(0, 56, 100, 19)];
        self.nameLabel.textAlignment = NSTextAlignmentCenter;
        self.nameLabel.backgroundColor = [UIColor clearColor];
        self.nameLabel.textColor = [UIColor whiteColor];
        self.nameLabel.font = [UIFont systemFontOfSize:12.0];
        [self.contentView addSubview:self.nameLabel];

        self.flagImageView =
            [[UIImageView alloc] initWithFrame:CGRectMake(6, 6, 88, 49)];
        [self.contentView addSubview:self.flagImageView];

        self.selectedBackgroundView = [[UIView alloc] initWithFrame:frame];
        self.selectedBackgroundView.backgroundColor = [UIColor grayColor];

    }
    return self;
}
@end

Now, you’re going to repeat the process for the header supplementary view. It’ll simply contain a label to display the name of the continent in the header of the respective section. Create a new class, this time as a subclass of UICollectionReusableView and with the name ContinentHeader. Then add the following property declaration to its header file:

//
//  ContinentHeader.h
//  Flag Picker Collection View Example
//

#import <UIKit/UIKit.h>

@interface ContinentHeader : UICollectionReusableView

@property (strong, nonatomic) UILabel *label;
@end

Then add the following initialization code to the implementation file:

//
//  ContinentHeader.m
//  Flag Picker Collection View Example
//

#import "ContinentHeader.h"

@implementation ContinentHeader

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.label = [[UILabel alloc] initWithFrame:
            CGRectMake(0, 0, frame.size.width, frame.size.height)];
        self.label.font = [UIFont systemFontOfSize:20];
        self.label.textColor = [UIColor whiteColor];
        self.label.backgroundColor = [UIColor clearColor];
        self.label.textAlignment = NSTextAlignmentCenter;
        [self addSubview:self.label];

    }
    return self;
}

@end

Now, to be able to use these two views to display the content you need to register them with the Collection View. Go back to the FlagPickerViewController.m file and add the following code to the viewDidLoad method (note that the code to set up the flag data has been removed for brevity):

//
//  FlagPickerViewController.m
//  Flag Picker Collection View Example
//

#import "FlagPickerViewController.h"
#import "FlagCell.h"
#import "ContinentHeader.h"


@implementation FlagPickerViewController

// ...
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.

    // ...

    [self.collectionView registerClass:FlagCell.class
        forCellWithReuseIdentifier:@"FlagCell"];
    [self.collectionView registerClass:ContinentHeader.class
        forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
        withReuseIdentifier:@"ContinentHeader"];

}

// ...

@end

With the cell and supplementary views registered you can go ahead and implement the delegate methods that will create and set them up. Still in the FlagPickerViewController.m file, add the following delegate method:

-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
FlagCell *cell =
        [collectionView dequeueReusableCellWithReuseIdentifier:@ "FlagCell"
        forIndexPath:indexPath];
    Flag *flag = [self flagForIndexPath:indexPath];
    cell.nameLabel.text = flag.name;
    cell.flagImageView.image = flag.image;
    return cell;
}

The dequeueReusableCellWithReuseIdentifier: method looks to see whether it can reuse an already created cell or create a new one if there’s none. No matter what, you can rely on receiving an allocated instance of a cell that you just update with the current data before you return it to the Collection View.

Also, the this method uses a helper method, flagForIndexPath: to get the corresponding Flag instance from the data model. The implementation of that helper method is as follows:

-(Flag *)flagForIndexPath:(NSIndexPath *)indexPath
{
    switch (indexPath.section) {
        case 0:
            return [africanFlags objectAtIndex:indexPath.row];
        case 1:
            return [asianFlags objectAtIndex:indexPath.row];
        case 2:
            return [australasianFlags objectAtIndex:indexPath.row];
        case 3:
            return [europeanFlags objectAtIndex:indexPath.row];
        case 4:
            return [northAmericanFlags objectAtIndex:indexPath.row];
        case 5:
            return [southAmericanFlags objectAtIndex:indexPath.row];
        default:
            return nil;
    }
}

Now, the corresponding delegate method for the supplementary view (the section header) looks like the code that follows. Add it to the FlagPickerViewController class as well:

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
    ContinentHeader *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"ContinentHeader" forIndexPath:indexPath];
    headerView.label.text = [self nameForSection:indexPath.section];
    return headerView;
}

Again, you are using a helper method to get data from your data model. This time you query the name of a section using this implementation:

- (NSString *)nameForSection:(NSInteger)index
{
    switch (index)
    {
        case 0:
            return @"African Flags";
        case 1:
            return @"Asian Flags";
        case 2:
            return @"Australasian Flags";
        case 3:
            return @"European Flags";
        case 4:
            return @"North American Flags";
        case 5:
            return @"South American Flags";
        default:
            return @"Unknown";
    }
}

Now, what’s remaining before the defining of the Collection View user interface is complete, is to set the sizes of the cells and supplementary views. You do that in the collectionView:collectionViewLayout:sizeForItemAtPath: and the collectionView:collectionViewLayout:referenceSizeForHeaderInSection: delegate methods, respectively:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(100, 75);
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
{
    return CGSizeMake(50, 50);
}

Note  When you set the size of a header or footer in a Collection View Flow Layout, only one dimension is considered. For example, if the flow is vertical, only the height component of the CGSize is used to determine the actual size of the supplementary view. The width is instead inferred by the width of the Collection View. The converse is true for horizontal flows which only considers the width you provide.

The last thing you need to do in the Flag Picker is to add code to notify the main view when a flag has been picked. This is easy now that the infrastructure is in place. Add the following method to FlagPickerViewController:

-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    Flag *selectedFlag = [self flagForIndexPath:indexPath];
    [self.delegate flagPicker:self didPickFlag:selectedFlag];
}

You’re done with the implementation of the Flag Picker. Now it’s time to turn your focus back to the main view.

Displaying the Flag Picker

To use the Flag Picker you just built, you first need to prepare the main view controller to be a Flag Picker delegate. Making it conform to the FlagPickerViewControllerDelegate protocol you defined earlier does this. Switch to ViewController.h and add the following code:

//
//  ViewController.h
//  Flag Picker Collection View Example
//

#import <UIKit/UIKit.h>
#import "FlagPickerViewController.h"

@interface ViewController : UIViewController <FlagPickerViewControllerDelegate>

@property (weak, nonatomic) IBOutlet UILabel *countryLabel;
@property (weak, nonatomic) IBOutlet UIImageView *flagImageView;

- (IBAction)pickFlag:(id)sender;

@end

Finally, you can now implement the pickFlag: action method. Go to ViewController.m and add the following implementation:

- (IBAction)pickFlag:(id)sender
{
    UICollectionViewController *flagPicker =
        [[FlagPickerViewController alloc] initWithDelegate:self];

    [self presentViewController:flagPicker animated:YES completion:NULL];

}

Finally, add the method that responds to a selection event from the Flag Picker. It simply dismisses the Flag Picker and updates the image view and the label with the new information:

-(void)flagPicker:(FlagPickerViewController *)flagPicker didPickFlag:(Flag *)flag
{
    self.flagImageView.image = flag.image;
    self.countryLabel.text = flag.name;
    [self dismissViewControllerAnimated:YES completion:NULL];
}

You now can build and run your application. When you tap the Change Flag button you should be presented with a view resembling the one in Figure 3-20.

9781430245995_Fig03-20.jpg

Figure 3-20.  A Collection View displaying a set of flags

You can scroll among the flags and select one that will then be used to update the main view, such as in Figure 3-21.

9781430245995_Fig03-21.jpg

Figure 3-21.  An updated main view after a flag has been selected in the Flag Picker

However, there is one small problem that you’ll notice if you rotate the device (by pressing image ) and activate the Flag Picker. As Figure 3-22 shows, the section headers are no longer centered.

9781430245995_Fig03-22.jpg

Figure 3-22.  The header files of the Flag Picker don’t get properly centered in landscape orientation

Using Autolayout to Center the Headers

The reason the header labels don’t stay centered is that they are created with a fixed size. This works as long as the app stays in portrait mode, but when the screen rotates, the static sized labels don’t follow, causing the effect you saw in Figure 3-22.

To fix that problem you can use a little Autolayout magic. Go to ContinentHeader.m and add the following code to the initWithFrame: method:

//
//  ContinentHeader.m
//  Flag Picker Collection View Example
//

#import "ContinentHeader.h"

@implementation ContinentHeader

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.label = [[UILabel alloc] initWithFrame:
            CGRectMake(0, 0, frame.size.width, frame.size.height)];
        self.label.font = [UIFont systemFontOfSize:20];
        self.label.textColor = [UIColor whiteColor];
        self.label.backgroundColor = [UIColor clearColor];
        self.label.textAlignment = NSTextAlignmentCenter;
        [self addSubview:self.label];

        self.label.translatesAutoresizingMaskIntoConstraints = NO;
        NSDictionary *viewsDictionary =
            [[NSDictionary alloc] initWithObjectsAndKeys:
             self.label, @"label", nil];
        [self addConstraints:
            [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label]|"
            options:0 metrics:nil views:viewsDictionary]];

    }
    return self;
}
@end

What we just did was to add an Autolayout constraint that instructs the label to expand to the width of its parent (the Header View) and stay that way even if the parent’s frame changes.

If you run the app now, you’ll see that the change has the desired effect on the headers. They are now centered even in landscape orientation as shown in Figure 3-23.

9781430245995_Fig03-23.jpg

Figure 3-23.  The continent labels in this app are centered using Autolayout

Summary

In this chapter we have shown you two of the core components of the iOS 6 platform: the Table View and the new Collection View. We have shown you how to set them up and use them within your app. We have also given you a glimpse of the amount of customization control the developer has over these views, though the full Apple documentation has a great deal more to say on the subject.

However, the key to Table Views and Collection Views is not how they work, but the data they present. It is up to you as a developer to find the information that users want or need and present it to them in the most efficient, flexible way possible. Table Views and Collection Views are fantastic tools, but the purpose they serve is far more important, and this is what will ultimately be the final product that you deliver to your customers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset