Chapter 9

UITableView Recipes

All day, every single day, we are receiving information. Whether in the form of video, radio, music, e-mails, 140-character messages, or even sights and sounds, there is always new data to acquire and process. As developers, we work to create and manage the medium between this information and the end users through data organization and display. We must be able to take the immense stream of information available and process it down to simple, concise pieces that our specific audience will be interested in. On top of this, we also have to make our data look visually appealing, while still maintaining efficiency and organization. In iOS development, one of our greatest tools in this goal is the UITableView: an incredibly flexible yet simple interface designed to be easy to use for both developers and customers. Throughout this chapter, we will focus on the step-by-step methodology for creating, implementing, and customizing these useful tools.

Recipe 9–1: Creating an Ungrouped Table

There are two kinds of UITableViews you can use in iOS: the grouped table and the ungrouped table. Your use of one or the other will depend on any given application, but you will start by focusing on an ungrouped table due to its ease of implementation.

In order to build a fully functional and customizable UITableView-based application, you will be starting from the ground up with an empty application, and ending up with a useful table to display information about various countries. Make a new project, and select the Empty Application template, as in Figure 9–1. This will give you only an application delegate, from which you can build all of your view controllers.

Image

Figure 9–1. Selecting an empty application to start from scratch

You will be using a single project throughout this entire chapter, so, rather than naming projects by recipe name, give your project whichever name you prefer (I chose “Countries”, since the application will be focused on displaying information about different countries), and create your project. I have used the class prefix “Main”, which will apply only to your app delegate files since you are using an empty application.

Since you used an empty application, you will start by making your main view controller, which will contain your UITableView.

Create a new file, and select the UIViewController Subclass template.

On the next screen, enter a name for your view controller, and make sure that the class is listed as a subclass of UIViewController. Name it “MainTableViewController”.

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

Since you will be focusing on the idea of a table in your application, you will start by dragging a UITableView out from the library into your view. Rather than making the table take up the entire view, you will shrink it down a bit to have a 20-point padding around it. Switch to the main view, and change the background color to a light gray so that you can differentiate it from your UITableView. This will result in the display shown in Figure 9–2.

Image

Figure 9–2. Configuring a UITableView in a XIB file

Connect your UITableView to a property in your header file using the property name tableViewCountries.

Next, switch over to your view controller’s header file. You will need your view controller to conform to a couple of protocols in order to fully implement your UITableView. Add the UITableViewDelegate and UITableViewDataSource protocols, so your header now looks like so:

    @interface MainTableViewController : UIViewController <UITableViewDelegate,
UITableViewDataSource>

Next, you will need a property of type NSArray to store the information that you will use to display your table’s information, called countries. Make sure to properly synthesize this array, and set its pointer equal to nil in your -viewDidUnload method.

    @property (strong, nonatomic) NSMutableArray *countries;

Before you continue to implement your view controller, you need to set up your model to store your data.

Create a new file as before, but choose the “Objective-C class” template. Name your class “Country”, and make sure that it is a subclass of NSObject, as in Figure 9–3.

Image

Figure 9–3. Creating your Country class as a subclass of NSObject

For your application, you will make your Country objects have four properties: three NSStrings referring to a country’s name, capital city, and motto, and a UIImage that will contain the country’s flag. Define these properties in your Country.h header file like so:

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

As with your view controllers, you need to synthesize all these properties in your implementation file. Unlike your view controllers, however, you do not need to set them equal to nil in any method, since there is no -viewdidUnload method.

    @synthesize name, capital, motto, flag;

Now that your model is set up, you can return to your view controller. The compiler will need to be able to access the methods of the new Country class that you have just set up, so add the following import statement to the header of your view controller.

    #import "Country.h"

Now, you can set up your data to be used in your UITableView. Before you proceed, make sure you have downloaded the image files for the flags that you will be using for the countries you add. Here, I will use those of the United States, England (as opposed to the UK), Scotland, France, and Spain. Here I have used some public domain flag images from Wikipedia, more of which are available at http://en.wikipedia.org/wiki/Gallery_of_country_flags.

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.

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

Image

Figure 9–4. Dialog for adding files; make sure the first box is checked.

In your -viewDidLoad method, you will make your testing data with the five countries I mentioned earlier by adding the following code:

    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"];

Make sure to add all these Country objects to your array with the following line:

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

Now that your testing data is all set up, you will focus on the construction of your UITableView through the use of its delegate and data source methods. Start by setting these two properties to your view controller in your -viewDidLoad with the following lines.

    self.tableViewCountries.delegate = self;
    self.tableViewCountries.dataSource = self;

You can also set the title for the view controller, which will appear at the top your of navigation bar with a simple command.

    self.title = @"Countries";

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

In order to correctly create an ungrouped UITableView, there are two main methods that you must correctly implement.

First, you need to specify to your UITableView how many rows will be displayed via the -tableView:numberOfRowsInSection: method.

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

Since your table is ungrouped, you have only one section, so this method is nice and easy.

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

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

    return cell;
}

Whenever you are dealing with a UITableView, it is pretty much always a good idea to “reuse” cells. Since most of the time not all the cells in a UITableView will be currently in the view of the user, you are able to reuse any cells that are not currently being displayed through the use of the UITableView method -dequeueReusableCellWithIdentifier:, allowing you to save on both memory and time, since you can perform any generic setup, such as font size, background color, etc., only on the initial creation of the cell.

In the previous sample, you can see that you first attempt to de-queue a reusable cell. If none are available (i.e., if cell is nil), then you create a new cell and give it a generic setup that can be reused for all of your cells. Then, no matter whether the cell was de-queued or created, you update the text to the appropriate value.

It is even possible to set up multiple differently configured cells, and specify which one is used or reused via the CellIdentifier.

The last task you need to do to get your program running is ensure that the application delegate, at the start of your program, will present your view controller. Xcode did not do this for you already because you chose the empty template.

In your application delegate implementation, import the header file of your view controller so that the compiler doesn’t complain.

    #import "MainTableViewController.h"

You will need to set up properties in your application delegate’s header file to store both your UINavigationController and your main view controller. Create these like so:

    @property (nonatomic, strong) UINavigationController *navcon;
    @property (nonatomic, strong) MainTableViewController *tableVC;

Make sure to synthesize both with the following line in your application delegate implementation file.

    @synthesize navcon, tableVC;

Now you just need to create your UINavigationController to display and manage the view controller and add its view as a subview of your application’s window. Overall, your -application:didFinishLaunchingWithOptions: method should look like so:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
    self.tableVC = [[MainTableViewController alloc] init];
    self.navcon = [[UINavigationController alloc] initWithRootViewController:tableVC];
    
    [self.window addSubview:navcon.view];
    [self.window makeKeyAndVisible];
    return YES;
}

Upon running this project in the simulator, you will see a basic view of a UITableView with some generic information, as in Figure 9–5.

Image

Figure 9–5. Basic application with a UITableView

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

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

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

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

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

Of these four styles, only the UITableViewCellStyleDefault style has only one line of text.

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

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

All you had to do here was grab the respective Country object, and call the -name method on it that was synthesized.

You can set the subtitle of the text very similarly using the detailTextLabel property of the cell.

    cell.detailTextLabel.text = [(Country *)[self.countries objectAtIndex:indexPath.row]
capital];

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

    cell.imageView.image = [(Country *)[self.countries objectAtIndex:indexPath.row]
flag];

You’ll probably notice that if you run your program now, all of your flags will appear, but with widely 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, define a method that will redraw a UIImage into a given size, like so:

+ (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 header file to avoid any potential compiler problems. This handler will be written like so:

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

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

UIImage *flag = [(Country *)[self.countries objectAtIndex:indexPath.row] flag];
cell.imageView.image = [MainTableViewController scale:flag toSize:CGSizeMake(115, 75)];

After all these configurations, your newly configured -tableView:cellForRowAtIndexPath: method should resemble the following:

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

Your resulting application, if you run it, should resemble Figure 9–6, complete with country information and flag images!

Image

Figure 9–6. Your table populated with country information

A Note on Rounded Corners

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

First, add the following import line to your view controller’s header file:

    #import <QuartzCore/QuartzCore.h>

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

    self.tableViewCountries.layer.cornerRadius = 8.0;
Image

Figure 9–7. Your UITableView with newly rounded corners

So now that you have a nice little table with your five countries set up and looking good, you can work on extending beyond the basic functionality of the UITableView. First, you’ll focus on the most straightforward ability of a UITableView: 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 is presented that will display all the known information about the selected country.

First, create a new file, and choose the “UIViewController subclass” template as before, naming it “CountryInfoViewController”.

Construct this controller’s view in its XIB file to resemble the one shown in Figure 9–8 by using a combination of UILabels, UITextFields, and a UIImageView. I have added a slight shadow to the “Country Title” UILabel as shown in Figure 9–8 through the use of the Attribute inspector, which, though optional, adds quite a bit to the visual design of the layout.

Image

Figure 9–8. CountryInfoViewController’s XIB file and configuration

Connect each UITextField, the UIImageView, and the top “Country Name” UILabel to your view controller with the following respective property names:

  • nameLabel
  • textFieldCapital
  • textFieldMotto
  • imageViewFlag

After switching over to your new view controller’s header file, add the UITextFieldDelegate protocol to the header, since you will need to be able to manipulate the behavior of your UITextFields.

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

#import "Country.h"

Create the Country property like so, and then make sure to synthesize it in the implementation file and set it to nil in -viewDidUnload.

@property (strong, nonatomic) Country *currentCountry;

You will later be implementing a delegate method for this CountryInfoViewController to be able to call, so create a protocol for this by adding the following class and protocol declarations before the header declaration.

@class CountryInfoViewController;

@protocol CountryInfoDelegate <NSObject>
-(void)countryInfoViewControllerDidFinish:(CountryInfoViewController *)countryVC;
@end

Now you will add a delegate property to your CountryInfoViewController, making sure that it is required to conform to the protocol you just created. Make sure to synthesize and nullify it just as with any other property.

@property (strong, nonatomic) id <CountryInfoDelegate> delegate;

In entirety, your header file should resemble the following code.

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

@class CountryInfoViewController;
@protocol CountryInfoDelegate <NSObject>

-(void)countryInfoViewControllerDidFinish:(CountryInfoViewController *)countryVC;

@end

@interface CountryInfoViewController : UIViewController <UITextFieldDelegate>

@property (strong, nonatomic) IBOutlet UILabel *nameLabel;
@property (strong, nonatomic) IBOutlet UIImageView *imageViewFlag;
@property (strong, nonatomic) IBOutlet UITextField *textFieldCapital;
@property (strong, nonatomic) IBOutlet UITextField *textFieldMotto;

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

@end

Now, in the CountryInfoViewController implementation file, create a method to populate the view.

-(void)populateViewWithCountry:(Country *)country
{
    self.currentCountry = country;
    
    self.imageViewFlag.image = country.flag;
    self.nameLabel.text = country.name;
    self.textFieldCapital.text = country.capital;
    self.textFieldMotto.text = country.motto;
}

You will want this method to be called after your view is loaded, but right before your view is displayed, so you will implement the -viewWillAppear:animated: method like so:

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

You will want to be able to dismiss the keyboard after editing your UITextFields, so implement the -textFieldShouldReturn: delegate method.

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

In your -viewDidLoad, configure the two UITextFields by setting their delegates to the view controller.

self.textFieldMotto.delegate = self;
self.textFieldCapital.delegate = self;

Since you are allowing the user to make changes to your data, you should include a button to “Revert” back to the original data before it has been overwritten. You will add this to the right side of your navigation bar by adding the following code to the -viewDidLoad method.

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

self.navigationItem.rightBarButtonItems = [NSArray arrayWithObject:revertButton];

NOTE: The rightBarButtonItems property of the UINavigationItem class is a new addition to iOS 5. It allows the user to set multiple objects to appear in the right side of a UINavigationBar. This was possible in previous versions of iOS, but was slightly more difficult as it required a custom-viewed UIBarButtonItem made out of UIToolbar containing the desired items.

Your entire -viewDidLoad method should look like this:

- (void)viewDidLoad
{
    [super viewDidLoad];
        
    self.textFieldMotto.delegate = self;
    self.textFieldCapital.delegate = self;
    
    UIBarButtonItem *revertButton = [[UIBarButtonItem alloc] initWithTitle:@"Revert"
style:UIBarButtonItemStyleBordered target:self action:@selector(revert)];
    self.navigationItem.rightBarButtonItems = [NSArray arrayWithObject:revertButton];
}

The selector “revert” that you specified as your revertButton’s action is easily implemented:

-(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 will implement your -viewWillDisappear:animated: to do this.

-(void)viewWillDisappear:(BOOL)animated
{
    self.currentCountry.capital = self.textFieldCapital.text;
    self.currentCountry.motto = self.textFieldMotto.text;
    [self.delegate countryInfoViewControllerDidFinish:self];
}

Switch back over to the header file of your MainTableViewController, and add the CountryInfoDelegate protocol that you created to the header. You will need to import the class you created first.

#import "CountryInfoViewController.h"

To make your implementation of the CountryInfoViewController delegate method easier, you will want to create an instance variable that will refer 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 so:

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

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

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

@end

You can now implement the CountryInfoViewController’s delegate method like so:

-(void)countryInfoViewControllerDidFinish:(CountryInfoViewController *)countryVC
{
    if (selectedIndexPath)
    {
        [tableViewCountries beginUpdates];
        [self.tableViewCountries reloadRowsAtIndexPaths:[NSArray arrayWithObject:selectedIndexPath] withRowAnimation:UITableViewRowAnimationNone];
        [tableViewCountries endUpdates];
    }
    selectedIndexPath = nil;
}

The -beginUpdates and -endUpdates methods, though unnecessary here, are very useful for reloading data in a UITableView, as they specify that any calls to reload data in between them should be animated. Since all of your reloading of data occurs while the UITableView is off-screen, this is not quite necessary, but it does not harm your application.

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

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    selectedIndexPath = indexPath;
    
    Country *chosenCountry = [self.countries objectAtIndex:indexPath.row];
    CountryInfoViewController *infoVC = [[CountryInfoViewController alloc] init];
    infoVC.delegate = self;
    infoVC.currentCountry = chosenCountry;
    
    [self.navigationController pushViewController:infoVC animated:YES];
}

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

Upon running this project now, you will able to view and edit country information, as in Figure 9–9.

Image

Figure 9–9. The resulting display of your CountryInfoViewController

Enhanced User Interaction

When you’re dealing with applications that focus on UITableViews, you often may want to allow the user to access multiple different 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, by selecting a smaller blue button on the right side of the row, view the contact information of the original caller. 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 will give us the nice little blue button that can respond to touches. The four possible values for this property are as follows:

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

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

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

-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    selectedIndexPath = indexPath;
    
    Country *chosenCountry = [self.countries objectAtIndex:indexPath.row];
    CountryInfoViewController *infoVC = [[CountryInfoViewController alloc] init];
    infoVC.delegate = self;
    infoVC.currentCountry = chosenCountry;
    
    NSLog(@"Accessory Button Tapped");
    [self.navigationController pushViewController:infoVC animated:YES];
}

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

Image

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

A Note on Cell View Customization

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

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

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

Recipe 9–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 in order to reveal a Delete button, which can then remove an item from a table. In your Mail application, you can press the Edit button in the upper right-hand 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 can look at is the idea of putting your UITableView into “editing” mode, since for your users to be able to use your editing functionality, they need to be able to access it. You will do this by adding an Edit button to the top right-hand corner of your view. Surprisingly enough, this is very easy to do by adding the following line to your -viewDidLoad method.

self.navigationItem.rightBarButtonItem = self.editButtonItem;

This editButtonItem property is not actually a property that you need to define, as it is pre-set for every UIViewController subclass. The especially great thing about this button is that it is programmed not only to call a specific method already, but also to toggle its text between “Edit” and “Done”.

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

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

The main ideas of this method are simple, in that first you call the super method, which will handle 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 will trigger the editing mode of the UITableView, allowing you to reveal the Delete buttons for any given row. However, since you haven’t actually implemented any behavior for these buttons, you won’t actually be able to delete any rows from your table yet. To do this, you must first implement one more delegate method, -tableView:commitEditingStyle:forRowAtIndexPath:.

Here’s a pretty basic implementation of this method that you’ll start with:

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

It is very important in this method that you make sure to delete the actual piece of data from your model before removing the row(s) from your UITableView, just like how in the previous example you first delete the country from your array, and then remove its row. Otherwise, your application will throw an exception.

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

Image

Figure 9–11. Your UITableView in editing mode, with functionality for removing rows

UITableView Row Animations

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

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

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

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

Image

Figure 9–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. While 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 in order to implement row insertion, you need to change this. For fun, you will give every other row an “insertion” editing style by implementing the -tableView:editingStyleForRowAtIndexPath: method.

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

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

-(void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        Country *deletedCountry = [self.countries objectAtIndex:indexPath.row];
        [self.countries removeObject:deletedCountry];
        
        [tableViewCountries 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.tableViewCountries insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:indexPath.row+
1 inSection:indexPath.section]] withRowAnimation:UITableViewRowAnimationRight];
    }
}

You can see that you have gone with a pretty easy implementation for insertion, in that all you have done is inserted a copy of the row selected. It should be noted that by changing the index values in this method, you could easily insert objects to 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.

Upon running your app and editing your table, you will be able to see both deletion and insertion buttons, as in Figure 9–13.

Image

Figure 9–13. Editing a UITableView with insertion or deletion

There are two other UITableView delegate methods that can be used in combination with editing to further customize your application’s behavior.

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

Recipe 9–3: Re-ordering a UITableView

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

First, you have to specify exactly which of your rows are allowed to move using -tableView:canMoveRowAtIndexPath:.

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

I’ve chosen the easy way out of this by simply making them all editable, but you can easily change this depending on your application.

Now, you simply need to implement a delegate to update your data model upon the successful movement of a row.

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

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

For extra control over the re-ordering 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 it or reject the proposed move and return a different destination.

Although you haven’t implemented functionality to confirm or reject your proposed movements, your application will now be able to successfully move and re-order your rows in addition to your previous deletion and copying functionalities, as in Figure 9–14.

Image

Figure 9–14. Your table with some re-ordering of cells

Recipe 9–4: Creating a Grouped UITableView

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

The absolute first thing you need to do in order to use a grouped table is to switch the “Style” of the UITableView from “Plain” to “Grouped”. The easiest way to do this is in your view controller’s XIB file by selecting your UITableView, and changing the style in the Attribute inspector, resulting in a display similar to Figure 9–15.

Image

Figure 9–15. Configuring a “grouped” UITableView

The specific option you are looking for is in the top of your Attribute inspector under the “Style” of the “Table View”, as shown here in Figure 9–16.

Image

Figure 9–16. Modifying the table’s “Style” to create a grouped UITableView

While this is the only thing necessary in order to change the style of your table, the problem is that up until now, your data model has been formatted for an ungrouped style. You don’t even have your data grouped at all. To remedy this, you will change the organization with 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. (Although a better practice would be to make these immutable, I have chosen a mutable version to make editing your data model from the table a more simple process.)

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

First, you need to create two more NSMutableArrays to be your subarrays, so add these two properties, making sure to properly handle them (synthesize!) in your implementation file. You will end up with a total of three NSMutableArray properties.

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

Now you’ll change your -viewDidLoad method to accommodate this change. Delete the following line from this method:

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

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

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

Now is the slightly tricky part, where you have to make sure all of your data source and delegate methods are adjusted to your new format. You have to include first a retrieval of the group’s array, and then retrieve a specific country from there in each method. First, you’ll change your -tableView:cellForRowAtIndexPath:. It should now look like so:

- (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 *country = [group objectAtIndex:indexPath.row];
    cell.textLabel.text = country.name;
    cell.detailTextLabel.text = country.capital;
    UIImage *flag = country.flag;
    cell.imageView.image = [MainTableViewController scale:flag toSize:CGSizeMake( 115, 75)];
    
    return cell;
}

Up next is -tableView:numberOfRowsInSection:.

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

Here is -tableView:didSelectRowAtIndexPath:.

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    selectedIndexPath = indexPath;
    /////BEGIN MODIFIED CODE FOR GROUPED TABLE
    NSArray *group = [self.countries objectAtIndex:indexPath.section];
    Country *chosenCountry = [group objectAtIndex:indexPath.row];
    /////END OF MODIFIED CODE
    CountryInfoViewController *infoVC = [[CountryInfoViewController alloc] init];
    infoVC.delegate = self;
    infoVC.currentCountry = chosenCountry;
    
    [self.navigationController pushViewController:infoVC animated:YES];
}

Here is -tableView:accessoryButtonTappedForRowWithIndexPath:.

-(void)tableView:(UITableView *)tableView
accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    selectedIndexPath = indexPath;
    ////BEGIN MODIFIED CODE FOR GROUPED TABLE
    NSArray *group = [self.countries objectAtIndex:indexPath.section];
    Country *chosenCountry = [group objectAtIndex:indexPath.row];
    ////END MODIFIED CODE FOR GROUPED TABLE
    CountryInfoViewController *infoVC = [[CountryInfoViewController alloc] init];
    infoVC.delegate = self;
    infoVC.currentCountry = chosenCountry;
    
    NSLog(@"Accessory Button Tapped");
    [self.navigationController pushViewController:infoVC animated:YES];
}

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

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

The last method you must fix is -tableView:commitEditingStyle:forRowAtIndexPath:, which will look like so:

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        //////Changed code
        NSMutableArray *group = [self.countries objectAtIndex:indexPath.section];
        Country *deletedCountry = [group objectAtIndex:indexPath.row];
        [group removeObject:deletedCountry];
        //////End of changed code
        
        [tableViewCountries deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    }
    else if (editingStyle == UITableViewCellEditingStyleInsert)
    {
        //////More changed code!
        NSMutableArray *group = [self.countries objectAtIndex:indexPath.section];
        Country *copiedCountry = [group objectAtIndex:indexPath.row];
        Country *newCountry = [[Country alloc] init];
        newCountry.name = copiedCountry.name;
        newCountry.flag = copiedCountry.flag;
        newCountry.capital = copiedCountry.capital;
        newCountry.motto = copiedCountry.motto;
        
        [group insertObject:newCountry atIndex:indexPath.row+
1];
        //////End of changed code
        
        [self.tableViewCountries insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:indexPath.row+
1 inSection:indexPath.section]] withRowAnimation:UITableViewRowAnimationRight];
    }
}

Finally, since 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 exactly how many sections your UITableView will have with the following method:

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

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

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

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

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

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

After all these changes, running your app should result in a view similar to that in Figure 9–17.

Image

Figure 9–17. Your application with grouped items and section headers

As one final addition that you can make for your table, you can also add “footers” to your sections. These work just like headers, but, as you might guess, appear on the bottom of your groups. Here’s a quick method to add some (slightly silly) footers to your UITableView.

-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
    if (section == 0)
        return @"I'm a footer!";
    return @"Me too, I guess...";
}

In keeping with all the other vastly customizable parts of a UITableView, these headers and footers are also incredibly easy to customize 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 9–18 shows the final result of your setup.

Image

Figure 9–18. Your completed grouped UITableView with both headers and footers

Summary

Throughout this entire chapter, you have seen how to programmatically create a UITableView, step-by-step, for two kinds of styles: “plain” and “grouped.” You have also been given a glimpse at the amount of customization control the developer has over the view and display of a UITableView, though the full Apple documentation has a great deal more to say on the subject. You have even included a great deal of functionality into your UITableViews to provide them with the most powerful user interface. However, the key to UITableViews is not how they work, but the data that they present. It is up to you as a developer to find the information that users want or need, and present it to them in the most efficient, flexible way possible. A UITableView is a fantastic tool, but the purpose it serves is by far more important, and this is what will ultimately be the final product that you deliver to your customers.

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

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