views:

919

answers:

2

I have a tableview with large images that fill the cells and the row heights are set based on the image size. Unfortunately, the table jerks badly when scrolling to the next cell.

I've been told that my tableview will scroll more smoothly if I cache the row heights and the images before they are loaded into the table. All my data are stored in a plist.

How do I go about caching something? What does the code look like and where does it go?

Thanks!

Here's my code for loading the images:

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

    static NSString *detailTableViewCellIdentifier = @"Cell";

    DetailTableViewCell *cell = (DetailTableViewCell *) 
        [tableView dequeueReusableCellWithIdentifier:detailTableViewCellIdentifier];

    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"DetailTableViewCell" owner:self options:nil];
    for(id currentObject in nib)
    {
        cell = (DetailTableViewCell *)currentObject;
    }
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSString *Path = [[NSBundle mainBundle] bundlePath];
    NSString *MainImagePath = [Path stringByAppendingPathComponent:([[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:@"MainImage"])];

    cell.mainImage.image = [UIImage imageWithContentsOfFile:MainImagePath];

    return cell;
}

I'm also using the following for calculating the row height:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    AppDelegate *appDelegate = (DrillDownAppAppDelegate *)[[UIApplication sharedApplication] delegate];
    NSString *Path = [[NSBundle mainBundle] bundlePath];
    NSString *MainImagePath = [Path stringByAppendingPathComponent:([[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:@"MainImage"])];
    UIImage *imageForHeight = [UIImage  imageWithContentsOfFile:MainImagePath]; 
    imageHeight = CGImageGetHeight(imageForHeight.CGImage);  
    return imageHeight;
}

EDIT: Here is the final code below.

#define PHOTO_TAG 1
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 static NSString *CellIdentifier = @"Photo";

 UIImageView *photo;
 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

 AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
 UIImage *theImage = [UIImage imageNamed:[[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:@"MainImage"]];

 imageHeight = CGImageGetHeight(theImage.CGImage);
 imageWidth = CGImageGetWidth(theImage.CGImage);

if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    photo = [[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imageWidth, imageHeight)] autorelease];
    photo.tag = PHOTO_TAG;
    [cell addSubview:photo];
   } else {
    photo = (UIImageView *) [cell viewWithTag:PHOTO_TAG];
    [photo setFrame:CGRectMake(0, 0, imageWidth, imageHeight)];
   }

 photo.image = theImage;
 return cell;
 }
+3  A: 

Caching is not a panacea for tableview performance. Caching is only valuable if there is something expensive to calculate, and you can avoid calculating it. If, on the other hand, you simply have too many views in your UITableViewCell, then caching will do nothing for you. If your row heights are all the same, then there's nothing to cache. If you use +[UIImage imageNamed:], then the system is already caching your images for you.

The most common first-order problem with UITableViewCells is putting too many subviews in them. How have you constructed your cell? Have you spent time studying the Table View Programming Guide, particularly A Closer Look at Table-View Cells? Understanding this document will save you much grief later.

EDIT: (Based on code above)

First, you're fetching a reusable cell, and then immediately throwing it away, reading a NIB and iterating over all the top level objects looking for a cell (one that looks almost exactly like the one you just threw away). Then you work out a string, which you use to open a file and read the contents. You do this every time UITableView wants a new cell, which is a lot. And you do it over and over again for the same rows.

Then, when UITableView wants to know the height, you read the image off of disk again. And you do that every time UITableView asks (and it may ask many times for the same row, though it does try to optimize this).

You should start by reading the UITableView Programming Guide I link above. That's hopefully going to help a lot. When you've done that, here are the things you should be thinking about:

  • You indicated that there is nothing but a single image view in this cell. Do you really need a NIB for that? If you do stick with a NIB (and there are reasons to use them in some case), then read the Programming Guide about how to implement a NIB-base cell. You should be using IBOutlet, not trying to iterate over the top-level objects.

  • +[UIImage imageNamed:] will automatically find files in your Resources directory without you having to work out the bundle's path. It will also cache those images for you automatically.

  • The point of -dequeueReusableCellWithIdentifier: is to fetch a cell that UITableView is no longer using and that you can reconfigure rather than you making a new one. You're calling it, but you immediately throw it away. You should check if it returned nil, and only load it out of the NIB if it did. Otherwise, you just need to change the image. Again, read the Programming Guide; it has many, many examples of this. Just make sure that you really try to understand what -dequeueReusableCellWithIdentifier: is doing, and don't treat it as just something you type at this point in the program.

Rob Napier
Rob, Thanks for the response. I only have one view for the cell and that is a single image. I got the idea of caching the images from a previous question I asked: http://stackoverflow.com/questions/1352479/tricks-for-improving-iphone-uitableview-scrolling-performance
Jonah
I added the code above in case it's any help. Thanks!
Jonah
rpetrich's answers are good. Regarding caching rowHeight, the easiest solution is to maintain an array of NSNumbers that store the calculated height for each row. You can start by having them all be 0, and if it's 0, then you calculate it and store the result in the array. If it's not zero, you return it. Do some profiling, though, in Instruments and take a look at where you're *really* spending your time before making too many random changes. Row height calculations may be the least of your issues. Maybe you're scaling the images too often (seems a likely possibility). Instruments will help.
Rob Napier
oh, wow.... yeah that's going to be really slow.... You're currently bypassing every optimization iPhone offers :D I'll need to edit my above answer to provide better detail. I'm shocked it's only sometimes a little jerky. Wow. That's doing a lot of work.
Rob Napier
I'll check it out in instruments. In order to improve performance I am not scaling the images. They are all set to the appropriate size before being loaded into the program.
Jonah
I'll look forward to your ideas! I posted a bit of code in another answer that I've experimented with, but without much success. Thanks again for the help!
Jonah
See my final code in the answer. It's working great. Thanks for your help!
Jonah
A: 

Here is another option I've tried. It scrolls easier, but for some reason the images begin to load incorrectly and pop up seemingly at random after scrolling around a bit.

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

static NSString *detailTableViewCellIdentifier = @"Cell";

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

AppAppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *Path = [[NSBundle mainBundle] bundlePath];
NSString *MainImagePath = [Path stringByAppendingPathComponent:([[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:@"MainTrackImage"])];

UIImageView *cellImageView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:MainImagePath]];
[cell addSubview:cellImageView];
return cell;
}
Jonah
This is closer to what you want. But you don't want to add the cellImageView to the cell every time. You only want to do that when you're making a new cell. In the above code, you fetch a pre-existing cell (one that already has a cellImageView in it), and you then slap another cellImageView into it (and then do that repeatedly). You want a single imageView in the cell. The only thing you want to do is change the image in it.
Rob Napier