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.
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/
.
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;
}
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.
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;
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.
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; }
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
.
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.
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]; }
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
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]; }
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;
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 TableView
s to develop great new interfaces.
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
.
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.
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
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
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; }
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]; }
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; }
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]; }
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.
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)]; }
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.
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; }
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.
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.
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.
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;
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;
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 databaseproductsTemp = [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];
}
}
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; }
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]; }
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]; }
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]; }
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; }
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]; }
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]; }
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.
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
.
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;
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;
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;
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;
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;
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; }
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; }
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; }
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]; }
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; }
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.
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.
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.
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.
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];
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.
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.
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
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.