Sunday, May 24, 2009

iPhone tutorial: UITableView from the ground up, part 2

In part 1 of this tutorial we created a simple table view with just a few lines of code to show that, even though table views are a bit intimidating, they are possible to fully understand if you take small enough steps. This part of the tutorial is going to continue from where the last part ended, so if you haven't already created the "Table1" XCode project you will have to work through part 1 first.

Classes/Table1AppDelegate.m

We're going to start by exploring how to reuse cells, since this is something Apple more or less recommends us to do. The first hint Apple gives us is that there is only one way to initialise a UITableViewCell and that involves calling 'initWithFrame:reuseIdentifier' which takes a "reuse identifier" as the second argument. You can set this to nil - as we did in part 1 - if you really don't want to reuse the cell, but that will probably affect the performance of (large) table views significally.

The table view calls it's 'dataSource' delegate whenever it needs a cell for a specific row since it has no clue of how a specific cell (row) should look like (background colour, detail disclosure buttons, etc.) or what it should contain (text, images, etc.). However, if you  assign a "reuse identifier" (name) to the cell you provide to the table view, the table view will try to cache the cell for you. This means that the table view tries to store the cells created by the "data source" for later use so that the data source doesn' t have to create a new cell everytime - sometimes it will be able to reuse an existing cell.

The UITableView class has a method called 'dequeueReuseableCellWithIdentifier' which is used to retrieve a UITableViewCell object from the UITableView cache, if present. If the cell isn't present, it returns nil. Let's modify our 'tableView:cellForRowAtIndexPath' method to make use of this reuse-scheme. We'll call our cell "cell1" in the example below:

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

// try to retrieve "cell1" from the UITableView cache
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell1"];
if ( cell == nil ) {
// "cell 1" wasn't present in the cache, so create it
NSLog(@"creating cell for row %d...", indexPath.row);
cell = [[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"cell1"];
} else {
// "cell 1" was present in the cache, so log that we're reusing it
NSLog(@"reusing cell for row %d...", indexPath.row);
}

if ( indexPath.row == 0 ) { 
// assign a text to row 0
cell.text = [NSString stringWithFormat:@"this is row %d", indexPath.row];
}
return cell;
}

The comments in the source code above tries to explain what we're doing, so I won't elaborate on that. One thing is worth mentioning though and that is the 'indexPath.row' statement. If you look in the API docs for NSIndexPath you won't see any 'row' property. This is because "UITableView declares a category on NSIndexPath that enables you to get the represented row index (row property)", as can be read in the API docs for the UITableViewDelegate protocol. The "category" that is mentioned above is an Obejctive C feature which allows you to add methods to a class without actually subclassing it.

Exploring the reuse scheme

If you compile and run (CMD-Return) in XCode and wait for the simulator to start, you'll see a table with the text "this is row 0" in the first row. Apart from that it is empty, but you are still able to select rows 1 and 2 as well. If you check the console window (CMD-R) in XCode, you'll see somthing like this:

2009-05-24 18:29:06.375 Table1-2[8618:20b] creating cell for row 2...
2009-05-24 18:29:06.387 Table1-2[8618:20b] creating cell for row 1...
2009-05-24 18:29:06.394 Table1-2[8618:20b] creating cell for row 0...

The first thing to notice is that UITableView seems to have requested cells in the "wrong order", that is starting with row 2 instead of row 0. This is of course nothing you should take advantage of, but it's interesting to note. The really interesting thing to note, though, is that none of the cells seems to have been reused, even though we initialised all our cells with the same reuse identfier "cell1". What's going on, is the reuse mechanism broken or what?

Adding more rows

Let's see what happens if we change the number of rows in our table by editing 'tableView:numberOfRowsInSection' to make it return 10 instead of 3.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 10;
}

Build and run (CMD-Return), wait for the simulator to start and then bring the XCode console window to the front (CMD-R). Now scroll the table view by "moving your finger upwards" on the simulator and you should see something like this in the console window:

2009-05-24 18:44:49.417 Table1-2[8663:20b] creating cell for row 9...
2009-05-24 18:44:49.425 Table1-2[8663:20b] creating cell for row 8...
2009-05-24 18:44:49.426 Table1-2[8663:20b] creating cell for row 7...
2009-05-24 18:44:49.427 Table1-2[8663:20b] creating cell for row 6...
2009-05-24 18:44:49.431 Table1-2[8663:20b] creating cell for row 5...
2009-05-24 18:44:49.434 Table1-2[8663:20b] creating cell for row 4...
2009-05-24 18:44:49.435 Table1-2[8663:20b] creating cell for row 3...
2009-05-24 18:44:49.435 Table1-2[8663:20b] creating cell for row 2...
2009-05-24 18:44:49.436 Table1-2[8663:20b] creating cell for row 1...
2009-05-24 18:44:49.436 Table1-2[8663:20b] creating cell for row 0...
2009-05-24 18:45:25.892 Table1-2[8663:20b] reusing cell for row 1...
2009-05-24 18:45:25.975 Table1-2[8663:20b] reusing cell for row 0...
2009-05-24 18:45:39.922 Table1-2[8663:20b] reusing cell for row 0...
2009-05-24 18:45:41.884 Table1-2[8663:20b] reusing cell for row 0...
2009-05-24 18:45:42.053 Table1-2[8663:20b] reusing cell for row 1...
2009-05-24 18:45:42.187 Table1-2[8663:20b] reusing cell for row 0...
2009-05-24 18:45:43.552 Table1-2[8663:20b] reusing cell for row 1...
2009-05-24 18:45:43.566 Table1-2[8663:20b] reusing cell for row 0...

As you can see, the 10 first rows are created without reusing any cells, but as soon as you start scrolling the reuse mechanism seems to kick in. Everytime row 0, 1 and 2 "reappears" the cells seem to be reused.

Adding even more rows

Wonder what happens if 'tableView:numberOfRowsInSection' returns 100 instead of 10? Edit the method, build and run (CMD-Return), wait for the simulator to start, bring the XCode console window to the front (CMD-R), start scrolling around and you should see somthing like this:

2009-05-24 18:54:22.903 Table1-2[8704:20b] creating cell for row 10...
2009-05-24 18:54:22.926 Table1-2[8704:20b] creating cell for row 9...
2009-05-24 18:54:22.932 Table1-2[8704:20b] creating cell for row 8...
2009-05-24 18:54:22.939 Table1-2[8704:20b] creating cell for row 7...
2009-05-24 18:54:22.947 Table1-2[8704:20b] creating cell for row 6...
2009-05-24 18:54:22.951 Table1-2[8704:20b] creating cell for row 5...
2009-05-24 18:54:22.953 Table1-2[8704:20b] creating cell for row 4...
2009-05-24 18:54:22.955 Table1-2[8704:20b] creating cell for row 3...
2009-05-24 18:54:22.962 Table1-2[8704:20b] creating cell for row 2...
2009-05-24 18:54:22.964 Table1-2[8704:20b] creating cell for row 1...
2009-05-24 18:54:22.968 Table1-2[8704:20b] creating cell for row 0...
2009-05-24 18:54:38.025 Table1-2[8704:20b] reusing cell for row 10...
2009-05-24 18:54:40.375 Table1-2[8704:20b] creating cell for row 11...
2009-05-24 18:54:40.526 Table1-2[8704:20b] reusing cell for row 12...
2009-05-24 18:54:41.365 Table1-2[8704:20b] reusing cell for row 13...
2009-05-24 18:54:42.043 Table1-2[8704:20b] reusing cell for row 14...
2009-05-24 18:54:42.136 Table1-2[8704:20b] reusing cell for row 15...
2009-05-24 18:54:42.693 Table1-2[8704:20b] reusing cell for row 16...
2009-05-24 18:54:42.857 Table1-2[8704:20b] reusing cell for row 17...
2009-05-24 18:54:43.040 Table1-2[8704:20b] reusing cell for row 18...
2009-05-24 18:54:43.374 Table1-2[8704:20b] reusing cell for row 19...

Wow, now there is a lot of reusing going on! It seems as if the cells that appears for the first time when scrolling also are resued. That's why you see that rows 12 to 19 are reused. If you're really observant, you'll also notice something strange in the simulator. The text "this is row 0" appears in more than one place! What a nasty bug!! Hey, take it easy, it might not be a bug, just some proof of that the cell really is reused.

Insights into the reuse scheme

If the text "this is row 0" had appeared for all reused cells, I would have been able to explain this, but now I don't really understand it. I'm guessing it has to do with the fact that we're creating cells for row 0 to 11 with the same identifier. After that we start reusing cells and since there are more than one cell with the same identifier we sometimes get the row-0 instance, sometime the row-1 instance, and so on up to row-11 instance. After that we get the row-0 instance again, and so on.

If we modify the 'tableView:cellForRowAtIndexPath' to make output the cell 'reuseIdentifier' property as well as the pointer to the object we see that this guess might be true.

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

// try to retrieve "cell1" from the UITableView cache
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell1"];
if ( cell == nil ) {
// "cell 1" wasn't present in the cache, so create it
cell = [[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"cell1"];
NSLog(@"creating cell '%@' (%p) for row %d...", cell.reuseIdentifier, cell, indexPath.row);
} else {
// "cell 1" was present in the cache, so log that we're reusing it
NSLog(@"reusing cell '%@' (%p) for row %d...", cell.reuseIdentifier, cell, indexPath.row);
}

if ( indexPath.row == 0 ) { 
// assign a text to row 0
cell.text = [NSString stringWithFormat:@"this is row %d", indexPath.row];
}
return cell;
}

If you run the modified version you'll see somthing like this:

2009-05-24 20:12:51.907 Table1-2[8919:20b] creating cell 'cell1' (0x5284c0) for row 10...
2009-05-24 20:12:51.917 Table1-2[8919:20b] creating cell 'cell1' (0x5293c0) for row 9...
2009-05-24 20:12:51.929 Table1-2[8919:20b] creating cell 'cell1' (0x5287e0) for row 8...
2009-05-24 20:12:51.930 Table1-2[8919:20b] creating cell 'cell1' (0x526560) for row 7...
2009-05-24 20:12:51.931 Table1-2[8919:20b] creating cell 'cell1' (0x5292f0) for row 6...
2009-05-24 20:12:51.934 Table1-2[8919:20b] creating cell 'cell1' (0x528940) for row 5...
2009-05-24 20:12:51.937 Table1-2[8919:20b] creating cell 'cell1' (0x5297d0) for row 4...
2009-05-24 20:12:51.938 Table1-2[8919:20b] creating cell 'cell1' (0x520970) for row 3...
2009-05-24 20:12:51.939 Table1-2[8919:20b] creating cell 'cell1' (0x529940) for row 2...
2009-05-24 20:12:51.940 Table1-2[8919:20b] creating cell 'cell1' (0x529a60) for row 1...
2009-05-24 20:12:51.941 Table1-2[8919:20b] creating cell 'cell1' (0x529be0) for row 0...
2009-05-24 20:13:08.380 Table1-2[8919:20b] creating cell 'cell1' (0x52e290) for row 11...
2009-05-24 20:13:09.190 Table1-2[8919:20b] reusing cell 'cell1' (0x529be0) for row 12...
2009-05-24 20:13:09.517 Table1-2[8919:20b] reusing cell 'cell1' (0x529a60) for row 13...
2009-05-24 20:13:52.790 Table1-2[8919:20b] reusing cell 'cell1' (0x529940) for row 2...
2009-05-24 20:13:53.415 Table1-2[8919:20b] reusing cell 'cell1' (0x529940) for row 14...
2009-05-24 20:13:53.511 Table1-2[8919:20b] reusing cell 'cell1' (0x520970) for row 15...
2009-05-24 20:13:54.068 Table1-2[8919:20b] reusing cell 'cell1' (0x5297d0) for row 16...
2009-05-24 20:13:54.106 Table1-2[8919:20b] reusing cell 'cell1' (0x528940) for row 17...
2009-05-24 20:13:54.568 Table1-2[8919:20b] reusing cell 'cell1' (0x5292f0) for row 18...
2009-05-24 20:13:54.635 Table1-2[8919:20b] reusing cell 'cell1' (0x526560) for row 19...
2009-05-24 20:13:54.652 Table1-2[8919:20b] reusing cell 'cell1' (0x5287e0) for row 20...
2009-05-24 20:13:54.710 Table1-2[8919:20b] reusing cell 'cell1' (0x5293c0) for row 21...
2009-05-24 20:13:54.743 Table1-2[8919:20b] reusing cell 'cell1' (0x5284c0) for row 22...
2009-05-24 20:13:54.776 Table1-2[8919:20b] reusing cell 'cell1' (0x52e290) for row 23...
2009-05-24 20:13:54.810 Table1-2[8919:20b] reusing cell 'cell1' (0x529be0) for row 24...
2009-05-24 20:13:54.860 Table1-2[8919:20b] reusing cell 'cell1' (0x529a60) for row 25...
2009-05-24 20:13:54.893 Table1-2[8919:20b] reusing cell 'cell1' (0x529940) for row 26...
2009-05-24 20:13:54.943 Table1-2[8919:20b] reusing cell 'cell1' (0x520970) for row 27...
2009-05-24 20:13:54.993 Table1-2[8919:20b] reusing cell 'cell1' (0x5297d0) for row 28...
2009-05-24 20:13:55.060 Table1-2[8919:20b] reusing cell 'cell1' (0x528940) for row 29...
2009-05-24 20:13:55.126 Table1-2[8919:20b] reusing cell 'cell1' (0x5292f0) for row 30...
2009-05-24 20:13:55.193 Table1-2[8919:20b] reusing cell 'cell1' (0x526560) for row 31...
2009-05-24 20:13:55.293 Table1-2[8919:20b] reusing cell 'cell1' (0x5287e0) for row 32...
2009-05-24 20:13:55.393 Table1-2[8919:20b] reusing cell 'cell1' (0x5293c0) for row 33...
2009-05-24 20:13:55.543 Table1-2[8919:20b] reusing cell 'cell1' (0x5284c0) for row 34...
2009-05-24 20:13:55.726 Table1-2[8919:20b] reusing cell 'cell1' (0x52e290) for row 35...
2009-05-24 20:13:56.043 Table1-2[8919:20b] reusing cell 'cell1' (0x529be0) for row 36...
2009-05-24 20:13:57.194 Table1-2[8919:20b] reusing cell 'cell1' (0x529a60) for row 37...

If you study the output closely - especially the pointers within the parentheses - you'll see that the cells are reused in a recurring pattern. This suggests that all the 'cell1' UITableViewCell objects are placed on a circular queue inside UITableView. Once a cell has been removed from the head of  the queue and reused it is placed at the tail of the queue again. That a queue is used in the "cache" implementation is further suggested by the method name 'dequeueReusableCellWithIdentifier' since "dequeue" is a fancier way of saying "remove from the head".

Resetting cell contents before reuse

The solution to all these "strange" problems is to reset the contents of a reused cell before returning it to the table view. In our case that means setting the 'text' property every time - both for newly created cells and for reused cells.

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

// try to retrieve "cell1" from the UITableView cache
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell1"];
if ( cell == nil ) {
// "cell 1" wasn't present in the cache, so create it
cell = [[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"cell1"];
NSLog(@"creating cell '%@' (%p) for row %d...", cell.reuseIdentifier, cell, indexPath.row);
} else {
// "cell 1" was present in the cache, so log that we're reusing it
NSLog(@"reusing cell '%@' (%p) for row %d...", cell.reuseIdentifier, cell, indexPath.row);
}

// reset cell contents
cell.text = [NSString stringWithFormat:@"this is row %d", indexPath.row];
return cell;
}

Summary

Ok, now we have digged further down into the world of the UITableView class and hopefully it's even less frightening now that we have examined how the cell reuse scheme works. We're still very sloppy with our memory management - in fact, we're completely ignoring it - so bear in mind that these small tests are just that - small tests to increase our understanding.

No comments:

Post a Comment