ByteSlinger
ByteSlinger

Reputation: 1617

How to programmatically sizeToFit width AND height on UILabel?

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?

Upvotes: 1

Views: 2239

Answers (2)

DonMag
DonMag

Reputation: 77690

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.

Upvotes: 0

ByteSlinger
ByteSlinger

Reputation: 1617

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];
}

Upvotes: 0

Related Questions