Chapter 3. Displaying Your Data: The UITableView

WHAT'S IN THIS CHAPTER?

  • Customizing the TableView by creating your own TableView cells

  • Searching and filtering your result sets

  • Adding important UI elements to your tables such as indexes and section headers

  • Avoiding and troubleshooting performance issues with your TableViews

The focus of the book thus far has been on how to get your data on to the iPhone and how to access that data on the device. This chapter focuses on how you can enhance the display of your data by customizing the TableViewCell. It also examines how to make your data easier to use by adding an index, section headings, and search functionality to the UITableView. In the next several sections, you take a closer look at how to use the TableView to display your data in ways that make it more useful to your target audience.

CUSTOMIZING THE TABLEVIEW

You begin by taking a look at the default TableView styles that are available in the iPhone SDK. Then, you learn the technique of adding subviews to the content view of a TableViewCell. In the event that neither of these solutions meets your needs for customizing the display of your data, you will examine how to design your own TableViewCell from scratch using Interface Builder. If you had trouble with IB in the previous chapter, now is a good time to review Apple's documentation on using IB, which you can find at http://developer.apple.com/.

TableViewCell Styles

Several pre-canned styles are available to use for a TableViewCell:

  • UITableViewCellStyleDefault: This style displays a cell with a black, left-aligned text label with an optional image view.

  • UITableViewCellStyleValue1: This style displays a cell with a black, left-aligned text label on the left side of the cell and an additional blue text, right-aligned label on the right side.

  • UITableViewCellStyleValue2: This style displays a cell with a blue text, right-aligned label on the left side of the cell and an additional black, left-aligned text label on the right side of the cell.

  • UITableViewCellStyleSubtitle: This style displays a cell with a black, left-aligned text label across the top with a smaller, gray text label below.

In each style, the larger of the text labels is defined by the textLabel property and the smaller is defined by the detailTextLabel property.

Let's change the Catalog application from the last chapter to see what each of these styles looks like. You will change the application to use the name of the part manufacturer as the subtitle.

In the RootViewController.m implementation file, add a line to the tableView:cellForRowAtIndexPath: method that sets the cell's detailTextLabel text property to the product's manufacturer:

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

    // Configure the cell.
    // Get the Product object
    Product* product = [self.products objectAtIndex:[indexPath row]];

    cell.textLabel.text = product.name;
    cell.detailTextLabel.text = product.manufacturer;
    return cell;
}
                                                         
TableViewCell Styles

When you run the application, you will see something like Figure 3-1 (a). Nothing has changed. You may be wondering what happened to the subtitle. When you initialized the cell, the style was set to UITableViewCellStyleDefault. In this style, only the cell's textLabel is displayed, along with an optional image, which you will add in a moment.

TableView Cell Styles

Figure 3.1. TableView Cell Styles

On the line where you initialize the cell, change the code to use UITableViewCellStyleValue1:

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
                               reuseIdentifier:CellIdentifier] autorelease];

The table now displays the part name and the manufacturer as shown in Figure 3-1 (b).

Changing the default to UITableViewCellStyleValue2 results in a table that looks like Figure 3-1 (c).

Changing the default to UITableViewCellStyleSubtitle results in a table that looks like Figure 3-1 (d).

Now, you add some images to the catalog items. You can obtain the images used in the example from the book's web site. Add the images to your application by right-clicking on the Resources folder in the left-hand pane of Xcode and select Add Existing Files. Next, you should add code to the tableView:cellForRowAtIndexPath: method that will look for the image in the application bundle using the image name from the database.

NSString *filePath = [[NSBundle mainBundle] pathForResource:product.image
                                                     ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
cell.imageView.image = image;
                                                         
TableView Cell Styles

Finally, you can add an accessory to each cell. The accessory is the little arrow on the right side of a cell that tells the user that selecting the cell will take him or her to another screen. To add the accessory, you need to add a line of code to the tableView:cellForRowAtIndexPath: method to configure the cell's accessoryType:

cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

There are a few different accessory types that you can use:

  • UITableViewCellAccessoryDisclosureIndicator is the gray arrow that you have added. This control doesn't respond to touches and is used to indicate that selecting this cell will bring the user to a detail screen or the next screen in a navigation hierarchy.

  • UITableViewCellAccessoryDetailDisclosureButton presents a blue button with an arrow in it. This control can respond to touches and is used to indicate that selecting it will lead to configurable properties.

  • UITableViewCellAccessoryCheckmark displays a checkmark on the right side of the cell. This control doesn't respond to touches.

Figure 3-2 shows how a catalog looks after adding the images and a disclosure indicator accessory.

Catalog application with images

Figure 3.2. Catalog application with images

The final code for the cellForRowAtIndexPath: method should look like this:

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

    // Configure the cell.
    // Get the Product object
    Product* product = [self.products objectAtIndex:[indexPath row]];

    cell.textLabel.text = product.name;
    cell.detailTextLabel.text = product.manufacturer;
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

    NSString *filePath = [[NSBundle mainBundle] pathForResource:product.image
                                                         ofType:@"png"];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    cell.imageView.image = image;

    return cell;
}
                                                         
Catalog application with images

There are also other properties that you can use to provide additional customization for the TableViewCell. You can use the backgroundView property to assign another view as the background of a cell, set the selectionStyle to control how a cell looks when selected, and set the indentation of text with the indentationLevel property.

You can further customize a cell by modifying properties that are exposed in the UIView class because UITableViewCell is a subclass of UIView. For instance, you can set the background color of a cell by using the backgroundColor property of the UIView.

Adding Subviews to the contentView

If none of the existing TableViewCell styles work for your application, it is possible to customize the TableViewCell by adding subviews to the contentView of the TableViewCell. This approach is effective when the OS can perform the cell layout using autoresizing and the default behavior of the cell is appropriate for your application. If you need full control of how the cell is drawn or wish to change the behavior of the cell from the default, you will need to create a custom subclass of UITableViewCell. You will learn how to create this subclass in the next section.

Because UITableViewCell inherits from UIView, a cell has a content view, accessible through the contentView property. You can add your own subviews to this contentView and lay them out using the superview's coordinate system either programmatically or with IB. When implementing customization this way, you should make sure to avoid making your subviews transparent. Transparency causes compositing to occur, which is quite time-consuming. Compositing takes time and will result in degraded TableView scrolling speed. You look at this in more detail at the end of this chapter in the section on performance.

Suppose that your customer is not happy with the table in the current application. He wants to see all of the existing information plus an indication of where the product was made, and the price. You could use a flag icon to represent the country of origin on the right-hand side and add a label to display the price, as shown in the mockup in Figure 3-3. It's not a beautiful design, but it's what the customer wants.

Catalog mockup with flags

Figure 3.3. Catalog mockup with flags

It is impossible to achieve this layout using any of the default cell styles. To build this customized cell, you will hand-code the layout of the cell.

Because you will be modifying the cell you display in the table, you will be working in the RootViewController.m file and modifying the tableView: cellForRowAtIndexPath: method.

First, you need variables for the two images and three labels that you plan to display. At the beginning of the method, add the declarations for these items:

UILabel *nameLabel, *manufacturerLabel, *priceLabel;
UIImageView *productImage, *flagImage;

Next, declare an NSString for the CellIdentifier and try to dequeue a cell using that identifier:

static NSString *CellIdentifier = @"Cell";

UITableViewCell *cell = [tableView
                         dequeueReusableCellWithIdentifier:CellIdentifier];

You have seen this code before in previous examples, but until now I haven't explained how it works. Creating TableViewCells is a relatively time-consuming process. In addition, memory is scarce on an embedded device such as the iPhone or iPad. It is inefficient to create all of the cells and have them hanging around using memory while off-screen and not viewable. Conversely, because it takes time to create cells, it would be a performance hit to create them dynamically each time they were needed.

In order to solve these problems, the engineers at Apple came up with a very good solution. They gave the TableView a queue of TableViewCells from which you can get a reference to an existing cell object. When you need a cell, you can try and pull one from the queue. If you get one, you can reuse it; if you don't, you have to create a new cell to use that will eventually be added to the queue. The framework handles the control logic by determining which cells are queued and available, and which are currently being used.

All you need to do as a developer is try to dequeue a cell and check the return. If the return is nil, you have to create the cell. If it is not, you have a valid cell that you can use. The type of cell that is dequeued is based on the cell identifier that you pass in when trying to dequeue. Remember that you set this identifier when you initialized a new cell with the reuseIdentifier.

The preceding code attempts to dequeue a cell using the reuseIdentifier, Cell. The following if (cell==nil) block either creates a new cell with the Cell reuseIdentifier or it goes on to work with the cell that was dequeued.

If the cell needs to be created, the following code is executed:

if (cell == nil) {
    // Create a new cell object since the dequeue failed
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                   reuseIdentifier:CellIdentifier] autorelease];

    // Set the accessoryType to the grey disclosure arrow
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

    // Configure the name label
    nameLabel = [[[UILabel alloc]
                  initWithFrame:CGRectMake(45.0, 0.0, 120.0, 25.0)]
                 autorelease];
    nameLabel.tag = NAMELABEL_TAG;

    // Add the label to the cell's content view
    [cell.contentView addSubview:nameLabel];

    // Configure the manufacturer label
    manufacturerLabel = [[[UILabel alloc]
                          initWithFrame:CGRectMake(45.0, 25.0, 120.0, 15.0)]
                         autorelease];
    manufacturerLabel.tag = MANUFACTURERLABEL_TAG;
    manufacturerLabel.font = [UIFont systemFontOfSize:12.0];
    manufacturerLabel.textColor = [UIColor darkGrayColor];

    // Add the label to the cell's content view
    [cell.contentView addSubview:manufacturerLabel];

    // Configure the price label
    priceLabel = [[[UILabel alloc]
                   initWithFrame:CGRectMake(200.0, 10.0, 60.0, 25.0)]
                  autorelease];
    priceLabel.tag = PRICELABEL_TAG;

    // Add the label to the cell's content view
    [cell.contentView addSubview:priceLabel];

    // Configure the product Image
    productImage = [[[UIImageView alloc]
                     initWithFrame:CGRectMake(0.0, 0.0, 40.0, 40.0)]
                    autorelease];
    productImage.tag = PRODUCTIMAGE_TAG;

    // Add the Image to the cell's content view
[cell.contentView addSubview:productImage];

    // Configure the flag Image
    flagImage = [[[UIImageView alloc]
                  initWithFrame:CGRectMake(260.0, 10.0, 20.0, 20.0)]
                 autorelease];
    flagImage.tag = FLAGIMAGE_TAG;

    // Add the Image to the cell's content view
    [cell.contentView addSubview:flagImage];
}
                                                         
Catalog mockup with flags

The first line allocates a new cell and initializes it with the Cell reuseIdentifier. You have to do this because cell==nil indicates that no existing cells are available for reuse.

Each block thereafter is similar. You first create the object to be added to the cell, either a UILabel or UIImage. Then, you configure it with the attributes that you want such as fonts and text colors. You assign a tag to the object that you can use to get the instance of the label or image if you are reusing an existing cell. Finally, you add the control to the contentView of the cell.

The tag values for each control must be integers and are commonly defined using #define statements. Put the following #define statements before the tableView:cellForRowAtIndexPath: method definition:

#define NAMELABEL_TAG 1
#define MANUFACTURERLABEL_TAG 2
#define PRICELABEL_TAG 3
#define PRODUCTIMAGE_TAG 4
#define FLAGIMAGE_TAG 5
                                                         
Catalog mockup with flags

The position of each UI element is set in the initWithFrame method call. The method takes a CGRect struct that you create using the CGRectMake function. This function returns a CGRect struct with the x, y, width, and height values set.

Next, code the else clause that gets called if you successfully dequeue a reusable cell:

else {
    nameLabel = (UILabel *)[cell.contentView
                            viewWithTag:NAMELABEL_TAG];
    manufacturerLabel = (UILabel *)[cell.contentView
                                    viewWithTag:MANUFACTURERLABEL_TAG];
    priceLabel = (UILabel *)[cell.contentView
                             viewWithTag:PRICELABEL_TAG];
    productImage = (UIImageView *)[cell.contentView
                                   viewWithTag:PRODUCTIMAGE_TAG];
    flagImage = (UIImageView *)[cell.contentView
                                viewWithTag:FLAGIMAGE_TAG];
}
                                                         
Catalog mockup with flags

You can now see how you use tags. The viewWithTag function of the contentView returns a pointer to the UI object that was defined with the specified tag. So, when you create a new cell, you define the UI objects with those tags. When you dequeue a reusable cell, the tags are used to get pointers back to those UI objects. You need these pointers to be able to set the text and images used in the UI objects in the final section of the method:

// Configure the cell.
// Get the Product object
Product* product = [self.products objectAtIndex:[indexPath row]];

nameLabel.text = product.name;
manufacturerLabel.text = product.manufacturer;
priceLabel.text = [[NSNumber numberWithFloat: product.price] stringValue];

NSString *filePath = [[NSBundle mainBundle] pathForResource:product.image
                                                     ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
productImage.image = image;

filePath = [[NSBundle mainBundle] pathForResource:product.countryOfOrigin
                                           ofType:@"png"];
image = [UIImage imageWithContentsOfFile:filePath];
flagImage.image = image;

return cell;
                                                         
Catalog mockup with flags

In this final section, you get an instance of the Product object for the row that you have been asked to display. Then, you use the product object to set the text in the labels and the images in the UIImage objects. To finish the method off, you return the cell object.

If you add the flag images to the project's resources folder, you should be able to build and run your application. The catalog should look like the mockup shown in Figure 3-3. You can see the running application in Figure 3-4.

Now that you know how to add subviews to the contentView for a TableViewCell, you have opened up a whole new world of customization of the TableViewCell. You can add any class that inherits from UIView to a cell. Now would be a good time to take some time to explore all of the widgets that are available to you and to think about how you could use them in TableViews to develop great new interfaces.

Subclassing UITableViewCell

If you need full control of how the cell is drawn or wish to change the behavior of the cell from the default, you will want to create a custom subclass of UITableViewCell. It is also possible to eke out some additional performance, particularly when dealing with problems with table scrolling, by subclassing the UITableViewCell.

Running catalog application

Figure 3.4. Running catalog application

There are a couple of ways to implement the subclass. One is to implement it just as you did in the previous section by adding subviews to the contentView. This is a good solution when there are only three or four subviews. If you need to use more than four subviews, scrolling performance could be poor.

If you need to use more than four subviews to implement your cell, or if the scrolling performance of your TableView is poor, it is best to manually draw the contents of the cell. This is done in a subview of the contentView by creating a custom view class and implementing its drawRect: method.

There are a couple of issues with the approach of implementing drawRect. First, performance will suffer if you need to reorder controls due to the cell going into editing mode. Custom drawing during animation, which happens when transitioning into editing mode or reordering cells, is not recommended. Second, if you need the controls that are embedded in the cell to respond to user actions, you cannot use this method. You could not use this method if, for example, you had some buttons embedded in the cell and needed to take different action based on which button was pressed.

In the example, you will be drawing text and images in the view, so implementing drawRect is a viable option. The cell will look like it contains image and label controls, but will in fact contain only a single view with all of the UI controls drawn in. Therefore, the individual controls are not able to respond to touches.

Because the TableViewCell will have more than four controls, is not editable and doesn't have sub controls that need to respond to touches, you will implement the cell using drawRect. You will find that most, if not all, of the tables that you create to display data will fall into this category, making this a valuable technique to learn.

Getting Started

To work along with this example, download a copy of the original catalog project from the book's web site. You will use that as the starting point to build the custom subclass version of the application.

In the project, you will first add a new Objective-C class. In the Add class dialog, select subclass of UITableViewCell from the drop-down in the center of the dialog box. Call the new class CatalogTableViewCell. This will be your custom subclass of the TableViewCell.

In the header for CatalogTableViewCell, add an #import statement to import the Product.h header:

#import "Product.h"

The subclass will implement a method that sets the Product to be used to display the cell.

Add a setProduct method that users will call to set the Product to be used for the cell:

- (void)setProduct:(Product *)theProduct;

Create a new Objective-C class that is a subclass of UIView, called CatalogProductView. This will be the custom view that is used by your cell subclass to draw the text and images that you will display. This view will be the only subview added to the cell.

In the CatalogProductView header, add an import statement and instance variable for the Product object. Also, add a function setProduct. The cell uses this function to pass the product along to the view. The view will then use the product to get the data used to draw in the view. The CatalogProductView header should look like this:

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

@interface CatalogProductView : UIView {
    Product* theProduct;
}

- (void)setProduct:(Product *)inputProduct;

@end
                                                         
Getting Started

Switch back to the CatalogTableViewCell header and add a reference, instance variable, and property for your custom view. The CatalogTableViewCell header should look like this:

#import <UIKit/UIKit.h>
#import "Product.h"
#import "CatalogProductView.h"

@interface CatalogTableViewCell : UITableViewCell {
    CatalogProductView* catalogProductView;
}

@property (nonatomic,retain) CatalogProductView* catalogProductView;

- (void)setProduct:(Product *)theProduct;

@end
                                                         
Getting Started

In the CatalogTableViewCell implementation file, below the @implementation line, add a line to synthesize the catalogProductView property:

@synthesize catalogProductView;

Continuing in the CatalogTableViewCell implementation, you'll add code to initWithStyle:reuseIdentifier: to initialize the custom view to the size of the container and add the subview to the cell's content view:

- (id)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {

        // Create a frame that matches the size of the custom cell
CGRect viewFrame = CGRectMake(0.0, 0.0,
                                      self.contentView.bounds.size.width,
                                      self.contentView.bounds.size.height);

        // Allocate and initialize the custom view with the dimenstions
        // of the custom cell
        catalogProductView = [[CatalogProductView alloc]
                              initWithFrame:viewFrame];

        // Add our custom view to the cell
        [self.contentView addSubview:catalogProductView];

    }
    return self;
}
                                                         
Getting Started

You will now implement the setProduct: method. All it will do is call the view's setProduct method:

- (void)setProduct:(Product *)theProduct
{
    [catalogProductView setProduct:theProduct];
}
                                                         
Getting Started

Now, let's get back to implementing the CatalogProductView. First, you need to implement the initWithFrame: method to initialize the view:

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // Initialization code
        self.opaque = YES;
        self.backgroundColor = [UIColor whiteColor];

    }
    return self;
}
                                                         
Getting Started

Here, you set the view to be opaque because there is a severe performance hit for using transparent views. If at all possible, always use opaque views when working with table cells. The next line sets the background color of the view to white.

Next, you implement the method setProduct that is called from the custom cell:

- (void)setProduct:(Product *)inputProduct
{
    // If a different product is passed in...
    if (theProduct != inputProduct)
    {
        // Clean up the old product
        [theProduct release];
        theProduct = inputProduct;

        // Hang on to the new product
        [theProduct retain];
    }

    // Mark the view to be redrawn
    [self setNeedsDisplay];
}
                                                         
Getting Started

This method does a couple of things. First, it sets the product to be displayed, and then it marks the view to be redrawn. You should never directly call the drawRect method to redraw a view. The proper way to trigger a redraw is to tell the framework that a view needs to be redrawn. The framework will then call drawRect for you when it is time to redraw.

Implementing drawRect:

Now you get to the real meat of this example, drawing the view. This is done in the drawRect function and is relatively straightforward:

- (void)drawRect:(CGRect)rect {
    // Drawing code

    // Draw the product text
    [theProduct.name drawAtPoint:CGPointMake(45.0,0.0)
                        forWidth:120
                        withFont:[UIFont systemFontOfSize:18.0]
                     minFontSize:12.0
                  actualFontSize:NULL
                   lineBreakMode:UILineBreakModeTailTruncation
              baselineAdjustment:UIBaselineAdjustmentAlignBaselines];

    // Set to draw in dark gray
    [[UIColor darkGrayColor] set];

    // Draw the manufacturer label
    [theProduct.manufacturer drawAtPoint:CGPointMake(45.0,25.0)
                                forWidth:120
                                withFont:[UIFont systemFontOfSize:12.0]
                             minFontSize:12.0
                          actualFontSize:NULL
lineBreakMode:UILineBreakModeTailTruncation
                      baselineAdjustment:UIBaselineAdjustmentAlignBaselines];

    // Set to draw in black
    [[UIColor blackColor] set];

    // Draw the price label
    [[[NSNumber numberWithFloat: theProduct.price] stringValue]
     drawAtPoint:CGPointMake(200.0,10.0)
     forWidth:60
     withFont:[UIFont systemFontOfSize:16.0]
     minFontSize:10.0
     actualFontSize:NULL
     lineBreakMode:UILineBreakModeTailTruncation
     baselineAdjustment:UIBaselineAdjustmentAlignBaselines];

    // Draw the images
    NSString *filePath = [[NSBundle mainBundle]
                          pathForResource:theProduct.image ofType:@"png"];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    [image drawInRect:CGRectMake(0.0, 0.0, 40.0, 40.0)];

    filePath = [[NSBundle mainBundle]
                pathForResource:theProduct.countryOfOrigin ofType:@"png"];
    image = [UIImage imageWithContentsOfFile:filePath];
    [image drawInRect:CGRectMake(260.0, 10.0, 20.0, 20.0)];
}
                                                         
Implementing drawRect:

Basically, you render each string using the drawAtPoint:forWidth:withFont:minFontSize:actualFontSize:lineBreakMode:baselineAdjustment: method. Boy, that's a mouthful! This function accepts a series of parameters and renders the string to the current drawing context using those parameters.

So, for the product name, you draw it at the point (45,0) with a width of 120 pixels using the system font with a size of 18. You force a minimum font size of 12 because the renderer will shrink the text to fit within the width specified. You won't specify an actual font size because you specified that in the withFont parameter. The lineBreakMode sets how the lines will be broken for multiline text. Here, you just truncate the tail, meaning that the renderer will just show "..." if the text size is reduced to 12 and still cannot fit in the 120 pixels that you've allotted. Finally, the baselineAdjustment specifies how to vertically align the text.

Now that you've drawn the product name, you set the drawing color to dark gray to draw the manufacturer name. The next drawAtPoint: call does just that.

Next, you set the color back to black and draw the price string. Notice that you need to get a string representation of the floating-point price field. You do that by using the stringValue method of the NSNumber class.

Finally, you obtain the product image and the flag image just as you did in the previous example. Then you render the images using the drawInRect: method of the UIImage class.

Finishing Up

Now that you've got the new cell subclass and custom view implemented, it's time to put them to use. In the RootViewController header, add a #include for the custom cell:

#import "CatalogTableViewCell.h"

In the RootViewController implementation, change the tableView:cellForRowAtIndexPath: method to use the new cell control:

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    CatalogTableViewCell *cell = (CatalogTableViewCell *)
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[CatalogTableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }

    // Configure the cell.
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

    // Get the Product object
    Product* product = [self.products objectAtIndex:[indexPath row]];

    // Set the product to be used to draw the cell
    [cell setProduct:product];

    return cell;
}
                                                         
Finishing Up

In this method, you replace the UITableViewCell with the new CatalogTableViewCell. When you try to dequeue the reusable cell, you must cast the return to a CatalogTableViewCell* because dequeueReusableCellWithIdentifier: returns a UITableViewCell*. If you are unsuccessful with the dequeue, you create a new CatalogTableViewCell just as you did with the UITableViewCell. Then, just as in the previous example, you set the accessory type and get the Product object that you want to display. Finally, you set the product in the custom cell object to be displayed and return the cell object.

Now all you have to do is add the flag images to the resources folder of your project, build, and run. You should get something that looks just like the previous example and Figure 3-4.

IMPLEMENTING SECTIONS AND AN INDEX

Now that you have the ability to create fantastic table cells, you need a better way to organize them. In this section, you learn to partition your data into sections, display them with section headers, and provide the user the ability to navigate them using an index.

If you have ever used the Contacts application on the iPhone, you should be familiar with section headers and the index. In Contacts, each letter of the alphabet is represented as a section header, the gray bar with the letter. Every contact whose name starts with that letter is grouped under the section header. The index on the right hand side of the screen can be used to quickly navigate to a section by tapping on the letter in the index that corresponds to the section.

You will be adding section headers and an index to the catalog application. When you are finished, your catalog application should look like Figure 3-5.

Catalog application with sections and index

Figure 3.5. Catalog application with sections and index

The data that you use to populate your indexed table needs to be organized such that you can easily build the sections. That is, the data should be an array of arrays where each inner array represents a section of the table. The scheme that you will use in the catalog application is shown in Figure 3-6.

Data scheme for sectioned tables

Figure 3.6. Data scheme for sectioned tables

These section arrays are then ordered in the outer array based on criteria that you provide. Typically, this ordering is alphabetical but you can customize it in any way that you wish.

You could take care of sorting and organizing the table data yourself, but there is a helper class in the iPhone SDK framework that has been specifically designed to help you with this task: UILocalizedIndexedCollation.

The UILocalizedIndexedCollation class is a helper class that assists with organizing, sorting, and localizing your table view data. The table view datasource can then use the collation object to obtain the section and index titles.

You will implement the indexed table using the UILocalizedIndexedCollation class. If you use this class, the data model object that you want to display in the table needs to have a method or property that the UILocalizedIndexedCollation can call when creating its arrays. It is also helpful for your data model class to have a property that maintains the index of the object in the section array. Because the product model class already has a name property, you can use that to define the sections. You will need to add a property to hold the section number. Add the section instance variable and property to the Product.h header like this:

@interface Product : NSObject {
    int ID;
    NSString* name;
    NSString* manufacturer;
    NSString* details;
    float price;
    int quantity;
    NSString* countryOfOrigin;
    NSString* image;
    NSInteger section;
}
@property (nonatomic) int ID;
@property (retain, nonatomic) NSString *name;
@property (retain, nonatomic) NSString *manufacturer;
@property (retain, nonatomic) NSString *details;
@property (nonatomic) float price;
@property (nonatomic) int quantity;
@property (retain, nonatomic) NSString *countryOfOrigin;
@property (retain, nonatomic) NSString *image;
@property NSInteger section;
                                                         
Data scheme for sectioned tables

Add the synthesize statement to the implementation:

@implementation Product
@synthesize ID;
@synthesize name;
@synthesize manufacturer;
@synthesize details;
@synthesize price;
@synthesize quantity;
@synthesize countryOfOrigin;
@synthesize image;
@synthesize section;
                                                         
Data scheme for sectioned tables

The next thing that you need to do is load all of the data from your datasource into your model objects. In the case of the catalog application, you are already doing that in the getAllProducts method of the DBAccess class. If you recall, that method queries the SQLite database, creates a Product object for each row that is returned, and adds each Product object to an array.

You will use this array along with the UILocalizedIndexedCollation object to create the sections. To create the necessary data arrays, you will have to make some changes to the viewDidLoad method of the RootViewController.m implementation.

Here is the new implementation of viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.products = [NSMutableArray arrayWithCapacity:1];

    NSMutableArray *productsTemp;

    //  Get the DBAccess object;
    DBAccess *dbAccess = [[DBAccess alloc] init];

    //  Get the products array from the database
    productsTemp = [dbAccess getAllProducts];

    //  Close the database because you are finished with it
    [dbAccess closeDatabase];

    //  Release the dbAccess object to free its memory
    [dbAccess release];

    UILocalizedIndexedCollation *indexedCollation =
        [UILocalizedIndexedCollation currentCollation];

    //  Iterate over the products, populating their section number
    for (Product *theProduct in productsTemp) {
        NSInteger section = [indexedCollation sectionForObject:theProduct
                                       collationStringSelector:@selector(name)];
        theProduct.section = section;
    }

    //  Get the count of the number of sections
    NSInteger sectionCount = [[indexedCollation sectionTitles] count];

    //  Create an array to hold the sub arrays
    NSMutableArray *sectionsArray = [NSMutableArray
                                     arrayWithCapacity:sectionCount];

    // Iterate over each section, creating each sub array
    for (int i=0; i<=sectionCount; i++) {
        NSMutableArray *singleSectionArray = [NSMutableArray
                                              arrayWithCapacity:1];
        [sectionsArray addObject:singleSectionArray];
    }

    // Iterate over the products putting each product into the correct sub-array
    for (Product *theProduct in productsTemp) {
        [(NSMutableArray *)[sectionsArray objectAtIndex:theProduct.section]
            addObject:theProduct];
    }

    // Iterate over each section array to sort the items in the section
    for (NSMutableArray *singleSectionArray in sectionsArray) {
        // Use the UILocalizedIndexedCollation sortedArrayFromArray: method to
        // sort each array
        NSArray *sortedSection = [indexedCollation
                                  sortedArrayFromArray:singleSectionArray
collationStringSelector:@selector(name)];
        [self.products addObject:sortedSection];
    }


}
                                                         
Data scheme for sectioned tables

The first part of the method is largely the same as the previous example, except that now you have added code to initialize the new products property. You then proceed to get the array of Products from the database access class, just as before.

After you release the DBAccess object, you move on to getting a reference to the UILocalizedIndexedCollation object:

UILocalizedIndexedCollation *indexedCollation = [UILocalizedIndexedCollation
                                                 currentCollation];

Next, you iterate over all of the products to populate the section index property:

for (Product *theProduct in productsTemp) {
        NSInteger section = [indexedCollation sectionForObject:theProduct
                                       collationStringSelector:@selector(name)];
        theProduct.section = section;
    }
                                                         
Data scheme for sectioned tables

You determine the section index using the UILocalizedIndexedCollation's sectionForObject:collationStringSelector: method. This method uses the property or method that is passed in as the collationStringSelector parameter to determine in which section the sectionForObject parameter belongs. So, in this case, the method uses the name property to determine the correct section for theProduct. You could use any method or property to organize your sections, as long as it returns a string.

The next section of code gets a count of all of the sections that you will need, creates the main array to hold all of the section sub-arrays, and creates each sub-array:

//  Get the count of the number of sections
NSInteger sectionCount = [[indexedCollation sectionTitles] count];

//  Create an array to hold the sub arrays
NSMutableArray *sectionsArray = [NSMutableArray
                                 arrayWithCapacity:sectionCount];

// Iterate over each section, creating each sub array
for (int i=0; i<=sectionCount; i++) {
    NSMutableArray *singleSectionArray = [NSMutableArray arrayWithCapacity:1];
    [sectionsArray addObject:singleSectionArray];
}
                                                         
Data scheme for sectioned tables

Next, you loop through each product again, placing it into the correct sub-array. Remember that the index to the correct sub-array was determined before and stored in the new section property of the Product object:

// Iterate over the products putting each product into the correct sub-array
for (Product *theProduct in productsTemp) {
    [(NSMutableArray *)[sectionsArray objectAtIndex:theProduct.section]
        addObject:theProduct];
}
                                                         
Data scheme for sectioned tables

Finally, the last section of the code goes back over each sub-array, sorts the data within the array using the UILocalizedIndexedCollation's sortedArrayFromArray:collationStringSelector: method, and then adds the array to the products array:

// Iterate over each section array to sort the items in the section
for (NSMutableArray *singleSectionArray in sectionsArray) {
    // Use the UILocalizedIndexedCollation sortedArrayFromArray:
    // method to sort each array
    NSArray *sortedSection = [indexedCollation
                              sortedArrayFromArray:singleSectionArray
                              collationStringSelector:@selector(name)];
    [self.products addObject:sortedSection];
}
                                                         
Data scheme for sectioned tables

Now you have the products array set up as you need it. It is now organized as an array of arrays, each of which contains a sorted list of Product objects, as shown in Figure 3-6.

The next thing that you need to do is configure the TableView to show the newly created sections. There are two TableView delegate methods that you need to implement: numberOfSectionsInTableView: and numberOfRowsInSection:.

The numberOfSectionsInTableView: method should return the number of sections that you will show in the TableView. You implement this by simply returning the count of objects in the products array:

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

The tableView: numberOfRowsInSection: method is used to return the number of rows in the requested section. To implement this, you just return the count of rows for the particular section that was requested:

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

You also need to modify the tableView:cellForRowAtIndexPath: method to get the product from the products array by section and row. If you recall, when you had only the single array, you just indexed into it directly to get the Product that you wanted to display. Now, you need to get the Product object that corresponds with the section and row that you are being asked to display:

Product* product = [[self.products objectAtIndex:[indexPath section]]
                    objectAtIndex:[indexPath row]];

You can see that you get the section from the indexPath and use that to index into the outer array. Then, you use the row as the index to the sub-array to get the Product.

Another modification that you will need to make in cellForRowAtIndexPath: is to change the cell's accessoryType to UITableViewCellAccessoryNone. You need to remove the accessory view because the index will obscure the accessory and it will look bad:

cell.accessoryType = UITableViewCellAccessoryNone;

Now that you will be using headers in the table, you need to implement the method tableView:titleForHeaderInSection:. This method returns a string that will be used as the header text for the section. You obtain the title from the UILocalizedIndexedCollation by using the sectionTitles property:

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section {
    // Make sure that the section will contain some data
    if ([[self.products objectAtIndex:section] count] > 0) {

        // If it does, get the section title from the
        // UILocalizedIndexedCollation object
        return [[[UILocalizedIndexedCollation currentCollation] sectionTitles]
                objectAtIndex:section];
    }
    return nil;
}
                                                         
Data scheme for sectioned tables

Likewise, because you are implementing an index, you need to provide the text to use in the index. Again, the UILocalizedIndexedCollation helps out. The property sectionIndexTitles returns an array of the index titles:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    // Set up the index titles from the UILocalizedIndexedCollation
    return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];
}
                                                         
Data scheme for sectioned tables

Once you've set up the index, you have to link the index to the section titles by implementing the tableView:sectionForSectionIndexTitle: atIndex: method. Again, using the UILocalizedIndexedCollation greatly simplifies this implementation:

- (NSInteger)tableView:(UITableView *)tableView
    sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
    // Link the sections to the labels in the index
    return [[UILocalizedIndexedCollation currentCollation]
            sectionForSectionIndexTitleAtIndex:index];
}
                                                         
Data scheme for sectioned tables

If you build and run the application, it should run and the table should be displayed with sections and an index. However, if you select a row, the application will not take you to the detail page. Because you modified how the data is stored, you need to go back and modify the tableView:didSelectRowAtIndexPath: method to use the new scheme. This is as simple as changing the line of code that obtains the product object to use in the detail view like this:

Product* product =  [[self.products objectAtIndex:[indexPath section]]
                     objectAtIndex:[indexPath row]];

Now, if you build and run again, all should be well. You should be able to navigate the application just as before, except that now you have a well-organized and indexed table for easy navigation.

IMPLEMENTING SEARCH

The sample application now has all of the items in the corporate catalog neatly organized into sections based on the product name, with an index for quick access to each section. The final piece of functionality that you will add is a search capability. Users should be able to search for particular products within the catalog, without having to scroll through the entire catalog.

You will implement functionality that is similar to the search capabilities of the built-in Contacts application. You will add a UISearchBar control at the top of the table and then filter the products list based on user input. The final interface will look like Figure 3-7.

When the user starts a search, you will remove the side index list and only show rows that meet the search criteria, as shown in Figure 3-8.

Implementing search requires two controls, the UISearchBar and the UISearchDisplayController, which was introduced in iPhone SDK 3.0. The UISearchBar is the UI widget that you will put at the top of the table to accept search text input. The UISearchDisplayController is used to filter the data provided by another View Controller based on the search text in the UISearchBar.

Catalog with search interface

Figure 3.7. Catalog with search interface

Search in progress

Figure 3.8. Search in progress

The UISearchDisplayController is initialized with a search bar and the View Controller containing the content to be searched. When a search begins, the search display controller overlays the search interface above the original View Controller's view to display a subset of the original data. The results display is a table view that is created by the search display controller.

The first step is to create the UISearchBar and add it to the table. In the RootViewController header, add an instance variable and associated property for the search bar:

@interface RootViewController : UITableViewController {
    NSMutableArray *products;
    UISearchBar* searchBar;
}

@property (retain, nonatomic) NSMutableArray *products;
@property (retain, nonatomic) UISearchBar* searchBar;
                                                         
Search in progress

In the RootViewController implementation file, synthesize the searchBar:

@synthesize searchBar;

You can now add the code to create the SearchBar and add it to the header of the TableView at the end of the viewDidLoad method:

// Create search bar
self.searchBar = [[[UISearchBar alloc] initWithFrame:
                       CGRectMake(0.0f, 0.0f, 320.0f, 44.0f)] autorelease];
self.tableView.tableHeaderView = self.searchBar;
                                                         
Search in progress

Next, you will create and configure the UISearchDisplayController. This controller will be used to filter and display the data in the RootViewController's TableView. In the header for RootViewController, add an instance variable and associated property for the SearchDisplayController:

@interface RootViewController : UITableViewController {
    NSMutableArray *products;

    UISearchBar* searchBar;
    UISearchDisplayController* searchController;
}

@property (retain, nonatomic) NSMutableArray *products;
@property (retain, nonatomic) UISearchBar* searchBar;
@property (retain, nonatomic) UISearchDisplayController* searchController;
                                                         
Search in progress

Synthesize the property in the RootViewController's implementation file:

@synthesize searchController;

Add the code to viewDidLoad to create and configure the display controller:

// Create and configure the search controller
self.searchController = [[[UISearchDisplayController alloc]
                          initWithSearchBar:self.searchBar
                          contentsController:self] autorelease];

self.searchController.searchResultsDataSource = self;
self.searchController.searchResultsDelegate = self;
                                                         
Search in progress

Because you are going to be creating a new, filtered table, you need to create an array to hold the filtered product list. Add an instance variable and property to the RootViewController header:

@interface RootViewController : UITableViewController {
    NSMutableArray *products;
    NSArray *filteredProducts;

    UISearchBar* searchBar;
    UISearchDisplayController* searchController;
}

@property (retain, nonatomic) NSMutableArray *products;
@property (retain, nonatomic) NSArray *filteredProducts;
@property (retain, nonatomic) UISearchBar* searchBar;
@property (retain, nonatomic) UISearchDisplayController* searchController;
                                                         
Search in progress

Synthesize the property in the RootViewController's implementation file:

@synthesize filteredProducts;

You have now completed adding the additional controls and properties that you need to implement the search feature. The next step is to modify the UITableView methods that are used to populate the UITableView.

In each function you need to determine if you are working with the normal table or the filtered table. Then, you need to proceed accordingly. You can determine which UITableView you are dealing with by comparing the UITableView passed into the function to the View Controller's tableView property, which holds the normal UITableView.

The first method that you will modify is numberOfSectionsInTableView:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // Is the request for numberOfRowsInSection for the regular table?
    if (tableView == self.tableView)
    {
        // Just return the count of the products like before
        return [self.products count];

    }

    return 1;
}
                                                         
Search in progress

The first thing that you do is compare the tableView that was passed into the method with the RootViewController's tableView. If they are the same, you are dealing with the normal TableView and you will determine the number of sections just as you did in the previous example. If you are dealing with the filtered table, you return 1 because you do not want to use sections in the filtered table.

Next, you'll modify the tableView:numberOfRowsInSection: method to check to see which table you are working with and then return the appropriate row count. You will use NSPredicate to filter the data. Predicates will be covered in detail in Part II of this book on Core Data. For now, the only thing that you need to understand about predicates is that they are a mechanism for providing criteria used to filter data. Predicates work like the WHERE clause in a SQL statement. Before you can use the predicate, you need to flatten the array of arrays that contains the data. You flatten the array, and then use the NSPredicate to filter the array:

- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {

    // Is the request for numberOfRowsInSection for the regular table?
    if (tableView == self.tableView)
    {
        // Just return the count of the products like before
        return [[self.products objectAtIndex:section] count];
}

    // You need the count for the filtered table
    //  First, you have to flatten the array of arrays self.products
    NSMutableArray *flattenedArray = [[NSMutableArray alloc]
                                      initWithCapacity:1];
    for (NSMutableArray *theArray in self.products)
    {
        for (int i=0; i<[theArray count];i++)
        {
            [flattenedArray addObject:[theArray objectAtIndex:i]];
        }
    }

    // Set up an NSPredicate to filter the rows
    NSPredicate *predicate = [NSPredicate predicateWithFormat:
                              @"name beginswith[c] %@", self.searchBar.text];
    self.filteredProducts = [flattenedArray
                             filteredArrayUsingPredicate:predicate];

    //  Clean up flattenedArray
    [flattenedArray release];

    return self.filteredProducts.count;
}
                                                         
Search in progress

This code uses the same methodology as the previous method for determining which TableView you are dealing with. If you are working with the normal TableView, you return the product count as in the previous example. If not, you need to determine the count of filtered products.

To accomplish this, you first flatten the products array of arrays into a single array. Remember that to implement sections, you should store your data as an array of arrays. Well, you need to flatten that structure down to a one-dimensional array in order to filter it with the NSPredicate. In order to flatten the products array, you simply loop over each array and put the contents of the sub-array into a new array called flattenedArray.

Next, you set up the NSPredicate object to filter out only rows that begin with the text that is input into the SearchBar. Then, you apply the predicate to the flattenedArray and put the result into the filteredProducts array. The filteredProducts array will be used from here on out when dealing with the filtered TableView. You then release the flattenedArray because you are finished with it and should free the memory. Finally, you return the count of items in the filteredProducts array.

Now that you have the correct row counts, you need to modify the tableView:cellForRowAtIndexPath: method to display the correct rows:

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    CatalogTableViewCell *cell = (CatalogTableViewCell *)
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[CatalogTableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }

    // Configure the cell.
    cell.accessoryType = UITableViewCellAccessoryNone;

    // Is the request for cellForRowAtIndexPath for the regular table?
    if (tableView == self.tableView)
    {

        // Get the Product object
        Product* product = [[self.products
                             objectAtIndex:[indexPath section]]
                             objectAtIndex:[indexPath row]];

        // Set the product to be used to draw the cell
        [cell setProduct:product];

        return cell;
    }

    // Get the Product object
    Product* product = [self.filteredProducts objectAtIndex:[indexPath row]];

    // Set the product to be used to draw the cell
    [cell setProduct:product];

    return cell;
}
                                                         
Search in progress

In this method, you do the same type of thing as you have done in the previous two methods. You create the cell just as you did in the previous example. The difference here is that if you are dealing with the normal table, you get the Product object from the self.products array, but if you are dealing with the filtered table, you get the Product object from the self.filteredProducts array.

Now you will modify the didSelectRowAtIndexPath: method to use either the normal or filtered table:

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    Product* product;

    if (tableView == self.tableView)
    {
        // Get the product that corresponds with the touched cell
        product =  [[self.products objectAtIndex:[indexPath section]]
                     objectAtIndex:[indexPath row]];
    }
else {
        product =  [self.filteredProducts objectAtIndex:[indexPath row]];

}


    //  Initialize the detail view controller from the NIB
    ProductDetailViewController *productDetailViewController =
        [[ProductDetailViewController alloc]
         initWithNibName:@"ProductDetailViewController" bundle:nil];

    //  Set the title of the detail page
    [productDetailViewController setTitle:product.name];

    //  Push the detail controller on to the stack
    [self.navigationController pushViewController:productDetailViewController
                                         animated:YES];

    //  Populate the details
    [productDetailViewController setLabelsForProduct:product];

    //  release the view controller becuase it is retained by the Navigation
    //  Controller
    [productDetailViewController release];
}
                                                         
Search in progress

Again, in this method, you do the same thing that you did in the previous method. If you are dealing with the normal table, you get the Product object from the self.products array, but if you are dealing with the filtered table, you get the Product object from the self.filteredProducts array.

Finally, modify the sectionIndexTitlesForTableView: method to return the regular index for the normal table but nil for the filtered table because you don't want to show the index while displaying the filtered table:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    if (tableView == self.tableView)
    {
        // Set up the index titles from the UILocalizedIndexedCollation
        return [[UILocalizedIndexedCollation currentCollation]
                sectionIndexTitles];
}

    return nil;
}
                                                         
Search in progress

You should now be able to build and run the code and have full searching capabilities. In a nutshell, you implement search by adding a SearchBar and a SearchDisplayController to your ViewController and then modify your TableView methods to handle dealing with either the normal or filtered data.

OPTIMIZING TABLEVIEW PERFORMANCE

In this chapter, you have explored how to use the TableView in detail. You have learned how to customize the TableView to look exactly as you want by using styles, adding subviews to the contentView, and creating your own cells by subclassing the UITableViewCell class. You have also learned how to organize your table and make it easy to use by adding sections, an index, and search capability. The final aspect of the TableView that you will look at is how to optimize performance.

The iPhone and iPad are amazing devices, and users expect an amazing experience from their apps. Apple has set the standard with the pre-installed applications. You should strive to get your applications to function as fluidly and elegantly as the default applications.

The primary problem that you will encounter when building an application with a TableView is poor scrolling performance. This can be caused by several factors, as you will see in the following sections.

Reusing Existing Cells

Creating objects at runtime can be an expensive operation in terms of how much processor time is used to create the object. Additionally, objects that contain a hierarchy of views, such as the TableViewCell, can consume a significant amount of memory.

On embedded devices like the iPhone and iPad, the processors are generally not as fast as those on desktop and laptop computers so you must try to do whatever you can to optimize your code for execution on these slower machines. Also, on devices such as the iPhone and iPad, memory is at a premium. If you use too much memory, the OS on the device will notify you to release some memory. If you fail to release enough memory, the OS will terminate your application. Random termination of your application does not lead to happy customers.

One step that you can take to optimize the code of your TableViews is to reuse existing cells when appropriate. The first step in debugging scrolling problems with the TableView is to look at object allocations. If objects are being allocated as you scroll, there is a problem.

The designers at Apple have provided us with a private queue that can be used to store TableViewCells for reuse. In the cellForRowAtIndexPath: method, you can access this queue to get an existing cell from the queue instead of creating a new one. In fact, you have done this in the code examples in this chapter.

If you want to see the difference that this optimization makes in your sample code, change the following line in cellForRowAtIndexPath:

UITableViewCell *cell = [tableView
                         dequeueReusableCellWithIdentifier:CellIdentifier];

to this:

UITableViewCell *cell = nil;

Now the code will not try to use a cell from the queue; it will create a new cell each time the TableView asks for a cell. The performance difference may not be too noticeable with a table with only a few rows, but imagine creating all of these cells if your data consisted of thousands of rows. Additionally, the final project is using subclassing and custom-drawn cells. Try going back and making this change to the code where you added subviews to the contentView. You should notice a significant slowdown in scrolling speed.

A common point of confusion with cell reuse is the cell identifier. The string that you provide in dequeueReusableCellWithIdentifier: is the identifier. This identifier does not define the cell's contents, only its style. When you plan on re-using the style of a cell, assign a string to the reuse identifier, and then pass that string into dequeueReusableCellWithIdentifier:. This allows you to create cells with different styles, store them with different identifiers, and queue them up for quick access.

When you call dequeueReusableCellWithIdentifier: the method returns either a cell that you can use, or nil. If the return is nil, you need to create a new cell to use in your table.

A final note about the cellForRowAtIndexPath: method. Do not do anything in cellForRowAtIndexPath that takes a long time. I recently saw an example of a developer downloading an image from the Web each time cellForRowAtIndexPath was called. This is a very bad idea! You need to make sure that cellForRowAtIndexPath returns very quickly as it is called each time the TableView needs to show a cell in your table. In the case where it will take a long time to create the content for a cell, consider pre-fetching the content and caching it.

Opaque Subviews

When you are using the technique of programmatically adding subviews to a cell's content view, you should ensure that all of the subviews that you add are opaque. Transparent subviews detract from scrolling performance because the compositing of transparent layers is an expensive operation. The UITableViewCell inherits the opaque property from UIView. This property defaults to YES. Do not change it unless you absolutely must have a transparent view.

Additionally, you should ensure that the subviews that you add to the UITableViewCell have the same background color as the cell. Not doing this detracts from the performance of the TableView.

It is possible to view layers that are being composited by using the Instruments application, which Apple provides for free as a part of Xcode. The tool is very useful in debugging applications and can be used to do a lot of helpful things including tracking memory leaks and logging all memory allocations. The Core Animation tool in Instruments can be used to show layers that are being composited. The Core Animation tool can only be used when running your application on a device.

In order to see where compositing is occurring in your application, start the Instruments tool. Instruments can be found in /Developer/Applications. When the tool, starts, it will ask you to choose a template for the trace document. In the left-hand pane, choose iPhone/All.

In this dialog, select Core Animation. This will open the Instruments interface with a Core Animation instrument. Click on the third icon from the left at the bottom of the screen to expand the detail view. In the debug options, select Color Blended Layers, as shown in Figure 3-9.

Instruments tool

Figure 3.9. Instruments tool

Now, when you run your application on the device, layers that are not composited are overlaid with green, while composited layers are red.

To see the difference, you can make a change to the RootViewController. In the cellForRowAtIndexPath: method, add a line to set productImage.alpha = 0.9 under the line productImage.tag = PRODUCTIMAGE_TAG. The snippet of code should look like this:

// Configure the product Image
productImage = [[[UIImageView alloc]
                 initWithFrame:CGRectMake(0.0, 0.0, 40.0, 40.0)] autorelease];
productImage.tag = PRODUCTIMAGE_TAG;
productImage.alpha = 0.9;
// Add the Image to the cell's content view
[cell.contentView addSubview:productImage];
                                                         
Instruments tool

Now run the application. You should see all of the product images overlaid with red. This means that these images are being composited, and that is not good. Remember that compositing takes a long time and should be avoided if possible.

There is an issue that you should be aware of with PNG images. If you are using PNG files, as in the sample, they should be created without an Alpha layer. Including the Alpha layer causes compositing to occur regardless of how the opaque property is set on the UIImageView.

You examine how to use Instruments in more detail in Appendix A.

Custom Drawn Cells with drawRect

You have examined this technique in the "Subclassing UITableViewCell" section of this chapter. The fastest way to render a cell is by manually drawing it in the drawRect method. It may take more work from the developer, but there is a large payoff. If a cell will contain more than three subviews, consider subclassing and drawing the contents manually. This can dramatically increase scrolling performance.

The technique basically boils down to collapsing multi-view TableCells down to one view that knows how to draw itself.

A subclass of UITableViewCell may reset attributes of the cell by overriding prepareForReuse. This method is called just before the TableView returns a cell to the datasource in dequeueReusableCellWithIdentifier:. If you do override this method to reset the cell's attributes, you should only reset attributes that are not related to content such as alpha, editing, and selection state.

UI Conventions for Accessory Views

You should abide by the following conventions when adding accessory views to cells in your table:

The UITableViewCellAccessoryDisclosureIndicator is a disclosure indicator. The control does not respond to touches and is used to indicate that touching the row will bring the user to a detail screen based on the selected row.

The UITableViewCellAccessoryDetailDisclosureButton does respond to touches and is used to indicate that configuration options for the selected row will be presented.

The UITableViewCellAccessoryCheckmark is used to display a checkmark indicating that the row is selected. The checkmark does not respond to touches.

For more information, you should read the iPhone Human Interface Guidelines located at: http://developer.apple.com

MOVING FORWARD

In this chapter, you have learned how to use the UITableView to display data in your application. Then, you learned how to customize the display of your data by building custom UITableViewCells. Next, you learned how to allow your user to manipulate the display of his data by searching and filtering the results. Finally, you learned how to avoid and troubleshoot performance problems with the UITableView.

In the next chapter, you will learn how to display and navigate your data using some of the unique UI elements that are available for use on the iPad. Then, in Part II of the book, you will move on to learn how to create and query data using the Core Data framework.

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

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