0
votes

I'm programmatically creating multi-line UILabels ([label setNumberOfLines:0];).

The built-in sizeToFit method of UILabel works great for 1 line UILabels, but for multi-line text, it sets the height properly, but the width is set too small, causing longer text lines to wrap.

I don't know the label width until after the user enters their text. I want to resize the labels to fit the width of the longest line of text. And per @DonMag's comment, I also want to restrict the label to not be wider than the screen.

I tried different lineBreakMode settings but there isn't a 'nowrap' option.

I've searched SO and there are many related solutions but none that solve the problem of sizeToFit for both width and height.

Is there a way to programmatically size a multi-line UILabel to fit BOTH the width AND the height of the text?

2
You shouldn't need sizeToFit with multi-line UILabels... The idea is that you set the width of the label, and let the wrapping setting handle the line breaks, and let auto-layout handle the height change.DonMag
Use auto layout to fix the leading and trailing distances (width). sizeToFit will then take care of the height.koen
@DonMag - I understand your point, but I DO need sizeToFit for my labels. I don't know the width of the label until the user enters the text. I want the UILabel to resize to the width of the longest line in the multi-line text. I'll update my question.ByteSlinger
@Koen - Can you explain how to do this programmatically? My labels are NOT created in the Storyboard.ByteSlinger
@ByteSlinger - that doesn't make sense... The "longest line" would be an infinite width, extending out past the edge of the view. Are you trying to say "I want the label to be at most 300-pts wide, and after word-wrapping I want the label background to appear as if the text fits perfectly"?DonMag

2 Answers

0
votes

You can do this with boundingRectWithSize...

Add your label to the view and give it a starting width constraint (doesn't really matter what value, as it will be changed).

Keep a reference to that width constraint (IBOutlet works fine if you're using IB).

Don't give it a height constraint.

When you set the text of the label, you can use this to change its width:

// get the font of the label
UIFont *theFont = _theLabel.font;

// get the text of the label
NSString *theString = _theLabel.text;

// calculate the bounding rect, limiting the width to the width of the view
CGRect r = [theString boundingRectWithSize:CGSizeMake(self.view.frame.size.width, CGFLOAT_MAX)
                                   options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                                attributes:@{NSFontAttributeName: theFont}
                                   context:nil];

// change the constant of the constraint to the calculated width
_theWidthConstraint.constant = ceil(r.size.width);

// NOTE: If you are *not* using auto-layout, 
// this same calculation can be used to explicitly set
// the .frame of the label.

Edit:

As per the OP's requirement, a complete, runnable example -- using code only, no storyboards / IB -- can be found here: https://github.com/DonMag/MultilineLabelFitWidth

Edit 2:

GitHub project updated... now includes examples for both manual frame setting and auto layout / constraints.

0
votes

With some more experimentation, I found something that does the trick that I have not seen in SO (yet...). In general it works like this:

  • Find the longest text line
  • Set numberOfLines to 1 (temporarily)
  • Set label text to longest text line
  • Call label.sizeToFit (sets label width for longest line)
  • Set numberOfLines to 0 (multi-line)
  • Set label text to full multi-line text
  • Call label.sizeToFit (sets label height for all lines)

Voila! Now your UILabel is sized to fit your multi-line text.

Here is an example (demo project on GitHub: UILabelSizeToFitDemo):

- (UILabel *)label = nil;

- (void)updateLabel:(NSString *)notes {
    // close to the "sticky" notes color
    UIColor *bananaColor = [ViewController colorWithHexString:@"#FFFC79"];

    if (_label == nil) {
        _label = [[UILabel alloc] init];
        _label.numberOfLines = 0;
        _label.textColor = UIColor.blackColor;
        [_label setBackgroundColor:[bananaColor colorWithAlphaComponent:0.9f]];
        _label.textAlignment = NSTextAlignmentLeft;

        [self.view addSubview:_label];
    }

    // make font size based on screen size
    CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
    CGFloat fontSize = MIN(screenWidth,screenHeight) / 12;
    [_label setFont:[UIFont systemFontOfSize:fontSize]];

    // split lines
    NSArray *lines = [notes componentsSeparatedByString:@"\n"];
    NSString *longestLine = lines[0];   // prime it with 1st line

    // fill a temp UILabel with each line to find the longest line
    for (int i = 0; i < lines.count; i++) {
        NSString *line = (NSString *)lines[i];

        if (longestLine == nil || line.length > longestLine.length) {
            longestLine = line;
        }
    }

    // force UILabel to fit the largest line
    [_label setNumberOfLines:1];
    [_label setText:longestLine];
    [_label sizeToFit];

    // make sure it doesn't go off the screen
    if (_label.frame.size.width > screenWidth) {
        CGRect frame = _label.frame;
        frame.size.width = screenWidth - 20;
        _label.frame = frame;
    }
    // now fill with the actual notes (this saves the previous width)
    [_label setNumberOfLines:0];
    [_label setText:notes];
    [_label sizeToFit];


    // center the label in my view
    CGPoint center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
    [_label setCenter:center];
}

UPDATE: Here is an alternate complete solution, using the boundinRectWithSize from the code snippet by @DonMag:

-(void)updateLabel:(NSString *)notes {
    // close to the "sticky" notes color
    UIColor *bananaColor = [ViewController colorWithHexString:@"#FFFC79"];

    if (_label == nil) {
        _label = [[UILabel alloc] init];
        _label.numberOfLines = 0;
        _label.textColor = UIColor.blackColor;
        _label.backgroundColor = [bananaColor colorWithAlphaComponent:0.9f];
        _label.textAlignment = NSTextAlignmentLeft;

        [self.view addSubview:_label];
    }

    // set new text
    _label.text = notes;

    // make font size based on screen size
    CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
    CGFloat fontSize = MIN(screenWidth,screenHeight) / 12;
    [_label setFont:[UIFont systemFontOfSize:fontSize]];

    // calculate the bounding rect, limiting the width to the width of the view
    CGRect frame = [notes boundingRectWithSize:CGSizeMake(self.view.frame.size.width, CGFLOAT_MAX)
                                       options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
                                    attributes:@{NSFontAttributeName: _label.font}
                                       context:nil];

    // set frame and then use sizeToFit
    [_label setFrame:frame];
    [_label sizeToFit];

    // center the label in my view
    CGPoint center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
    [_label setCenter:center];
}