61

Following question is sort-of continuation of this one:

iOS: Multi-line UILabel in Auto Layout

The main idea is that every view is supposed to state it's "preferred" (intrinsic) size so that AutoLayout can know how to display it properly. UILabel is just an example of a situation where a view cannot by itself know what size it needs for display. It depends on what width is provided.

As mwhuss pointed out, setPreferredMaxLayoutWidth did the trick of making the label span across multiple lines. But that is not the main question here. The question is where and when do I get this width value that I send as an argument to setPreferredMaxLayoutWidth.

I managed to make something that looks legit, so correct me if I am wrong in any way and tell me please if you know a better way.

In the UIView's

-(CGSize) intrinsicContentSize

I setPreferredMaxLayoutWidth for my UILabels according to self.frame.width.

UIViewController's

-(void) viewDidLayoutSubviews

is the first callback method I know where subviews of the main view are appointed with their exact frames that they inhabit on the screen. From inside that method I, then, operate on my subviews, invalidating their intrinsic sizes so that UILabels are broken into multiple lines based on the width that was appointed to them.

Community
  • 1
  • 1
AndroC
  • 4,758
  • 2
  • 46
  • 69

9 Answers9

64

There's an answer this question on objc.io in the "Intrinsic Content Size of Multi-Line Text" section of Advanced Auto Layout Toolbox. Here's the relevant info:

The intrinsic content size of UILabel and NSTextField is ambiguous for multi-line text. The height of the text depends on the width of the lines, which is yet to be determined when solving the constraints. In order to solve this problem, both classes have a new property called preferredMaxLayoutWidth, which specifies the maximum line width for calculating the intrinsic content size.

Since we usually don’t know this value in advance, we need to take a two-step approach to get this right. First we let Auto Layout do its work, and then we use the resulting frame in the layout pass to update the preferred maximum width and trigger layout again.

The code they give for use inside a view controller:

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

Take a look at their post, there's more information about why it's necessary to do the layout twice.

Community
  • 1
  • 1
nevan king
  • 112,709
  • 45
  • 203
  • 241
  • 5
    @AlexanderLongbeach I'm not sure what you mean. The label is always inside a subview. It's impossible to have a `UILabel` that's not inside a subview. – nevan king Apr 24 '14 at 10:08
  • I mean. All the examples here count on that you know the size of the label's superview, but if that superview also has been added using auto layout without hardcoding a width.. what then? This might be a dumb question, but Im new to auto layout.. – AlexanderN Apr 24 '14 at 11:16
  • @AlexanderLongbeach The example above actually doesn't rely on having a hardcoded width in the superview. In fact, you could almost always use label.superview.frame.size.width, without even needing a direct pointer to the superview. – RonLugge Jun 06 '14 at 23:14
  • Seeing claims that iOS 7 handles this for you: http://danielgarbien.com/ios/2013/11/16/preferredMaxLayoutWidth.html – Bob Spryn Sep 16 '14 at 18:57
  • 1
    Update. That article is incorrect. Still required until iOS 8. – Bob Spryn Sep 16 '14 at 19:38
  • Either the superviews width is defined by the size of the label (ie inverse intrinsic content size inheritance), or the superview will know it's correct frame after [super layoutSubviews] has been called for the first time – Sam Clewlow Nov 18 '14 at 11:40
  • What about if the label is inside a UITableViewCell and the view controller can change? – Chris Harrison Jan 06 '15 at 05:23
  • In my case this wasn't working on iOS 8, because `myLabel.frame.size.width` was zero. On iOS 7 `viewDidLayoutSubviews` was called three times (first with zero and than with the correct frame value) - on iOS 8 only one time. Now I used `self.view.bounds.size.width` and now it is working fine. – testing Feb 05 '15 at 10:09
  • Subclassing `UILabel` as described in [objc.io](http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html) does also the job for me. Now it works for iOS 7 and iOS 8. – testing Feb 05 '15 at 10:15
  • I did this in a UILabel subclass and it works: `- (void)updateConstraints { self.preferredMaxLayoutWidth = self.frame.size.width; [super updateConstraints]; }` – n8tr Jan 29 '16 at 13:49
44

It seems annoying that a UILabel doesn't default to its width for the preferred max layout width, if you've got constraints that are unambiguously defining that width for you.

In nearly every single case I've used labels under Autolayout, the preferred max layout width has been the actual width of the label, once the rest of my layout has been performed.

So, to make this happen automatically, I have used a UILabel subclass, which overrides setBounds:. Here, call the super implementation, then, if it isn't the case already, set the preferred max layout width to be the bounds size width.

The emphasis is important - setting preferred max layout causes another layout pass to be performed, so you can end up with an infinite loop.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • Thanks, man. Totally forgot about the "setBounds". This is actually what I was looking for as a point where a subview is appointed its size. I was trying setFrame and it's not being called. But I'm not really knowledgeable about these things, so tnx once again. – AndroC Jul 08 '13 at 08:59
  • 2
    Under iOS8 if the UILabel is in a tableview cell, setBounds doesn't seem to be called. Is there any way to calculate a good initial guess at `preferredMaxLayoutWidth`? – Rog Sep 16 '14 at 13:04
  • @RogerNolan haven't had the time to look at it on iOS8 yet(!) Will update if and when I get the chance, or you might want to ask a new question – jrturton Sep 16 '14 at 15:15
  • I just posted a solution to this question that works on iOS 7 and iOS 8 – Nick Snyder Oct 03 '14 at 15:22
  • @RogerNolan It worked for me. Did you set the class of the UILabel in the xib file to the custom class ? I've a UILabel inside a UITableViewCell's contentView. I subclassed the UILabel and set the custom class in the xib file. As soon as I set the custom class in the xib file, it started calling the setBounds: method. – newDeveloper Nov 13 '14 at 01:37
  • 2
    @RogerNolan Any solution for UILabel inside tableview for iOS8? – Pushparaj Dec 01 '14 at 13:34
  • @zala In iOS8 the preferred max layout width of "automatic" for the label should render this unnecessary – jrturton Dec 01 '14 at 13:50
  • 1
    I'am also supporting iOS 7 so I have set max layout width as "Explicit". Now I want change the max layout width for iOS 8 programatically. – Pushparaj Dec 01 '14 at 14:01
33

Update

My original answer appears to be helpful so I have left it untouched below, however, in my own projects I have found a more reliable solution that works around bugs in iOS 7 and iOS 8. https://github.com/nicksnyder/ios-cell-layout

Original answer

This is a complete solution that works for me on iOS 7 and iOS 8

Objective C

@implementation AutoLabel

- (void)setBounds:(CGRect)bounds {
  if (bounds.size.width != self.bounds.size.width) {
    [self setNeedsUpdateConstraints];
  }
  [super setBounds:bounds];
}

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

@end

Swift

import Foundation

class EPKAutoLabel: 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()
    }
}
Grzegorz Krukowski
  • 18,081
  • 5
  • 50
  • 71
Nick Snyder
  • 2,966
  • 1
  • 21
  • 23
  • This worked for me. I adapt this logic because i just need to remove the Xcode warning for label when label.numberOfLines = 0; – Asif Bilal Dec 03 '14 at 11:52
  • This worked for me in iOS 8 from within a UITableViewCell. – nsantorello Jul 28 '15 at 03:40
  • This is also the only suggestion that worked for me inside UITableViewCell. Thanks a lot – yura Aug 24 '15 at 23:20
  • This didn't work for me on iOS 7! I added this: - (void)layoutSubviews { [super layoutSubviews]; self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds); [super layoutSubviews]; } – Rudolf J Oct 08 '15 at 08:00
  • Works perfectly in both collections and tables, make lists and collections fluid by not adding massive label bounds recomputations like other solutions. – Softlion Jul 30 '16 at 08:39
  • 1
    I don't think that swift version is ever going to fire the `self.setNeedsUpdateConstraints()` line. It should be checking if `bounds.size.width != oldValue.size.width`. The way it is currently written is comparing the value with itself. – aranasaurus Dec 06 '16 at 22:13
10

We had a situation where an auto-layouted UILabel inside a UIScrollView laid out fine in portrait, but when rotated to landscape the height of the UILabel wasn't recalculated.

We found that the answer from @jrturton fixed this, presumably because now the preferredMaxLayoutWidth is correctly set.

Here's the code we used. Just set the Custom class from Interface builder to be CVFixedWidthMultiLineLabel.

CVFixedWidthMultiLineLabel.h

@interface CVFixedWidthMultiLineLabel : UILabel

@end 

CVFixedWidthMultiLineLabel.m

@implementation CVFixedWidthMultiLineLabel

// Fix for layout failure for multi-line text from
// http://stackoverflow.com/questions/17491376/ios-autolayout-multi-line-uilabel
- (void) setBounds:(CGRect)bounds {
    [super setBounds:bounds];

    if (bounds.size.width != self.preferredMaxLayoutWidth) {
        self.preferredMaxLayoutWidth = self.bounds.size.width;
    }
}

@end
Ben Clayton
  • 80,996
  • 26
  • 120
  • 129
  • 1
    Having the same problem but this isn't working for me unfortunately. I'm working with labels inside of a tableView – Eichhörnchen Jul 07 '14 at 16:39
  • 1
    This solution works pretty well with one caveat. I was using label in tableView and in heightForRowAtIndexpath I'm ordinary do 'systemLayoutSizeFittingSize' in order to get correct cell dimensions. However before calling this method you have to update cell width as 'dequeueReusableCellWithIdentifier' will return it in size defined in xib file(mine was 320, actual size 227). So update cell frame = tableView.contentSize.width before calling layoutIfNeeded combined with this solution happens to work on all devices(4s, 5, 6, 6+). – hris.to Apr 20 '15 at 12:48
  • The only this approach worked for me (within a UICollectionViewCell) was to set the initial frame when calculating the size. – D6mi Jul 17 '17 at 11:07
2

Using boundingRectWithSize

I resolved my struggle with two multi-line labels in a legacy UITableViewCell that was using "\n" as a line-break by measuring the desired width like this:

- (CGFloat)preferredMaxLayoutWidthForLabel:(UILabel *)label
{
    CGFloat preferredMaxLayoutWidth = 0.0f;
    NSString *text = label.text;
    UIFont *font = label.font;
    if (font != nil) {
        NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
        mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;

        NSDictionary *attributes = @{NSFontAttributeName: font,
                                     NSParagraphStyleAttributeName: [mutableParagraphStyle copy]};
        CGRect boundingRect = [text boundingRectWithSize:CGSizeZero options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
        preferredMaxLayoutWidth = ceilf(boundingRect.size.width);

        NSLog(@"Preferred max layout width for %@ is %0.0f", text, preferredMaxLayoutWidth);
    }


    return preferredMaxLayoutWidth;
}

Then calling the method was then as simple as:

CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
    textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];
Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126
2

As I'm not allowed to add a comment, I'm obliged to add it as an answer. The version of jrturton only worked for me if I call layoutIfNeeded in updateViewConstraints before getting the preferredMaxLayoutWidth of the label in question. Without the call to layoutIfNeeded the preferredMaxLayoutWidth was always 0 in updateViewConstraints. And yet, it had always the desired value when checked in setBounds:. I didn't manage to get to know WHEN the correct preferredMaxLayoutWidth was set. I override setPreferredMaxLayoutWidth: on the UILabel subclass, but it never got called.

Summarized, I:

  • ...sublcassed UILabel
  • ...and override setBounds: to, if not already set, set preferredMaxLayoutWidth to CGRectGetWidth(bounds)
  • ...call [super updateViewConstraints] before the following
  • ...call layoutIfNeeded before getting preferredMaxLayoutWidth to be used in label's size calculation

EDIT: This workaround only seems to work, or be needed, sometimes. I just had an issue (iOS 7/8) where the label's height were not correctly calculated, as preferredMaxLayoutWidth returned 0 after the layout process had been executed once. So after some trial and error (and having found this Blog entry) I switched to using UILabel again and just set top, bottom, left and right auto layout constraints. And for whatever reason the label's height was set correctly after updating the text.

dergab
  • 965
  • 7
  • 16
1

As suggested by another answer I tried to override viewDidLayoutSubviews:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
    [self.view layoutIfNeeded];
}

This worked, but it was visible on the UI and caused a "visible flicker" i.e. first the label was rendered with the height of two lines, then it was re-rendered with the height of only one line.

This was not acceptable for me.

I found then a better solution by overriding updateViewConstraints:

-(void)updateViewConstraints {
    [super updateViewConstraints];

    // Multiline-Labels and Autolayout do not work well together:
    // In landscape mode the width is still "portrait" when the label determines the count of lines
    // Therefore the preferredMaxLayoutWidth must be set
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
}

This was the better solution for me, because it did not cause the "visual flickering".

jbandi
  • 17,499
  • 9
  • 69
  • 81
0

A clean solution is to set rowcount = 0 and to use a property for the heightconstraint of your label. Then after the content is set call

CGSize sizeThatFitsLabel = [_subtitleLabel sizeThatFits:CGSizeMake(_subtitleLabel.frame.size.width, MAXFLOAT)];
_subtitleLabelHeightConstraint.constant = ceilf(sizeThatFitsLabel.height);

-(void) updateViewConstraints has a problem since iOS 7.1.

Matt
  • 9,068
  • 12
  • 64
  • 84
netshark1000
  • 7,245
  • 9
  • 59
  • 116
0

In iOS 8, you can fix multi-line label layout problems in a cell by calling cell.layoutIfNeeded() after dequeuing and configuring the cell. The call is harmless in iOS 9.

See Nick Snyder's answer. This solution was taken from his code at https://github.com/nicksnyder/ios-cell-layout/blob/master/CellLayout/TableViewController.swift.

phatmann
  • 18,161
  • 7
  • 61
  • 51