26
votes

I have a UITableView that is populated with custom cells (inherited from UITableViewCell), each cell contains a UIWebView that is automatically resize based on it's contents. Here's the thing, how can I change the height of the UITableView cells based on their content (variable webView).

The solution must be dynamic since the HTML used to populate the UIWebViews is parsed from an ever changing feed.

I have a feeling I need to use the UITableView delegate method heightForRowAtIndexPath but from it's definition:

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ;//This needs to be variable
}

I can't access the cell or it's contents. Can I change the height of the cell in cellForRowAtIndexPath?

Any help would be grand. Thanks.

Note

I asked this question over 2 years ago. With the intro of auto layout the best solution for iOS7 can be found:

Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

and on iOS8 this functionality is built in the SDK

12

12 Answers

29
votes

This usually works pretty well:

Objective-C:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewAutomaticDimension;
}

Swift:

override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
    return UITableViewAutomaticDimension;
}
20
votes

The best way that I've found for dynamic height is to calculate the height beforehand and store it in a collection of some sort (probably an array.) Assuming the cell contains mostly text, you can use -[NSString sizeWithFont:constrainedToSize:lineBreakMode:] to calculate the height, and then return the corresponding value in heightForRowAtIndexPath:

If the content is constantly changing, you could implement a method that updated the array of heights when new data was provided.

6
votes
self.tblVIew.estimatedRowHeight = 500.0; // put max you expect here.
self.tblVIew.rowHeight = UITableViewAutomaticDimension;
5
votes

I tried many solutions, but the one that worked was this, suggested by a friend:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    int height = [StringUtils findHeightForText:yourLabel havingWidth:yourWidth andFont:[UIFont systemFontOfSize:17.0f]];

    height += [StringUtils findHeightForText:yourOtherLabel havingWidth:yourWidth andFont:[UIFont systemFontOfSize:14.0f]];

    return height + CELL_SIZE_WITHOUT_LABELS; //important to know the size of your custom cell without the height of the variable labels
}

The StringUtils.h class:

#import <Foundation/Foundation.h>

@interface StringUtils : NSObject

+ (CGFloat)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font;

@end

StringUtils.m class:

#import "StringUtils.h"

@implementation StringUtils

+ (CGFloat)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
    CGFloat result = font.pointSize+4;
    if (text) {
        CGSize size;

        CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX)
                                          options:NSStringDrawingUsesLineFragmentOrigin
                                       attributes:@{NSFontAttributeName:font}
                                          context:nil];
        size = CGSizeMake(frame.size.width, frame.size.height+1);
        result = MAX(size.height, result); //At least one row
    }
    return result;
}

@end

It worked perfectly for me. I had a Custom Cell with 3 images with fixed sizes, 2 labels with fixed sizes and 2 variable labels.

4
votes

The big problem with cells with dynamic height in iOS is that the table vc must calculate and return a height of each cell before the cells are drawn. Before a cell is drawn, though, it doesn't have a frame and thus no width. This causes a problem if your cell is to change its height based on, say, the amount of text in the textLabel, since you do not know its width.

A common solution that I've seen is that people define a numeric value for the cell width. This is a bad approach, since tables can be plain or grouped, use iOS 7 or iOS 6 styling, be displayed on an iPhone or iPad, in landscape or portrait mode etc.

I struggled with these issues in an iOS app of mine, which supports iOS5+ and both iPhone and iPad with multiple orientations. I needed a convenient way to automate this and leave the logic out of the view controller. The result became a UITableViewController sub class (so that it can hold state) that supports default cells (Default and Subtitle style) as well as custom cells.

You can grab it at GitHub (https://github.com/danielsaidi/AutoSizeTableView). I hope it helps those of you who still struggle with this problem. If you do check it out, I'd love to hear what you think and if it worked out for you.

3
votes

Here is code that I used for dynamic cell height when fetching tweets from twitter and then storing them in CoreData for offline reading.

Not only does this show how to get the cell and data content, but also how to dynamically size a UILabel to the content with padding

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    Tweet *tweet = [self.fetchedResultsController objectAtIndexPath:indexPath];

    NSString* text = tweet.Text;

    TweetTableViewCell *cell = (TweetTableViewCell*)[self tableView:tableView cellForRowAtIndexPath:indexPath];

    //Set the maximum size
    CGSize maximumLabelSize = cell.tweetLabel.frame.size;
    CGPoint originalLocation = cell.tweetLabel.frame.origin;

    //Calculate the new size based on the text
    CGSize expectedLabelSize = [text sizeWithFont:cell.tweetLabel.font constrainedToSize:maximumLabelSize lineBreakMode:cell.tweetLabel.lineBreakMode];


    //Dynamically figure out the padding for the cell
    CGFloat topPadding = cell.tweetLabel.frame.origin.y - cell.frame.origin.y;


    CGFloat bottomOfLabel = cell.tweetLabel.frame.origin.y + cell.tweetLabel.frame.size.height;
    CGFloat bottomPadding = cell.frame.size.height - bottomOfLabel;


    CGFloat padding = topPadding + bottomPadding;


    CGFloat topPaddingForImage = cell.profileImage.frame.origin.y - cell.frame.origin.y;
    CGFloat minimumHeight = cell.profileImage.frame.size.height + topPaddingForImage + bottomPadding;

    //adjust to the new size
    cell.tweetLabel.frame = CGRectMake(originalLocation.x, originalLocation.y, cell.tweetLabel.frame.size.width, expectedLabelSize.height);


    CGFloat cellHeight = expectedLabelSize.height + padding;

    if (cellHeight < minimumHeight) {

        cellHeight = minimumHeight;
    }

    return cellHeight;
}
2
votes

Also i think such an algorithm will suit you:

1) in cellForrowAtIndexPath you activate your webviews for loading and give them tags equal to indexPath.row

2) in webViewDidFinishLoading you calculate the height of the content in the cell, and compose a dictionary with keys and values like this: key= indexPath.row value = height

3)call [tableview reloadData]

4) in [tableview cellForRowAtIndexPath:indexPath] set proper heights for corresponding cells

1
votes

This is one of my nice solution. it's worked for me.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    cell.textLabel.text = [_nameArray objectAtIndex:indexPath.row];
    cell.textLabel.numberOfLines = 0;
    cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewAutomaticDimension;
}

We need to apply these 2 changes.

1)cell.textLabel.numberOfLines = 0;
  cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;

2)return UITableViewAutomaticDimension;
1
votes

In Swift 4+ you can set it dinamic

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableView.automaticDimension
}
0
votes

I always implement this in all my cells in a super cell class because for some reason UITableViewAutomaticDimension doesn't work so well.

-(CGFloat)cellHeightWithData:(id)data{
    CGFloat height = [[self contentView] systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    [self fillCellWithData:data]; //set the label's text or anything that may affect the size of the cell
    [self layoutIfNeeded];
    height = [[self contentView] systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    return height+1; //must add one because of the cell separator
}

just call this method on your -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPathusing a dummy cell.

note: this works only with autolayout, but it also works with ios 7 and later.

pd: don't forget to check the checkbox on the xib or storyboard for "preferred width explicit" and set the static width (on the cmd + alt + 5 menu)

0
votes

Swift Use custom cell and labels. Set up the constrains for the UILabel. (top, left, bottom, right) Set lines of the UILabel to 0

Add the following code in the viewDidLoad method of the ViewController:

tableView.estimatedRowHeight = 68.0
tableView.rowHeight = UITableViewAutomaticDimension

// Delegate & data source

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableViewAutomaticDimension;
}
0
votes

I had very large test in UILabel. Above all fail to work, then i create category for string as below and got the exact height

- (CGFloat)heightStringWithEmojifontType:(UIFont *)uiFont ForWidth:(CGFloat)width {

// Get text
CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), (CFStringRef) self );
CFIndex stringLength = CFStringGetLength((CFStringRef) attrString);

// Change font
CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef) uiFont.fontName, uiFont.pointSize, NULL);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, stringLength), kCTFontAttributeName, ctFont);

// Calc the size
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
CFRange fitRange;
CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(width, CGFLOAT_MAX), &fitRange);

CFRelease(ctFont);
CFRelease(framesetter);
CFRelease(attrString);

return frameSize.height + 10;}