Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic UILabel Heights/Widths in UITableViewCell in all Orientations

Tags:

ios

uikit

My question essentially boils down to the best way to support dynamic heights of UILabel's (and I suppose other elements) in a UITableCell, and also correctly resize the label width/height and cell heights when rotating.

I'm aware of how to get the expected height of UILabels, and size the heights accordingly, but things seem to get pretty tricky when you support landscape orientation as well.

I'm thinking this might go the route of layoutSubviews, but I'm not sure how you would combine that with the table needing to calculate the height of cells. Another interesting post sets the frame manually on init to make sure they are doing calculations off a known quantity, but that only addresses part of the issue.

So here's an image of what I'm dealing with, with red arrows pointing to the dynamic height labels, and blue arrows pointing towards the cells that will change height.

Dynamic UILabel/Cell Height/Width

I've managed to get it working correctly, but not sure if its the correct method.

Here's a few of the things I learned:

  • The cell.frame in cellForRowAtIndexPath always gives its size in portrait mode. (ie. for the iphone app, it always reports 320).
  • Storing a prototype cell for reference in a property has the same issue, always in portrait mode
  • autoresizing masks for the width of labels seem to be useless in this use case, and in fact cause issues with the calculated height on rotation
  • The tableView.frame in cellForRowAtIndexPath gives its size in the correct orientation
  • You need to call [tableView reloadData] on rotation. I couldn't find any other way to update the cell and label heights

Here are the steps I took to get this working for me:

  1. Disable any autoresize masks on UILabels, as they interfere with getting the right label height for some reason
  2. On viewDidLoad I grab a reference to each label font, and a float for the label width percentage compared to the tableView in portrait

    CWProductDetailDescriptionCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
    self.descriptionLabelWidthPercentage = cell.descriptionLabel.frame.size.width / 320;
    self.descriptionLabelFont = cell.descriptionLabel.font;
    
  3. In heightForRowAtIndexPath I calculate the cell height using the tableView width and the percentage I already grabbed:

    case TableSectionDescription:
        CGFloat labelWidth = self.tableView.frame.size.width * self.descriptionLabelWidthPercentage;
        CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(labelWidth, MAXFLOAT) AndFont:self.descriptionLabelFont];
        return newLabelFrameSize.height + kTextCellPadding;
    
  4. In cellForRowAtIndexPath I calculate the frame for the label frame and update it

    cell = [tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
    ((CWProductDetailDescriptionCell *)cell).descriptionLabel.text = self.product.descriptionText;
    CGRect oldFrame = ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame;
    CGFloat labelWidth = self.tableView.frame.size.width * self.descriptionLabelWidthPercentage;
    CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(labelWidth, MAXFLOAT) AndFont:((CWProductDetailDescriptionCell *)cell).descriptionLabel.font];
    ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, labelWidth, newLabelFrameSize.height);
    
  5. In didRotateFromInterfaceOrientation you need to [self.tableView reloadData]

Problems with this method:

  1. You can't leverage the autoresizing masks and margins you might set in IB
    • those don't seem to fire until after you've returned the cell in cellForRowAtIndexPath (not sure if this is exactly true)
    • they mess up the height calculation somehow, and your labels end up taller than desired in landscape.
  2. If your label's width isn't an equal percentage in portrait and landscape, you'll need to know both percentages
  3. You need to know/calculate your labels width percentages. Ideally you would be able to calculate these on the fly after an auto-resizing mask has done its thing.
  4. It just feels clumsy. I have a feeling I'm going to run into more headaches in editing mode with indenting.

So. What is the right way to do this? I've seen lots of SO questions, but none quite address this exactly.

like image 393
Bob Spryn Avatar asked Apr 20 '12 01:04

Bob Spryn


1 Answers

Apparently it took writing out my giant question and taking a break from the computer to figure it out.

So here's the solution:

The main steps to dynamically calculate UILabel heights on the fly, and take advantage of autoresizing masks:

  1. Keep a prototype cell for each cell that will need to have its height adjusted
  2. In heightForRowAtIndexPath
    • adjust your reference cell width for the orientation you are in
    • thenyou can then reliably use the contained UILabel frame widths for calculating height
  3. Don't do any frame adjustments in cellForRowAtIndexPath aside from initial setup
    • UITableViewCells perform all their layout stuff right after this is called, so your efforts at best are futile, at worst they screw things up
  4. Do your frame adjustments in willDisplayCell forRowAtIndexPath
    • I'm furious I forgot about this
    • It occurs after all the UITableViewCell internal layout and right before it draws
    • Your labels with autoresizing masks on them will already have correct widths you can use for calculations

In viewDidLoad grab references to cells that will need height adjustments

Note that these are retained/strong(ARC), and never added to the table itself

self.descriptionReferenceCell = [self.tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
self.attributeReferenceCell = [self.tableView dequeueReusableCellWithIdentifier:@"AttributeCell"];

In heightForRowAtIndexPath do calculation for variable height cells

I first update the width of my reference cell so that it lays out its subviews (UILabels), which I can then use for calculations. Then I use the updated label width to determine the height of my labels, and add any necessary cell padding to calculate my cell height. (Remember these calculations are only necessary if your cell changes heights along with your label).

UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
float width = UIDeviceOrientationIsPortrait(orientation) ? 320 : 480;

case TableSectionDescription:
    CGRect oldFrame = self.descriptionReferenceCell.frame;
    self.descriptionReferenceCell.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, width, oldFrame.size.height);
    CGFloat referenceWidth = self.descriptionReferenceCell.descriptionLabel.frame.size.width;
    CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(referenceWidth, MAXFLOAT) AndFont:self.descriptionReferenceCell.descriptionLabel.font];
    return newLabelFrameSize.height + kTextCellPadding;

In cellForRowAtIndexPath Don't do any dynamic layout related updates relying on orientation

In willDisplayCell forRowAtIndexPath Update the label heights themselves

Since the table cells have performed layoutSubviews, the labels already have the correct widths, which I use to calculate the exact height of the labels. I can safely update my label heights.

case TableSectionDescription:
    CGRect oldFrame = ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame;
    CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(oldFrame.size.width, MAXFLOAT) AndFont:((CWProductDetailDescriptionCell *)cell).descriptionLabel.font];
    ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.size.width, newLabelFrameSize.height);
    break;

Reload Data on rotate

Your visible cells won't update their heights and labels if you don't reload data.

-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
    [self.tableView reloadData];
}

The only other thing to point out is that I'm using a helper class for calculating the label height that just wraps the basic call. Really its not saving much of any code anymore (I used to have other stuff in there), so just use the normal method straight off your string.

- (CGSize) sizeForString:(NSString*)string WithConstraint:(CGSize)constraint AndFont:(UIFont *)font {
    CGSize labelSize = [string sizeWithFont:font
                          constrainedToSize:constraint 
                              lineBreakMode:UILineBreakModeWordWrap];
    return labelSize;
}
like image 163
Bob Spryn Avatar answered Nov 01 '22 06:11

Bob Spryn