Search code examples
iosobjective-cuitableviewcustom-cell

How to calculate the height of a custom expand/collapsable UITableViewCell dynamically


I have an interesting scenario. I have some custom UITableViewCells designed in separate xib files. One of them have an UIImageView, which will load images whom size are not constant, so it means UIImageView's height must be flexible. The cell also has a UIView containing some UILabels, and UIView's size is constant, say 100. I want to expand and collapse the cells on didselectrowatindexpath event. To collapse the cell I have to hide the UIView that have some labels. Please guide me in this regard to achieve the goal. Also my question is "How can I calculate the height of the row when cell is expanded and collapsed." Thanks

Edit: This is what I have tried. . .But failed

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    static DynamicTableViewCell *cell = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    });
    
    [self setUpCell:cell atIndexPath:indexPath];
    
    CGFloat cellHeight = [self calculateHeightForConfiguredSizingCell:cell];
    if(cellHeight >0) {
        if(cell.isExpanded) {
            
            return cellHeight;
        }
        else
        {
            return (cellHeight - detailViewHeight);      // cell.detailView.frame.size.height = 100
        }

    }
    else {
        return cellHeight;
    }
}


Solution

  • First of all you should not have any reference to instance to your cell, because of performance reasons.

    Second you should use models to build your cells in the right way. Provided code does not show usage of model storage at all.

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        // Say you have a array of models NSArray<YourModel *> *dataSource;
    
        YourModel *model = dataSource[indexPath.row]; // hope you have only one section.
    }
    

    Third it's a good way to use any architecture like MVVM or VIPER, or MVC, because if you have no one you will probably have problems in future support of your product. So in case of MVVM YourModel is like ViewModel.

    To define a state of dynamic height cell, you use property isExpanded. That's a good point, but it should be defined in another place - YourModel. If you'll do that in a proper way, you'll know the state of cell without the cell, actually.

    @interface YourModel ...
    @property BOOL isExpanded;
    @end
    

    Be sure that you correctly change your state in didSelect:

    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
        YourModel *model = dataSource[indexPath.row];
        model.isExpanded = !model.isExpanded; // inverse works like switch
        // Then relayout your row
        [tableView beginUpdates];
        // I commented next line because it might not be needed for you
        // Test it please
        // [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        [tableView endUpdates]; 
    }
    

    So back to heightForRow:

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        // Say you have a array of models NSArray<YourModel *> *dataSource;
    
        YourModel *model = dataSource[indexPath.row]; // hope you have only one section.
    
        // It's better to extract next code to function or any class, but not in your YourModel.
        CGSize imageSize = [model.image doSizeCalulation]; // Use your algorithm to calculate image size
        CGSize shortText = [model.shortText sizeWithAttributes:@[/* font attributes */]];
    
        // If the cell is expanded then we should calculate height of text else height is zero.
        CGSize fullText = model.isExpanded ? [model.fullText sizeWithAttributes:@[/* font attributes */]]; : 0; 
    
        // just sum
        return imageSize.height + shortText.height + fullText.height;
    }
    

    Another way to achieve this to use UITableViewAutomaticDimension (just return it in heightForRow method). In that case you should properly setup constraints, and in runtime change the constant of fullText's height constraint depending on isExpanded property.