Apr 27

The UITableView class is a wonder of efficient memory management, if you use it correctly.

Here’s the standard template code that Xcode generates when you create a subclass of UITableViewController:

  1. // Customize the appearance of table view cells.  
  2. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {  
  3.       
  4.     static NSString *CellIdentifier = @"Cell";  
  5.       
  6.     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];  
  7.     if (cell == nil) {  
  8.         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];  
  9.     }  
  10.       
  11.     // Set up the cell...  
  12.       
  13.     return cell;  
  14. }  

The keys here are the CellIdentifier variable and the call to dequeueReusableCellWithIdentifier, which enable the iPhone OS to reuse existing instances of UITableViewCell whenever possible.

(Don’t create a unique reuse identifier for each row as I’ve seen some developers do. Yes, it’s much easier to deal with asynchronous download of images for each row if you know how to uniquely identify the cell, and you know that the cell is still in memory. But that totally defeats the efficient memory management that UITableView is capable of.)

Under normal circumstances a UITableView will create one instance of a UITableViewCell per row that is visible on the screen. As you scroll, the cell instance that just rolled off the screen will be reused for the cell that is about to appear.

To verify that this memory management is working as it should, add a log statement each time a new cell is created:

  1. if (cell == nil) {  
  2.     DLog(@"creating a new cell");  
  3.     cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];  
  4. }  

When you run your app and start scrolling your table view, you should not see any creation of cells beyond the initial list (plus one). If you see “creating a new cell” log statements scrolling off the screen as you scroll the table view, you’ve got a problem.

If you just follow the standard Xcode template above, you should be fine. However if you’re loading a Nib for a custom table view cell layout using Apple’s recommended way, there’s an important detail you must not forget. (Tip of the hat to Jeff LaMarche for inspiring this blog post.)

Here’s the typical NIB loading code from Apple:

  1. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {  
  2.     static NSString *CellIdentifier = @"CheckedTableViewCell";  
  3.       
  4.     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];  
  5.     if (cell == nil) {  
  6.         DLog(@"creating a new cell");  
  7.       
  8.         // Load the table view cell from a Nib file.  
  9.         [[NSBundle mainBundle] loadNibNamed:@"CheckedTableViewCell" owner:self options:nil];  
  10.       
  11.         // The checkedTableViewCell property is just a temporary placeholder for loading the Nib.  
  12.         cell = checkedTableViewCell;  
  13.       
  14.         // We don't need this anymore, so set to nil.  
  15.         self.checkedTableViewCell = nil;  
  16.     }  
  17.       
  18.     return cell;  
  19. }  

The key here is that the CellIdentifier value must also be entered into Interface Builder, like this:

UITableViewCell-Identifier.png

If you don’t do this, then UITableViewCells will not be reused. (A telltale sign of this is that you’ll see lots of “creating a new cell” log messages.) There is no compiler or runtime warning if you fail to enter this critical piece of information into Interface Builder. So that log statement can be a useful warning.

(BTW, if you’re wondering what DLog is, then see this post: The Evolution of a Replacement for NSLog.)

written by Nick \\ tags: , , , ,

Sep 02

To change the height of the cells in a UITableView, use the property rowHeight. Or change the value in Interface Builder.

There is a method called heightForRowAtIndexPath in UITableViewDelegate, where you can also set the height. However this is NOT recommended. Apple’s release notes states the following about this method:

It is very, very expensive to customize row heights (via tableView:heightForRowAtIndexPath:).

It makes sense that having rows with different heights in your table will wreak havoc with the table view’s reuse of cells. But it also turns out that if you return the same value from this method for each row, you also suffer a significant performance penalty.

So just use the simple rowHeight property. It’s less code to type, and it’s significantly faster.

Thanks to Brent for the performance testing.

Addendum: If you really, really must have different row heights in your table. Then heightForRowAtIndexPath is the only way to achieve this. If your table only has a handful of rows then performance will not be a big issue. But if you have hundreds of rows, all with varying heights, then I would suggest looking at constructing the table using HTML in a web view instead.

written by Nick \\ tags: , ,

Jan 27

Have you every been frustrated with the small circular touch area when setting the accessory type of a UITableViewCell to be UITableViewCellAccessoryDisclosureIndicator? Unless you have child fingers, it’s very difficult to select consistently.

Here’s one way to improve the user experience and create the same effect of a larger touch pad like you see on the YouTube application. To do this, you need to implement a custom table view cell. You can either create your own subclass, or you can add subviews to the cell’s contextView. This example shows how to do the latter.

When setting up my UITableViewCells I like to create methods that provide the elements I wish to use to add to the cell’s contentView. Here I am creating a UIButton of type UIButtonTypeDetailDisclosure to be added to each cell in the table. The button possesses an area of 44 x 44 pixels and occupies the right most area of the table view cell that will be 320 pixels wide and 44 pixels tall. Note here that you define a target and action of your own rather than use the callbacks for the accessory type defined in the UITableViewDelegate protocol.

  1. - (UIButton *) getDetailDiscolosureIndicatorForIndexPath: (NSIndexPath *) indexPath   
  2. {  
  3.     UIButton *button = [UIButton buttonWithType: UIButtonTypeDetailDisclosure];  
  4.     button.frame = CGRectMake(320.0 - 44.0, 0.0, 44.0, 44.0);  
  5.     [button addTarget:self action:@selector(detailDiscolosureIndicatorSelected:) forControlEvents:UIControlEventTouchUpInside];  
  6.     return button;  
  7. }    

The following shows how to add the button to the cell in the cellForRowAtIndexPath method in your table view controller class. As you add subviews to the contentView, be sure your detailed disclosure indicator is rendered on top. Also, ensure you are properly handling the reuse of your cells.

  1. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath   
  2. {  
  3. ...  
  4. [cell.contentView addSubview: [self getDetailDiscolosureIndicatorForIndexPath: indexPath]];  
  5. ...  
  6. }   

Finally to process the selection event you need the following code:

  1. - (void) detailDiscolosureIndicatorSelected: (UIButton *) sender   
  2. {     
  3.     //  
  4.     // Obtain a reference to the selected cell  
  5.     //  
  6.     UITableViewCell *cell = [self.tableView cellForRowAtIndexPath: [self.tableView indexPathForSelectedRow]];  
  7.   
  8.     //  
  9.     // Do something like render a detailed view  
  10.     //        
  11.     ...  
  12. }   

written by Jess \\ tags: , ,