8

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;
 }
Jonah
  • 4,810
  • 14
  • 63
  • 76

2 Answers2

16

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
  • 286,113
  • 34
  • 456
  • 610
  • 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 Sep 03 '09 at 03:09
  • 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 Sep 03 '09 at 03:23
  • 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 Sep 03 '09 at 03:26
  • 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 Sep 03 '09 at 03:27
  • 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 Sep 03 '09 at 03:46
  • See my final code in the answer. It's working great. Thanks for your help! – Jonah Sep 05 '09 at 03:51
5

If you do need to cache the heights, I did something like this (caching heights for a cell displaying an "article" object - article maybe one of several subclasses):

+ (CGFloat) heightForArticle: (Article*) article atWidth: (CGFloat) width {

    static NSCache* heightCache = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        heightCache = [NSCache new];
    });

    NSAssert(heightCache, @"Height cache must exist");

    NSString* key = @"unique"; //Create a unique key here
    NSNumber* cachedValue = [heightCache objectForKey: key];
    if( cachedValue ) 
        return [cachedValue floatValue];
    else {
        CGFloat height = 40;//Perform presumably large height calculation here

        [heightCache setObject: [NSNumber numberWithFloat: height] forKey: key];

        return height;
    }
}
Paul de Lange
  • 10,613
  • 10
  • 41
  • 56
  • This really helped me, I used it to cache the height and also to cache my table view cell images. Thanks :) – Supertecnoboff Feb 28 '16 at 12:02
  • Actually with respect, but I take back my comment. This doesn't work, I have noticed that sometimes, it return the wrong height values. So the cells end up being too big or too small at times. – Supertecnoboff Mar 03 '16 at 07:40