Chapter     4

Table and Collection View Recipes

All day, every single day, we receive information. Whether in the form of video, radio, music, emails, 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 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 UICollectionView, which brings multicolumn support to the table.

The UITableView, or simply table view, is the most common way of displaying a list of information. As an example, a music app uses a table view to display the songs in a user’s library. A table view comprises configurable cells that make up the individual rows in the table view.

The UICollectionView, or simply collection view, is similar to a UITableView; however, it has columns as well as rows. The grid structure of a collection view makes it more suitable for displaying photos and items of that nature. For example, the content of a photo album in a photo app is an example of a collection view. Collection views also comprise configurable cells, with each cell having a column and a row.

Throughout this chapter, we focus on the step-by-step methodology for creating, implementing, and customizing both types of views.

Recipe 4-1: Creating an Ungrouped Table

You can use two kinds of UITableViews 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, we start from the ground up with an empty application and end with a useful table for displaying information about various countries. Instead of using the storyboard approach as we did in Chapter 2, we’ll be using .xib files to illustrate multiscene development.

In Xcode, create 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 most of this chapter, so give your project whatever name you prefer. Also, be sure to deselect the “Use Core Data” option because we won’t be using it in this chapter. Figure 4-1 shows these options.

9781430259596_Fig04-01.jpg

Figure 4-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 select “With XIB for user interface” option so that Xcode creates a user interface file for your view controller. Refer to Recipe 1-6 for more in-depth instructions about creating a class.

Note   Some might 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 approach is that the UITableView given in the controller’s .xib file is more difficult to configure and reframe. For this reason, in this recipe 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 and resize it so it fits beneath the status bar. This results in the display shown in Figure 4-2.

9781430259596_Fig04-02.jpg

Figure 4-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 shown in Listing 4-1.

Listing 4-1.  Setting the title in MainViewController.m

- (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, which you set up next. This is done in the application delegate, so switch to your AppDelegate.h file and add the code shown in Listing 4-2.

Listing 4-2.  Adding properties to AppDelegate.h

//
//  AppDelegate.h
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#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

Now, switch to AppDelegate.m and add the code in Listing 4-3 to the application:didFinishLaunchingWithOptions: method.

Listing 4-3.  Setting up necessary code for the UINavigationController

- (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 code in Listing 4-3 simply creates an instance for the MainTableViewController and the UINavigationController. The UINavigationController is set up with the MainTableViewController as its root view controller. Then you simply set the window root view controller to the UINavigationController.

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 iOS simulator, you should see a screen like the one in Figure 4-3.

9781430259596_Fig04-03.jpg

Figure 4-3. Basic application with an empty UITableView

Adding a Model for Countries

Next, you will use an array to store the information called upon in order to display your table’s information. Declare it as a property of your view controller, with the type NSMutableArray, and the name “countries,” as shown in the code in Listing 4-4.

Listing 4-4.  Creating an NSMutableArray property for countries

//
//  Country.h
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#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 by using the Objective-C class template. Name your new class “Country,” and make sure 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 class, as shown in Listing 4-5.

Listing 4-5.  Setting country information properties in the new Country.h class

//
//  Country.h
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#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"

Before you proceed to creating the test data, make sure you have downloaded the image files for the flags you will be using for the countries you add. In this recipe, you use flags of the United States, England (as opposed to the United Kingdom), Scotland, France, and Spain. We downloaded some public domain flag images from Wikipedia: the 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 all the files downloaded and visible in the finder, select and drag them into your project in Xcode under Supporting Files. A dialog box appears with options for adding the files to your project. Make sure the option labeled “Copy items into destination group’s folder (if needed)” is checked, as in Figure 4-4.

9781430259596_Fig04-04.jpg

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

Set up your test data—the five countries mentioned earlier—in the viewDidLoad method of the MainTableViewController.m file, as shown in Listing 4-6.

Listing 4-6.  Setting country properties and adding them to the countries array

- (void)viewDidLoad
{
    [super viewDidLoad];

    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 you’ll need to add an outlet named “countriesTableView.” Open the .xib file and control-click and drag from the table view to the interface file, just as you would for a button outlet.

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

Your table view communicates with your program through two protocols: UITableViewDelegate and UITableViewDataSource. You’ll need to add a little bit of code to the interface line to let the class know it’s conforming to these protocols, so add them to its header, as shown in Listing 4-7.

Listing 4-7.  Declaring the use of protocols

//
//  MainTableViewController.h
//  Recipe 4-1 to 4-5: Creating UITableViews
//

#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 shown in Listing 4-8. Setting these properties lets the table view know that the data population and handling of interactions will take place in the MainTableViewController.

Listing 4-8.  Setting the table view delegate and datasource as “self” in MainTableViewController.m

- (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"];
    
    // ...
}

Note   With the table view selected, you can also set up delegate and datasource by dragging from the circles in the connections inspector to “File’s Owner” in the document outline under Placeholders, as shown in Figure 4-5. When working with storyboards, “File’s Owner” becomes the name of the view controller.

9781430259596_Fig04-05.jpg

Figure 4-5. Alternative way of connecting delegate and datasources

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 through 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 return the number of countries in your array, as shown in Listing 4-9.

Listing 4-9.  Implementing the tableView:numberOfRowsInSection: method

-(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. Listing 4-10 is a generic implementation of this method, which you can modify for your data.

Listing 4-10.  Generic implementation of the tableView:cellForRowAtIndexPath: method

- (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 4-6 shows, has a generic title (Cell 0, Cell 1, Cell 2, and so on) and a disclosure accessory indicator. The disclosure accessory indicator is the little gray arrow on the right of the cell that lets the user know that tapping the cell will display details about that cell.

9781430259596_Fig04-06.jpg

Figure 4-6. Your app displaying five cells with 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 will take care of that, as well as customize the look and content of the cells, in a moment, but first we’ll discuss cell reuse.

Considerations for Cached Cells and Reuse

The preceding code deserves some explanation. A table view in iOS tries to save 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 call the dequeueReusableCellWithIdentifier: method to see whether there is a free cell to reuse before you allocate a new one. In the preceding example, you can see that you first attempt to dequeue a reusable cell. If none are available (that is, if the 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, as shown in Listing 4-11.

Listing 4-11.  Updating the display style of the rows

cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitlereuseIdentifier: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 4-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 put on the detail text label.

Next, you can 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, as shown in Listing in 4-12.

Listing 4-12.  Setting the cell text label

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, as shown in Listing 4-13.

Listing 4-13.  Setting the cell subtitle label

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 Listing 4-14 to your cell configuration.

Listing 4-14.  Generic implementation of the tableView:cellForRowAtIndexPath: method

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 shown in Listing 4-15.

Listing 4-15.  Creating a class method to draw UIImage in a given size

+ (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 Listing 4-16.

Listing 4-16.  Creating a private method handler for scale: image: toSize: method

//
//  MainTableViewController.h
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#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, as shown in Listing 4-17.

Listing 4-17.  Utilizing the scale: image: toSize: method to adjust image size

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

After all these configurations, the resulting tableView:cellForRowAtIndexPath: method should look like Listing 4-18.

Listing 4-18.  Completed tableView:cellForRowAtIndexPath: method

- (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 4-7, complete with country information and flag images.

9781430259596_Fig04-07.jpg

Figure 4-7. Your table populated with country information

Implementing the Accessory Views

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, which is 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 will appear that displays all the known information about the selected country.

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

Construct this controller’s view in its .xib file so that it resembles the one shown in Figure 4-8 by using a combination of labels, text fields, and an image view. For this example, we sized the UIImage view to 111 points wide by 68 points high. You can do this easily from the size inspector, with the UIImage view selected.

9781430259596_Fig04-08.jpg

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

Create outlets for the components 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 declaration to its header, as shown in Listing 4-19.

Listing 4-19.  Adding UITextFieldDelegate to the view controller header file

@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, as shown in Listing 4-20.

Listing 4-20.  Importing a country class

#import "Country.h"

Declare the property, as shown in Listing 4-21.

Listing 4-21.  Declaring currentCountry property

@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 additions to your CountryDetailsViewController.h file, as shown in Listing 4-21.

Listing 4-21.  Setting up custom protocol and delegate properties in CountryDetailsViewController.h

//
//  CountryDetailsViewsController.h
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#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>
//...

Create a property for the delegate, as shown in Listing 4-22, so you can later set the owner of the delegate.

Listing 4-22.  Creating a delegate property CountryDetailsViewController.h

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

@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, so let’s start by adding a method to populate the view, as shown in Listing 4-23.

Listing 4-23.  Implementing populateViewWithCountry: method

-(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 the call to the new delegate method to your detailed view controller, as shown in Listing 4-24.

Listing 4-24.  Calling the populateViewWithCountry: delegate from within the viewWillAppear method

-(void)viewWillAppear:(BOOL)animated
{

    [self populateViewWithCountry:self.currentCountry];
}

Next, let’s consider the text fields. You should dismiss the keyboard when the user is done editing, so implement the textFieldShouldReturn: delegate method, as shown in Listing 4-25.

Listing 4-25.  Implementing the textFieldShouldReturn: 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, as shown in Listing 4-26.

Listing 4-26.  Setting the delegates 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 code in Listing 4-27 to the viewDidLoad method.

Listing 4-27.  Creating a navigation bar button for reverting to original data

- (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 the revertButton action is easily implemented. It should merely repopulate the view with the data from the currentCountry property. Add the implementation shown in Listing 4-28 to your CountryDetailsViewController.m file.

Listing 4-28.  Implementing the revert method

-(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. Add the code in Listing 4-29 to the CountryDetailsViewController.m file.

Listing 4-29.  Adding a viewWillDisappear method override

-(void)viewWillDisappear:(BOOL)animated
{
    // End any editing that might 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 to the header the CountryDetailsViewControllerDelegate protocol that you created. You need to import the class you created first.

#import "CountryDetailsViewController.h"

To make your implementation of the CountryDetailsViewController delegate method easier, you should 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 like Listing 4-30, with recent changes marked in bold.

Listing 4-30.  Adding a delegate declaration and an instance variable to MainTableViewController.h

//
//  MainTableViewController.h
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#import <UIKit/UIKit.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 shown in Listing 4-31.

Listing 4-31.  Adding the countryDetailsViewControllerDidFinish: delegate method

-(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 begin and end update 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, as shown in Listing 4-32.

Listing 4-32.  Implementing the tableView: didSelectRowAtIndexPath: method

-(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 a multitude of other delegate methods for dealing with the selection or deselection of a row, which include the following:

  • tableView:willSelectRowAtIndexPath: Lets the delegate know that a row is about to be selected
  • tableView:didSelectRowAtIndexPath: Lets the delegate know that a row was selected
  • tableView:willDeselectRowAtIndexPath: Lets the delegate know that a row is about to be deselected
  • tableView:didDeselectRowAtIndexPath: Lets the delegate know that a row was deselected

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 shown in Figure 4-9.

9781430259596_Fig04-09.jpg

Figure 4-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 info icon 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 info icon 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 make it possible to add and remove check marks from a list as 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 accessory through the accessoryView property to be any other UIView subclass.

Now that you turned your accessory into a button, it is incredibly easy to implement an action to handle this interaction. 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(), as shown in Listing 4-33, although it should be very easy to see how you could implement different actions.

Listing 4-33.  Implementing the tableView: accessoryButtonTappedForRowWIthIndexPath: method

-(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 4-10.

9781430259596_Fig04-10a.jpg

9781430259596_Fig04-10b.jpg

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

Considerations for 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 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 might 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 when a UITableView is enabled so as 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 more detail soon.

Although most developers stick to the generic UITableView because it fits well with the iOS design theme, if you look around the app store you can find some creative implementations using custom views. All this extra customization might add a lot of development time to your project, but a high-quality, custom UITableView certainly stands out in an application for its uniqueness. See cocoacontrols.com or search github.com for code examples of custom table view implementations. When creating custom UITableViews, be sure to be mindful of the impact it might have on the performance of your app.

Recipe 4-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 email application, you can press the “Edit” button in the upper-right corner 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 should consider is putting your UITableView into editing mode, because in order for your users to use your editing functionality, they need to be able 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 line shown in Listing 4-34 to the viewDidLoad method of your main table view controller.

Listing 4-34.  Adding an edit button to the navigation bar

- (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.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, 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, as shown in Listing 4-35.

Listing 4-35.  Implementing the setEditing:animated: override method

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

The main concepts 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 delete any rows from your table yet. To do this, you must first implement one more delegate method, tableView:commitEditingStyle:forRowAtIndexPath:.

Listing 4-36 is a basic implementation of the method that you’ll start with.

Listing 4-36.  Implementing the tableView:commitEditingStyle:editingStyle:forRowAtIndexPath: method

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

It is important that you delete the actual piece of data from your model before removing the row(s) from your UITableView, similar to how in the preceding recipe 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 might throw an exception.

Now when you run your app you can tap the “Edit” button to put your UITableView into editing mode, which will resemble Figure 4-11.

9781430259596_Fig04-11.jpg

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

UITableView Row Animations

In the method you just added, you specified an 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 result in 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 to determine 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 recreate your data every time the application runs, it should be relatively easy to test this. When you are about to delete a row from a table, your table should resemble Figure 4-12.

9781430259596_Fig04-12.jpg

Figure 4-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, as shown in Listing 4-37.

Listing 4-37.  Modifying tableView:editingStyleForRowAtIndexPath: to add insertion

-(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. Add a case to your tableView:commitEditingStyle:forRowAtIndexPath: so the method looks like Listing 4-38.

Listing 4-38.  Adding behavior to handle an “Insertion” button

-(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 an 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 is, 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 4-13.

9781430259596_Fig04-13.jpg

Figure 4-13. Editing a UITableView via 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 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 on a row, but only after you have completed a row’s editing.

Recipe 4-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 is to make it so you can move your rows around. This is 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. Do this by implementing the tableView:canMoveRowAtIndexPath: delegate method, as shown in Listing 4-39.

Listing 4-39.  Implementing the tableView:canMoveRowAtIndexPath: 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, as shown in Listing 4-40.

Listing 4-40.  Implementing the tableView:moveRowAtIndexPath:toIndexPath: method

-(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 now successfully allows you to move and reorder your rows, in addition to the previous deletion and copying functionalities, as in Figure 4-14.

9781430259596_Fig04-14.jpg

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

Recipe 4-4: Creating a Grouped UITableView

Now that you have almost completed 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 number 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, which results in a display similar to the one in Figure 4-15.

9781430259596_Fig04-15.jpg

Figure 4-15. Configuring a “grouped” UITableView

While this is the only action 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 method by which your data is 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.

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

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

Listing 4-41.  Adding NSMutableArray properties to contain country groups

@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 line shown in Listing 4-42.

Listing 4-42.  Line that needs to be removed from the viewDidLoad method

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

Now, add the line in Listing 4-43 in place of the line removed in Listing 4-42 in order to properly organize your countries.

Listing 4-43.  Creating country groups and adding them to the countries array

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 datasource 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 the group’s array in each method. First, change your tableView:cellForRowAtIndexPath, as shown in Listing 4-44.

Listing 4-44.  Updating the tableView:cellForRowAtIndexPath: method

- (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;
}

Next, change the tableView:numberOfRowsInSection:, as shown in Listing 4-45.

Listing 4-45.  Updating the tableView:numberOfRowsInSection: method

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

Listing 4-46 shows the update to tableView:didSelectRowAtIndexPath.

Listing 4-46.  Updating the tableView:didSelectRowAtIndexPath: method

-(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];
}

See the same change in tableView:accessoryButtonTappedForRowWithIndexPath:, shown in Listing 4-47.

Listing 4-47.  Updating the tableView:accessoryButtonTappedForRowWithIndexpath: method

-(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 can 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, as expected in this particular application. For an application where it might be reasonable to have objects change groups, include code to do so accordingly.

Update the code for the tableView:moveRowAtIndexPath:toIndexpath: method, as shown in Listing 4-48.

Listing 4-48.  Updating the tableView:moveRowAtIndexPath:toIndexPath: method

-(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 Listing 4-49.

Listing 4-49.  Updating the tableView:commitEditingStyle:forRowAtIndexPath

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        NSMutableArray *group = [self.countries objectAtIndex:indexPath.section];
        Country *deletedCountry = [groupobjectAtIndex:indexPath.row];
        [groupremoveObject:deletedCountry];
        
        [self.countriesTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    }
    else if (editingStyle == UITableViewCellEditingStyleInsert)
    {
        NSMutableArray *group = [self.countries objectAtIndex:indexPath.section];
        Country *copiedCountry = [groupobjectAtIndex:indexPath.row];
        Country *newCountry = [[Country alloc] init];
        newCountry.name = copiedCountry.name;
        newCountry.flag = copiedCountry.flag;
        newCountry.capital = copiedCountry.capital;
        newCountry.motto = copiedCountry.motto;
        
        [groupinsertObject: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, using the method shown in Listing 4-50.

Listing 4-50.  Implementing the numberOfSectionsInTableView: 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. Add the implementation shown in Listing 4-51.

Listing 4-51.  Implementing the tableView:titleForHeaderInSection: method

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

If your data model were more complicated, you would probably want to have the names of your groups stored somewhere with the groups themselves. A good way to achieve this would be with an NSDictionary, where you would store the names of the groups as the dictionary keys, and the group items would be the dictionary 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 (Listing 4-52) is entirely optional and varies in its use based on the needs of any given application.

Listing 4-52.  Optional method to change the delete button text

-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return NSLocalizedString(@"Remove", @"Delete");
}

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

9781430259596_Fig04-16.jpg

Figure 4-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 at the bottom of your groups. Listing 4-53 shows a quick method to add footers to your UITableView.

Listing 4-53.  A method implementation for adding footers to the groups

-(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 4-17shows the final result of your setup.

9781430259596_Fig04-17.jpg

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

Recipe 4-5: Registering a Custom Cell Class

For a moment, let’s return to the method that’s responsible for creating and initializing a given table view cell. For reference, Listing 4-54 shows the implementation from the previous recipes.

Listing 4-54.  Repeated implementation for initializing a table view cell

- (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 preceding code into the other project.

A better solution would be to make a custom table view cell class of your own so 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 4-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. Open CountryCell.h and add a country property to the class, as shown in Listing 4-55.

Listing 4-55.  Adding a country property to the new CountryCell.m interface

//
//  CountryCell.m
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#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 cells—that is, cell style, accessory type, and the fonts of the two labels, as shown in Listing 4-56.

Listing 4-56.  Overriding the initializer to set up common properties

- (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—a method that controls how a property is set—for the country property. This method 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. Implement the setter as shown in Listing 4-57.

Listing 4-57.  Implementing the custom country property setter method

- (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. Make sure you remove the method declaration in the @interface section of your MainTableViewController as well as the method.

Your complete implementation file should now resemble the code shown in Listing 4-58.

Listing 4-58.  The complete CountryCell.m implementation

//
//  CountryCell.m
//  Recipe 4-1 to 4-5 Creating UITableViews
//

#import "CountryCell.h"

@implementation CountryCell

- (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

To register your cell class, switch to MainTableViewController.m and add the line in Listing 4-59 to its viewDidLoad method. To make it compile, you also need to import CountryCell.h.

Listing 4-59.  Importing the CountryCell class into the MainTableViewController.m file

#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 feature of iOS 7 that changes the semantics of the dequeueReusableCellWithIdentifier: method a little. The new behavior of that method is that if a suitable cached cell object cannot be found, a new cell is created as long as 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, as shown in Listing 4-60.

Listing 4-60.  Updating the tableView:cellForRowAtIndexPath: method to take advantage of the new class

- (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 now your code is a bit better encapsulated and better prepared for reuse.

Recipe 4-6: Creating a Flag Picker Collection View

One great feature that was added in iOS 6 and remains nearly unchanged in iOS 7 is the collection view. It has evolved from the good old table view, not to be its replacement, but rather to be 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 total control of how items are laid out, but offer fewer built-in functions. Collection views offer more possibilities but at the price of more work on the part 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. This class provides a basic multicolumn layout that expands in one direction (that is, supports either horizontal or vertical scrolling).

In this recipe, we’ll show you how to set up 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 the United Kingdom
  • North American flags: Canada, Mexico, and the 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, such as 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 might be a good idea to create a new group folder to host the files, preferably in the Supporting Files folder, as shown in Figure 4-18.

9781430259596_Fig04-18.jpg

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

The next task you need to perform is to set up 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 Main.storyboard file to edit the single view controller and add a label, an image view, and a button to the view in such a way that it resembles Figure 4-19. Initialize the image view with one of your flag images by setting the image view’s image attribute in the attribute inspector.

9781430259596_Fig04-19.jpg

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

Because you’ll change the content of both the label and the image view at runtime, you need outlets to reference them from your 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 Listing 4-61.

Listing 4-61.  The starting ViewController.h file

//
//  ViewController.h
//  Recipe 4-6 Creating a flag picker collection view
//

#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 implementing 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 will set up a simple model to hold the data. You will 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” with NSObject as its parent. Then, in Flag.h, add the code in Listing 4-62 to declare properties and an initialization method for the class.

Listing 4-62.  Declaring properties and an initialization method for the Flag class

//
//  Flag.h
//  Recipe 4-6 Creating a flag picker collection view
//

#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 to add implementation of the initialization method, as shown in Listing 4-63.

Listing 4-63.  Implementing the custom initialization method in Flag.m

//
//  Flag.m
//  Recipe 4-6 Creating a flag picker collection view
//

#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   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. This allows you to initialize the image resource when you access the property with the custom getter. By doing this, you 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, the app will display a collection of flags for the user to choose from. This is the perfect job for a collection view, so let’s set one up.

First, you need a new view controller to handle the collection view, so 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 deselected.

With the new class in place, set up the delegation pattern to use for notifying the main view that a flag has been picked. Go to the header file of the new class and add the code in Listing 4-64.

Listing 4-64.  Setting up the delegation pattern for the FlagPickerViewController

//
//  FlagPickerViewController.h
//  Recipe 4-6 Creating a flag picker collection view
//

#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 originate from, you need six arrays, as shown in Listing 4-65.

Listing 4-65.  Creating instance arrays to hold the flags of each group

//
//  FlagPickerViewController.h
//  Recipe 4-6 Creating a flag picker collection view
//

// ...

@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 implementation for the initialization method, as shown in Listing 4-66.

Listing 4-66.  Adding a custom initialization to the FlagPickerViewController.m file

//
//  FlagPickerViewController.m
//  Recipe 4-6 Creating a flag picker collection view
//

#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, which 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 code in Listing 4-67. Note that you should adjust the code according to which flags you actually downloaded and imported into your project.

Listing 4-67.  Adding and initializing flags in their respective groups

- (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 datasource methods to notify the collection view regarding 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 shown in Listing 4-68 to provide that data.

Listing 4-68.  Adding datasource methods to set the number of sections and items

//
//  FlagPickerViewController.m
//  Recipe 4-6 Creating a flag picker collection view
//

// ...

@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 might have noticed, we did not explicitly assign a datasource 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 a 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 such as section headers and footers, while cell views are the individual items. It’s your job to define these views and register them with the collection view.

You will 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 property declarations to the header file of the new class, as shown in Listing 4-69.

Listing 4-69.  Adding property declarations to the FlagCell.h file

//
//  FlagCell.h
//  Recipe 4-6 Creating a flag picker collection view
//

#import <UIKit/UIKit.h>

@interface FlagCell : UICollectionViewCell

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

@end

In the implementation file, add the initialization code to the initWithFrame: method, as shown in Listing 4-70. 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 changes the color of the background view that’s displayed when the cell is highlighted.

Listing 4-70.  Adding code to FlagCell.m to create and add labels and images, as well to as change the background color

//
//  FlagCell.m
//  Recipe 4-6 Creating a flag picker collection view
//

#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 property declaration to its header file, as shown in Listing 4-71.

Listing 4-71.  Adding a label property to the ContinentHeader.h file

//
//  ContinentHeader.h
//  Recipe 4-6 Creating a flag picker collection view
//

#import <UIKit/UIKit.h>

@interface ContinentHeader : UICollectionReusableView

@property (strong, nonatomic) UILabel *label;

@end

Next, add the initialization code to the implementation file, as shown in Listing 4-72.

Listing 4-72.  Adding initialization code to ContinentHeader.m

//
//  ContinentHeader.m
//  Recipe 4-6 Creating a flag picker collection view
//

#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

To use these two views to display the content, you need to register them with the collection view. Return to the FlagPickerViewController.m file and add the code in Listing 4-73 to the viewDidLoad method (note that the code to set up the flag data has been removed for brevity).

Listing 4-73.  Registering the FlagCell and ContinentHeader views in FlagPickerViewController.m

//
//  FlagPickerViewController.m
//  Recipe 4-6 Creating a flag picker collection view
//

#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 implement the datasource and delegate methods that will create and set them up. Still in the FlagPickerViewController.m file, add the delegate method shown in Listing 4-74.

Listing 4-74.  Implementing the collectionView:cellForItemAtIndexPath: 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 whether it must create a new one if there’s none. No matter what, you can rely on receiving an allocated instance of a cell that you then just update with the current data before you return it to the collection view.

Also, the method uses a helper method, flagForIndexPath:, to get the corresponding flag instance from the data model. The implementation of that helper method is given in Listing 4-75.

Listing 4-75.  Implementing the flagForIndexpath: helper method

-(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 datasource 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 the implementation shown in Listing 4-76.

Listing 4-76.  Implementing the nameForSection: method

- (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";
    }
}

At this point, 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. Do that in the collectionView:collectionViewLayout:sizeForItemAtPath: and the collectionView:collectionViewLayout:referenceSizeForHeaderInSection: datasource methods, as shown in Listing 4-77.

Listing 4-77.  Implementing delegates for setting the size and supplementary views of cells

- (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 consider the width you provide.

The last task you need to complete in the Flag Picker is to add code that notifies the main view when a flag has been picked. This is easy now that the infrastructure is in place. Add the delegate method to FlagPickerViewController, as shown in Listing 4-78.

Listing 4-78.  Adding the collectionView:didSelectItemAtIndexPath: method

-(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 code in Listing 4-79.

Listing 4-79.  Making the main view controller conform to the FlagPickerViewControllerDelegate protocol

//
//  ViewController.h
//  Recipe 4-6 Creating a flag picker collection view
//

#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 implementation, as shown in Listing 4-80.

Listing 4-80.  Implementing the pickFlag action method in ViewController.m

- (IBAction)pickFlag:(id)sender
{
    UICollectionViewController *flagPicker =
        [[FlagPickerViewController alloc] initWithDelegate:self];
    
    [self presentViewController:flagPicker animated:YES completion:NULL];
}

In Listing 4-80, you first create a new UICollectionViewController instance and initialize it with the ViewController class as the delegate. Then you present the new UICollectionViewController as the current view.

Finally, add the method that responds to a selection event from the Flag Picker (Listing 4-81). It simply dismisses the Flag Picker and updates the image view and the label with the new information.

Listing 4-81.  Implementing the flagPicker: didPickFlag: delegate method

-(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 4-20.

9781430259596_Fig04-20.jpg

Figure 4-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 4-21.

9781430259596_Fig04-21.jpg

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

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

9781430259596_Fig04-22.jpg

Figure 4-22. The header files of the Flag Picker are not properly centered in landscape orientation

Using Auto Layout 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 see in Figure 4-22.

To fix that problem, you can use a little Auto Layout magic. Go to ContinentHeader.m and add the code in Listing 4-82 to the initWithFrame: method. What this code does is add an Auto Layout 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.

Listing 4-82.  Adding Auto Layout constraints to fix a rotation problem (additions are in bold)

//
//  ContinentHeader.m
//  Recipe 4-6 Creating a flag picker collection view
//

#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

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

9781430259596_Fig04-23.jpg

Figure 4-23. The continent labels in this app are centered using Auto Layout

Summary

In this chapter, we have shown you two of the core components of the iOS 7 platform: the table view and the 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 level 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 and flexible way possible. Table views and collection views are fantastic tools, but the purpose they serve is far more important, and this is what ultimately will be the final product 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