22

I have a UITableView with cells that have a fixed height of 100 points. The cells are created in a xib file that uses 3 constraints to pin a UILabel to the left, right and top edges of the cell's contentView. The label's vertical hugging priority is set to 1000 because I want the cell's height to be as small as possible.

When the width of the cell in the xib file is set to 320 points, the same as the tableView's width on the iPhone, autolayout works as expected. However, when I set the width of the cell to less than 320 points, I get unexpected results. (I want to use the same cell in tableViews that have different widths, e.g. in a universal app)

For example: when I set the width to 224 points and give the label a text that takes up 2 lines at that width, the label's height will increase to fit the 2 lines, but when the cell is then resized to 320 points to fit in a tableView of that width, the text only takes up 1 line, but the height of the label remains at 2 lines.

I have put a sample project on GitHub to demonstrate the problem: https://github.com/bluecrowbar/CellLayout

Is there a way to make the UILabel always resize to hug its text content?

Steven Vandeweghe
  • 2,170
  • 2
  • 20
  • 24

6 Answers6

72

Adding this in the cell subclass works:

- (void)layoutSubviews
{
    [super layoutSubviews];
    [self.contentView layoutIfNeeded];
    self.myLabel.preferredMaxLayoutWidth = self.myLabel.frame.size.width;
}

I found this on http://useyourloaf.com/blog/2014/02/14/table-view-cells-with-varying-row-heights.html.

Update 1: This answer was for iOS 7. I find auto layout in table view cells to be very unreliable since iOS 8, even for very simple layouts. After lots of experimentation, I (mostly) went back to doing manual layout and manual calculation of the cell's height.

Update 2: I've run some tests on iOS 9 and it seems that UITableViewAutomaticDimension finally works as advertised. Yay!

Steven Vandeweghe
  • 2,170
  • 2
  • 20
  • 24
  • Good find. The statement `[self.contentView layoutIfNeeded]` is where I got stuck going down this path. I assumed `[super layoutSubviews]` called `layoutSubviews` but apparently it doesn't. That certainly explains why the label's frame wasn't immediately updated after `[super layoutSubviews]`. – Timothy Moose May 18 '14 at 18:31
  • Awesome. I was literally going to ditch auto layout after spending hours trying to figure this one out (Dynamic sized labels on a tableViewCell and the cell.contentView.bounds.size.height is either too small or too large -> happened on any screen greater than 320px wide). Thank you. – newDeveloper Nov 17 '14 at 08:05
  • I would say this is one the most hidden gems for autolayout cells with text (UILabel) with different height in UITableView. My labels were reporting different height, showing wrong height and frame, having wrong vertical layout on iOS 7. Thank you! – Alex Sorokoletov Nov 20 '14 at 07:09
  • 1
    it's a bug of UILabel? – Bimawa Nov 21 '14 at 06:20
  • @StevenVandeweghe, looks like it fix my width, but ruins my row height... Any idea with it? (Even if I set height constraints manualy). – Dima Deplov May 31 '15 at 00:00
  • 1
    My original answer was for iOS 7. I find auto layout in table view cells to be very unreliable since iOS 8, even for very simple layouts. After lots of experimentation, I (mostly) went back to doing manual layout and manual calculation of the cell's height. – Steven Vandeweghe May 31 '15 at 09:37
  • Thanks, @StevenVandeweghe. I agree with you, I prefer calculating cell's height manually too. – Thomás Pereira Aug 10 '15 at 14:49
  • Thank you for your answer ! – user3369214 Oct 09 '15 at 08:39
  • Can confirm that the `UITableViewAutomaticDimension` has been fixed in iOS 9. FINALLY! – Sakiboy Oct 27 '15 at 16:33
  • I had to invoke `layoutIfNeeded` after setting `prefferedMaxLayoutWidth` for it to work for me. – Maciej Stramski Jul 27 '16 at 13:09
  • 1
    not work for me, @Bimawa yes it seems UILabel issue because contentView width seems perfect, but label one not correct. – Amit Battan Mar 23 '17 at 06:23
17

Stupid bug! I've lost almost one day in this problem and finally I solved It with Steven Vandewghe's solution.

Swift version:

override func layoutSubviews() {
    super.layoutSubviews()
    self.contentView.layoutIfNeeded()
    self.myLabel.preferredMaxLayoutWidth = self.myLabel.frame.size.width
}
Thomás Pereira
  • 9,589
  • 2
  • 31
  • 34
  • 2
    Same here. I wasted couple of hours. But found above the working. Only question is why do we need self.contentView.layoutIfNeeded() to get the correct size? – NNikN Dec 30 '16 at 04:29
3

Since you're constraining the label's width, the intrinsicContentSize honors that width and adjusts the height. And this sets up a chicken and egg problem:

  1. The cell's Auto Layout result depends on the label's intrinsicContentSize
  2. The label's intrinsicContentSize depends on the label's width
  3. The label's width depends on the cell's Auto Layout result

So what happens is that the cell's layout is only calculated once in which (2) is based on the static width in the XIB file and this results in the wrong label height.

You can solve this by iterating. That is, repeat the Auto Layout calculation after the label's width has been set by the first calculation. Something like this in your custom cell will work:

- (void)drawRect:(CGRect)rect
{
    CGSize size = self.myLabel.bounds.size;
    // tell the label to size itself based on the current width
    [self.myLabel sizeToFit];
    if (!CGSizeEqualToSize(size, self.myLabel.bounds.size)) {
        [self setNeedsUpdateConstraints];
        [self updateConstraintsIfNeeded];
    }
    [super drawRect:rect];
}

original solution does not work reliably:

- (void)layoutSubviews
{
    [super layoutSubviews];
    // check for need to re-evaluate constraints on next run loop
    // cycle after the layout has been finalized
    dispatch_async(dispatch_get_main_queue(), ^{
        CGSize size = self.myLabel.bounds.size;
        // tell the label to size itself based on the current width
        [self.myLabel sizeToFit];
        if (!CGSizeEqualToSize(size, self.myLabel.bounds.size)) {
            [self setNeedsUpdateConstraints];
            [self updateConstraintsIfNeeded];
        }
    });
}
Timothy Moose
  • 9,895
  • 3
  • 33
  • 44
  • The problem with this approach is that you can see the re-layout. Try running the app in the iPad simulator to see what I mean. – Steven Vandeweghe May 18 '14 at 08:32
  • @StevenVandeweghe Hmm. It only does that in the iPad Retina simulator. The other simulators work. Might be a simulator bug. – Timothy Moose May 18 '14 at 09:13
  • Updated answer. Moving the code into `drawRect` avoids the `dispatch_async` and works reliably as far as I can tell. – Timothy Moose May 18 '14 at 09:31
  • 1
    I hadn't thought of `drawRect:`. This also works in `drawRect`: `[super drawRect:rect]; self.myLabel.preferredMaxLayoutWidth = self.myLabel.frame.size.width;`. Anyway, I've opened a DTS, just to be sure what Apple thinks is the best solution. – Steven Vandeweghe May 18 '14 at 09:46
  • @TimothyMoose Thanks. Good explanations. – Serge Maslyakov Sep 26 '16 at 14:11
1

I'm using XCode 10 with iOS 12 and I still get autolayout problems with cells not being given the correct height when the table is first presented. Timothy Moose's answer didn't fix the problem for me, but based on his explanation I came up with a solution which does work for me.

I subclass UITableViewController and override the viewDidLayoutSubviews message to check for width changes, and then force a table update if the width does change. This fixes the problem before the view is presented, which makes it look much nicer than my other efforts.

First add a property to your custom UITableViewController subclass to track the previous width:

@property (nonatomic) CGFloat previousWidth;

Then override viewDidLayoutSubviews to check for width changes:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    CGFloat width = self.view.frame.size.width;
    if (self.previousWidth != width) {
        self.previousWidth = width;
        [self.tableView beginUpdates];
        [self.tableView endUpdates];
    }
}

This fixed issues with my table cells sometimes being given the wrong height initially.

John Stephen
  • 7,625
  • 2
  • 31
  • 45
1

I know this is an old issue, but maybe this UILabel subclass can also help for some:

class AutoSizeLabel: UILabel {
    override var bounds: CGRect {
        didSet {
            if bounds.size.width != oldValue.size.width {
                self.setNeedsUpdateConstraints()
            }
        }
    }

    override func updateConstraints() {
        if self.preferredMaxLayoutWidth != self.bounds.size.width {
            self.preferredMaxLayoutWidth = self.bounds.size.width
        }
        super.updateConstraints()
    }
}

Note: works also for cases when your UILabel won't size itself correctly when inside of a StackView

πter
  • 1,899
  • 2
  • 9
  • 23
0

I usually add these two lines to viewDidLoad()

    self.tableView.rowHeight = UITableViewAutomaticDimension
    self.tableView.estimatedRowHeight = 96

This will automatically resize the cell

Maria
  • 4,471
  • 1
  • 25
  • 26