41

I have a uilabel setup in a view. It doesn't have a width constraint, but its width is instead determined by a leading constraint to the thumbnail image, and a trailing constraint to the edge of the view.

enter image description here

The label is set to have 0 lines, and to word wrap. My understanding is that this should cause the frame of the uilabel to grow, and indeed it does sometimes. (Previous to auto layout, I would calculate and update the frame of the label in code).

So the result is, it works correctly in some instance and not others. See most cells working correctly there, but the last cell appears to be too big. In fact it's the right size. The title "Fair Oaks Smog Check Test" actually ends with "Only". So my calcuation for the cell size is right, it should be that size. However the label doesn't wrap the text for whatever reason. It's frame width does not extend off to the right, so that's not the issue.

enter image description here

So what is going on here? It's 100% consistent, always on that cell and not the ones above it, which makes me think it's related to the size of the text, and UILabel isn't re-laying out the text once this view is added to the cell (which makes it actually smaller width wise).

Any thoughts?

Some additional information

The height of the cells is calculated from one sample cell I create and store in a static variable:

- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (self.items.count == 0) {
        return 60;
    }
    static TCAnswerBuilderCell *cell = nil;
    static dispatch_once_t pred;

    dispatch_once(&pred,
                  ^{
                      // get a sample cellonce
                      cell = [tableView dequeueReusableCellWithIdentifier:TC_ANSWER_BUILDER_CELL];
                  });
    [cell configureCellWithThirdPartyObject:self.items[indexPath.row]];
    return [cell heightForCellWithTableWidth:self.tableView.frame.size.width];
}

I configure the cell with my data object on the fly, and then call a method I have on it which calculates the height of the cell with a given table width (can't always rely on the cell frame being correct initially).

This in turn calls a height method on my view, since it is really where the label lives:

- (CGFloat)heightForCellWithTableWidth:(CGFloat)tableWidth {
    // subtract 38 from the constraint above
    return [self.thirdPartyAnswerView heightForViewWithViewWidth:tableWidth - 38];
}

This method determines the height by figuring out the correct width of the label, and then doing a calculation:

- (CGFloat)heightForViewWithViewWidth:(CGFloat)viewWidth {
    CGFloat widthForCalc = viewWidth - self.imageFrameLeadingSpaceConstraint.constant - self.thumbnailFrameWidthConstraint.constant - self.titleLabelLeadingSpaceConstraint.constant;
    CGSize size = [self.titleLabel.text sizeWithFont:self.titleLabel.font constrainedToSize:CGSizeMake(widthForCalc, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
    CGFloat returnHeight = self.frame.size.height - self.titleLabel.frame.size.height + size.height;
    CGFloat height = returnHeight < self.frame.size.height ? self.frame.size.height : returnHeight;
    return height;
}

This works 100% correctly.

The cells are created obviously in cellForRowAtIndexPath and immediately configured:

if (self.items.count > 0) {
    TCAnswerBuilderCell *cell = [tableView dequeueReusableCellWithIdentifier:TC_ANSWER_BUILDER_CELL forIndexPath:indexPath];
    [cell configureCellWithThirdPartyObject:self.items[indexPath.row]];
    return cell;
}

In configuration of the cell, my view is loaded from a nib (it's re-used elsewhere, which is why it's not directly in the cell). The cell adds it as follows:

- (void) configureCellWithThirdPartyObject:(TCThirdPartyObject *)object {
    self.detailDisclosureImageView.hidden = NO;
    if (!self.thirdPartyAnswerView) {
        self.thirdPartyAnswerView = [TCThirdPartyAPIHelper thirdPartyAnswerViewForThirdPartyAPIServiceType:object.thirdPartyAPIType];
        self.thirdPartyAnswerView.translatesAutoresizingMaskIntoConstraints = NO;
        [self.contentView addSubview:self.thirdPartyAnswerView];
        [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[_thirdPartyAnswerView]-38-|" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_thirdPartyAnswerView)]];
    }
    [self.thirdPartyAnswerView configureViewForThirdPartyObject:object forViewStyle:TCThirdPartyAnswerViewStyleSearchCell];
}

Finally my view configuration looks like this:

- (void) configureViewForThirdPartyObject:(TCTPOPlace *)object forViewStyle:(TCThirdPartyAnswerViewStyle) style {
    self.titleLabel.text = object.name;
    self.addressLabel.text = [NSString stringWithFormat:@"%@, %@, %@", object.address, object.city, object.state];
    self.ratingsLabel.text = [NSString stringWithFormat:@"%d Ratings", object.reviewCount];
    NSString *ratingImageName = [NSString stringWithFormat:@"yelp_star_rating_%.1f.png", object.rating];
    UIImage *ratingsImage = [UIImage imageNamed:ratingImageName];
    if (ratingsImage) {
        self.ratingImageView.image = ratingsImage;
    }
    if (object.imageUrl) {
        [self.thumbnailImageView setImageWithURL:[NSURL URLWithString:object.imageUrl] completed:nil];
    }
}

A sort of solution, but I don't understand why

  1. My subview was designed at 320 width, but has no constraints of its own for width
  2. The subview was added to the cell, but with horizontal constraints that look like this:
    • @"|[_thirdPartyAnswerView]-38-|"
  3. The view was configured immediately after being added to the cell, meaning the text for the titleLabel was set right then.
  4. For whatever reason, the text was laid out as if the view had the full 320 instead of 282.
  5. The label was never updated, even though the frame of the subview was updated to 282, and there were constraints on the label that would keep it sized correctly.
  6. Changing the size of the view in the xib to be 282 fixed the issue, because the label has the right size to begin with.

I'm still not understanding why the label doesn't re-lay out after the size of the parent view is updated when it has both leading and trailing constraints.

SOLVED

See Matt's answer below: https://stackoverflow.com/a/15514707/287403

In case you don't read the comment, the primary problem was that I was unknowingly setting preferredMaxLayoutWidth via IB when designing a view at a bigger width than it would be shown (in some cases). preferredMaxLayoutWidth is what is used to determine where the text wraps. So even though my view and titleLabel correctly resized, the preferredMaxLayoutWidth was still at the old value, and causing wrapping at unexpected points. Setting the titleLabel instead to it's automatic size (⌘= in IB), and updating the preferredMaxLayoutWidth dynamically in layoutSubviews before calling super was the key. Thanks Matt!

Community
  • 1
  • 1
Bob Spryn
  • 17,742
  • 12
  • 68
  • 91
  • Have you tried to catch this cell during the render to see all of the properties? As a follow up, does this ever happen to cells that are rendered on the initial screen? I had a similar issue with a listview with varying height cells. With autosizing on, the cell would sometimes render the height incorrectly. – munch1324 Mar 20 '13 at 01:55
  • Any connection to reusing of the cell? e.g. cell scrolls out of site and back in - effect exactly as before? Cell is drawn at the initially visible past of that tableview, effect still the same? ... – Till Mar 20 '13 at 02:04
  • No connection to re-use. Same result if drawn right away or if scrolled in later. – Bob Spryn Mar 20 '13 at 02:12
  • you saved my life dude! preferredMaxLayoutWidth was killing it – benjamin.ludwig Jun 08 '16 at 14:39

3 Answers3

43

I'm someone who has written an app that uses autolayout of five labels in a cell in a table whose cells have different heights, where the labels resize themselves according to what's in them, and it does work. I'm going to suggest, therefore, that the reason you're having trouble might be that your constraints are under-determining the layout - that is, that you've got ambiguous layout for the elements of the cell. I can't test that hypothesis because I can't see your constraints. But you can easily check (I think) by using po [[UIWindow keyWindow] _autolayoutTrace] when paused in the debugger.

Also I have one other suggestion (sorry to just throw stuff at you): make sure you're setting the label's preferredMaxLayoutWidth. This is crucial because it's the width at which the label will stop growing horizontally and start growing vertically.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Cool. Thanks Matt! I'll try out the autolayout trace. So would you recommend leaving the label at it's natural size (⌘= in IB) and then setting the preferredMaxLayoutWidth? Also, at what point do I need to set that? When I'm configuring the view just after it was added to the cell, or some other point like layoutSubviews? – Bob Spryn Mar 20 '13 at 04:14
  • Btw, you are right I have AMBIGUOUS LAYOUT on the subview and it's contents. http://screencast.com/t/xOPyF3JleCm Now, how to determine what's ambiguous about it. – Bob Spryn Mar 20 '13 at 04:27
  • 1
    You are good! You are very good! ;) I set the preferredMaxLayoutWidth on the label in layoutSubviews (before calling super). Not sure if this is a good place to do it, but my max width for my label depends on the size of the parent view, so I need that to be evaluated before I can set it. Is that the right place to be setting that if it's dynamic? – Bob Spryn Mar 20 '13 at 04:41
  • 1
    FYI to anyone else who runs into this, setting the `preferredMaxLayoutWidth` was the real solution. Otherwise by setting the width in IB and using a constraint, the max width of the label and subsequent wrapping was being evaluated before the parent view size was updated. Then text was just missing (which was weird). I ended up setting the `preferredMaxLayoutWidth` in `layoutSubviews` (before calling `super`) because the parent frame was known at that point, and my `preferredMaxLayoutWidth` was dynamic. I did have ambiguous layout, but that was not the cause of the problem. – Bob Spryn Mar 20 '13 at 05:25
  • 1
    One other thing to note. It may not have been premature evaluation of the label width that caused my issue, but more the fact that if you set a width other than the automatic size (⌘= in IB) you are in fact setting `preferredMaxLayoutWidth` without knowing it. Since my view was larger in IB, my `preferredMaxLayoutWidth` was also larger, and that's what is used to evaluate where the wrapping takes place. In fact I think this is almost definitely what was happening. So use automatic size (⌘= in IB) and `preferredMaxLayoutWidth`. – Bob Spryn Mar 20 '13 at 05:31
  • "I ended up setting the preferredMaxLayoutWidth in layoutSubviews (before calling super)", yes, although I think you should probably do it *after* calling super. That is the solution proposed in my book, at the end of this section: http://www.apeth.com/iOSBook/ch23.html#_uilabel – matt Mar 20 '13 at 16:58
  • Hmm.. When I call it after calling super the app crashes: `*** Assertion failure in -[TCThirdPartyAnswerViewPlace layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2380.17/UIView.m:5776 ` – Bob Spryn Mar 20 '13 at 17:14
  • Well I guess you'd better not do that then! :) – matt Mar 20 '13 at 17:15
  • Haha. Any idea why that would be the case? Btw just ordered your book. Looks solid! Thanks for the effort! – Bob Spryn Mar 20 '13 at 17:23
  • Well, what does the assertion say? Maybe that will give us a clue. The reason I thought calling after super would be better is that until we've done layout how do we know what the width will be? But perhaps I'm wrong about that. – matt Mar 20 '13 at 17:40
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/26568/discussion-between-bob-spryn-and-matt) – Bob Spryn Mar 20 '13 at 18:21
  • @matt Can you help solve this problem? I have an exact same problem like this person in the link http://stackoverflow.com/questions/32435425/changing-uitableviewcells-width-constraints-constant-value-for-each-instance – kerry Sep 07 '15 at 11:18
  • @kerry No idea what you're talking about. If you have a question, ask it as a question! Feel free to blip me with the URL and I'll look at it. – matt Sep 07 '15 at 17:03
13

I had the same problem and solved it using a suggestion from this answer. In a subclass of UILabel I placed this code:

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.preferredMaxLayoutWidth = self.bounds.size.width;
}

I don't understand why this is not the default behavior of UILabel, or at least why you cannot just enable this behavior via a flag.

I am a little concerned that preferredMaxLayoutWidth is being set in the middle of the layout process, but I see no easy way around that.

Community
  • 1
  • 1
phatmann
  • 18,161
  • 7
  • 61
  • 51
0

Also, check that you are passing integral numbers to your layout constraints in code.

For me it happened that after some calculations (e.g. convertPoint:toView:), I was passing in something like 23.99999997, and eventually this lead to a 2-line label displaying as a one-liner (although its frame seemed to be calculated correctly). In my case CGRectIntegral did the trick!

Rounding errors could kill ya :)

Sergiu Todirascu
  • 1,367
  • 15
  • 23