lobianco
lobianco

Reputation: 6276

Dynamic heightForRowAtIndexPath: with many (thousands) of cells

I'm having some performance issues when using a dynamic value in heightForRowAtIndexPath: (I know it's this method because if I set a static value the responsiveness increases substantially). My table contains around 3000 cells.

I understand why the performance suffers when using a dynamic value (mainly because the calculations in the method must be performed once for every cell in the table before the data can be shown), but I can't figure out how to make it more efficient.

In many of the similar questions I've come across, the proposed solution was to use NSString's sizeWithFont method in heightForRowAtIndexPath: to speed things up. I currently do that but the table still takes roughly ~1.5sec to load (and reload, which is done somewhat frequently). This is just too long and I need to optimize it.

The code I'm currently using (the essence of it at least) is below:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell;
    UILabel *label = nil;

    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

        label = [[UILabel alloc] initWithFrame:CGRectZero];
        // set up label..
        [[cell contentView] addSubview:label];

    }

    NSDictionary *dict = alphabetDict; //dictionary of alphabet letters (A-Z). each key contains an NSArray as its object
    CGFloat rightMargin = 50.f; //padding for the tableview's index titles

    NSString *key = [[[dict allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section];
    NSArray *array = [dict objectForKey:key];
    NSString *cellText = [array objectAtIndex:indexPath.row];

    //TABLE_WIDTH is 268.f, CELL_MARGIN is 14.f
    CGSize constraintSize = CGSizeMake(TABLE_WIDTH - (CELL_MARGIN * 2) - rightMargin, 20000.0f);
    CGSize labelSize = [cellText sizeWithFont:[UIFont systemFontOfSize:17] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
    [label setFrame:CGRectMake(CELL_MARGIN, CELL_MARGIN, TABLE_WIDTH - (CELL_MARGIN * 2) - rightMargin, labelSize.height)];
    [label setText:cellText];

    return cell;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //TODO make this faster

    NSDictionary *dict = alphabetDict;
    CGFloat rightMargin = 50.f;

    NSString *key = [[[dict allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section];
    NSArray *array = [dict objectForKey:key];
    NSString *cellText = [array objectAtIndex:indexPath.row];

    CGSize constraintSize = CGSizeMake(TABLE_WIDTH - (CELL_MARGIN * 2) - rightMargin, 20000.0f);
    CGSize labelSize = [cellText sizeWithFont:[UIFont systemFontOfSize:17] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];

    return labelSize.height + (CELL_MARGIN * 2) + 16.f;  
}

Could someone point me in the right direction to streamline this code further? Thanks!

Upvotes: 0

Views: 4284

Answers (3)

Jim
Jim

Reputation: 5960

crypticcoder has a good suggestion. I would suggest a variation of that, which might be more flexible, and support table changes (insert, delete, reorder).

Create a class that is a subclass of UITableViewCell and add a cellHeight property adn ivar cellHeight_ to it. Synthesize it this way:

@synthesize cellHeight=cellHeight_;

Calculate the cell height in a lazy way with this code inside the class:

-(CGFloat) cellHeight{
    if (cellHeight_ != 0)
        return cellHeight_;
    else {
        NSDictionary *dict = alphabetDict;
        CGFloat rightMargin = 50.f;

        NSString *key = [[[dict allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section];
        NSArray *array = [dict objectForKey:key];
        NSString *cellText = [array objectAtIndex:indexPath.row];

        CGSize constraintSize = CGSizeMake(TABLE_WIDTH - (CELL_MARGIN * 2) - rightMargin, 20000.0f);
        CGSize labelSize = [cellText sizeWithFont:[UIFont systemFontOfSize:17] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];

        cellHeight_ = labelSize.height + (CELL_MARGIN * 2) + 16.f; 
        return cellHeight_; 
    }
}

Like sch, I don't like the sort in there, but if it's not really the performance problem, I'm leaving it there.

If you want to change the contents of a cell, make sure you reset cellHeight_ to zero so it is recalculated lazily (when it's needed again).

Upvotes: 0

SayeedHussain
SayeedHussain

Reputation: 1724

I have faced this similar situation before. My solution is to do all the calculations and store the heights in an NSMutableArray called heightsArray before you do [tableView reloadData]. Then

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
   [heightsArray objectAtIndex:indexpath.row];
}

No calculations while scrolling up and down. Plain reading from the array. The initial hold-up will be more because of the calculation being done upfront but it is worth it in terms of performance gains while scrolling. You can also perform the calculation on a background thread thereby saving processing time. Just do all that in a method which you can call on a background thread and prepare your heights array.

Upvotes: 5

sch
sch

Reputation: 27506

The most time consuming part in heightForRowAtIndexPath is:

[[[dict allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section];

So, you can start by adding a property sortedKeys and use it like this:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *key = [sortedKeys objectAtIndex:indexPath.section];
    CGFloat rightMargin = 50.f;
    // ...
}

Upvotes: 1

Related Questions