How to multi-line UILabel iOS revisited

Leave a comment

I wrote an earlier post about multi-line labels but I wanted something more distilled. Here are some condensed notes on how to do multi-line lables with xibs.

1. Set label to multi-line.

2. Set your UILabel struts and springs.

3. Calculate label height.

+ (CGFloat)heightForLabelWithString:(NSString *)pString andWidth:(CGFloat)pWidth
{
    CGSize labelSize = [pString sizeWithFont:[UIFont fontWithName:@"HelveticaNeue" size:14.5f] constrainedToSize:CGSizeMake(pWidth, MAXFLOAT) lineBreakMode:UILineBreakModeWordWrap];
    return labelSize.height;   
}

4. Redraw label based on height.

CGFloat bodyHeight = [CustomCell heightForLabelWithString:text.body andWidth:290];
    self.bodyLabel.frame = CGRectMake(self.bodyLabel.frame.origin.x, self.bodyLabel.frame.origin.y, self.bodyLabel.frame.size.width, bodyHeight);
    
    self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, self.bodyLabel.frame.origin.y + self.bodyLabel.frame.size.height + 10); 

Bring it all together in code

CustomCell.h

#import <UIKit/UIKit.h>
#import "NSString.h"

@interface CustomCell : UITableViewCell

@property (strong, nonatomic) IBOutlet UILabel *bodyLabel;

+ (NSString *)reuseIdentifier;
- (void)populate:(NSString *)text;
+ (CGFloat)heightForText:(NSString *)text;
@end

CustomCell.m

#import "CustomCell.h"
#import "UITableViewCell+Utils.h"

#define kYOrigin 10

@implementation CustomCell
@synthesize bodyLabel = _bodyLabel;

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        // Initialization code
    }
    return self;
}

- (void)populate:(NSString *)text
{
    self.bodyLabel.text = text;

    CGFloat bodyHeight = [CustomCell heightForLabelWithString:text andWidth:290];
    self.bodyLabel.frame = CGRectMake(self.bodyLabel.frame.origin.x, self.bodyLabel.frame.origin.y, self.bodyLabel.frame.size.width, bodyHeight);
    
    self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, self.bodyLabel.frame.origin.y + self.bodyLabel.frame.size.height + 10); //10 for padding
}

+ (CGFloat)heightForText:(NSString *)text
{
    CGFloat bodyHeight = [self heightForLabelWithString:text andWidth:290];
    return kYOrigin + bodyHeight + 10;
}

+ (NSString *)reuseIdentifier 
{    
    return @"CustomCellIdentifier";
}

@end

MyViewController.m

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
        return [CustomCell heightForText:text];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier:[CustomCell reuseIdentifier]];
    if (cell == nil) {
        [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
        cell = _textSectionCell;
        _textSectionCell = nil;
    }        
    
    [cell populate:@”some really long multi-line body of text ...”];
    return cell; 
}

UITableViewCell+Utils.h (category)

#import "UITableViewCell+Utils.h"

@implementation UITableViewCell (Utils)

+ (CGFloat)heightForLabelWithString:(NSString *)pString 
{
    CGSize labelSize = [pString sizeWithFont:[UIFont fontWithName:@"HelveticaNeue" size:14.5f] constrainedToSize:CGSizeMake(kDefaultLabelWidth, MAXFLOAT) lineBreakMode:UILineBreakModeWordWrap];
    return labelSize.height;
}

+ (CGFloat)heightForLabelWithString:(NSString *)pString andWidth:(CGFloat)pWidth
{
    CGSize labelSize = [pString sizeWithFont:[UIFont fontWithName:@"HelveticaNeue" size:14.5f] constrainedToSize:CGSizeMake(pWidth, MAXFLOAT) lineBreakMode:UILineBreakModeWordWrap];
    return labelSize.height;   
}
@end

How to shrink a UILabel down to it’s actual size

Leave a comment

Say you have a label that’s three digits long, with a fixed with of something big (like 200pts) and you want to know it’s actual width.

Here’s how you do it:

ViewController.m

- (void)viewWillAppear:(BOOL)animated
{
    // get label size
    CGSize size = self.myLabel.frame.size;
    CGFloat width = size.width;
    
    NSLog(@"\n\n=======================");
    NSLog(@"original width=%f", width);
    
    // shrink it down to it's actual size
    CGSize textSize = [self.myLabel.text sizeWithFont:[self.myLabel font]];
    
    CGFloat strikeWidth = textSize.width;
    NSLog(@"new width=%f", strikeWidth);
    
}

Gives the output:

original width=200.000000
new width=35.000000

Links that help
http://stackoverflow.com/questions/1340667/pixel-width-of-the-text-in-a-uilabel

How to multiline label ios objective-c

7 Comments

Say you’ve got a view with a label, and you need a multiline label to dynamically render itself based on how much text you have to display.

(see here for how to create a custom UITableViewCell xib with a UITableView)

Here’s how you do it.

Multiline label

1. Set the line count on the label to 0.

2. Set your structs and springs.

Struts and springs are very important. Get these wrong and you could spend hours trying to figure out why things aren’t working (like I have).

For this point in the example we want the struts and springs to look like this:

3. Make the label Wordwrap.

Can set in the Interface Builder (IB)

Or code:

cell.myLabel.lineBreakMode = UILineBreakModeWordWrap;

4. Increase the size of the row.

Before our labels will completely show we need more row space. Hack it like this for now.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 150;
}

Run it and voila – multiline label.

Now that’s all fine and dandy. But there are few things do be desired.

For one it would be nice to dynamically size the row (instead of hacking).

Secondly it would be nice to keep the label pinned at the top to prevent it from sliding down.

Dynamically resizing the table cell row height.

Here we basically calculate the height of our lable, and then return that (along with some padding in our UITableView delegate row callback.

1. Calculate the height of the label text.

ViewController.m

- (CGFloat)heightForText:(NSString *)bodyText
{
    UIFont *cellFont = [UIFont systemFontOfSize:17];
    CGSize constraintSize = CGSizeMake(300, MAXFLOAT);
    CGSize labelSize = [bodyText sizeWithFont:cellFont constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
    CGFloat height = labelSize.height + 50;
    NSLog(@"height=%f", height);
    return height;    
}

17 is the font size of my label.
300 is the label width of my cell.
50 is just an offset I applied for padding.

Then we simple call that from our delegate heightForRowAtIndexPath method:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//    return 150;
    NSString *labelText = [self.data objectAtIndex:indexPath.row];
    return [self heightForText:labelText];
}

Pinning label to the top

Now this is nice, but as your label grows, it can shift and get offset from the stop.

To pin the label in place and prevent it from sliding around we need to redraw the label frame, and specify the x y coordinates of where we’d like it to sit.

ViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    …

    // pin label to top
    CGFloat rowHeight = [self heightForText:labelText];
    cell.myLabel.frame = CGRectMake(0, 0, 300, rowHeight);
     
    return cell;     
}

Now if we just did this, our labels would look as follows:

Not what we want. The struts and springs are overriding our redrawing of the UILabel. To fix that change your struts and springs to the following:

When you do that, you get this:

Now you’ll notice the y coordinate is off a bit here. I told it to go to the origin (0,0) but it is ignoring me.

That’s because that offset of 50 is currently making the height bigger than it needs to be. If you tweak it down to 10 or so

- (CGFloat)heightForText:(NSString *)bodyText
{

   CGFloat height = labelSize.height + 10;
 }

things should look more like this:

So there you have it. Don’t get discouraged if you find placing and spacing labels tricky. It is!

If things aren’t looking right just try tweaking stuff. And be sure to check your struts and springs.

Happy labeling!

Source code in it’s entirety:


#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController
@synthesize customCell = _customCell;
@synthesize data = _data;

- (NSArray *)data
{
    if (_data == nil) {
        
        NSString *str1 = @"Single line";
        NSString *str2 = @"Double line 0123456789 0123456789 0123456789";
        NSString *str3 = @"Triple line 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789";
        
        _data = [NSArray arrayWithObjects:str1, str2, str3, nil];
    }
    return _data;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.data.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    
    CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier:[CustomCell reuseIdentifier]];
    if (cell == nil) {
        [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
        cell = _customCell;
        _customCell = nil;
    }        
    
    NSString *labelText = [self.data objectAtIndex:indexPath.row];
    cell.myLabel.text = labelText;
    cell.myLabel.lineBreakMode = UILineBreakModeWordWrap;
    
    // pin label to top
    CGFloat rowHeight = [self heightForText:labelText];
    cell.myLabel.frame = CGRectMake(0, 0, 300, rowHeight);
     
    return cell;     
}

- (CGFloat)heightForText:(NSString *)bodyText
{
    UIFont *cellFont = [UIFont systemFontOfSize:17];
    CGSize constraintSize = CGSizeMake(300, MAXFLOAT);
    CGSize labelSize = [bodyText sizeWithFont:cellFont constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
    CGFloat height = labelSize.height + 10;
    return height;    
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//    return 150;
    NSString *labelText = [self.data objectAtIndex:indexPath.row];
    return [self heightForText:labelText];
}

@end

Update

So all that works, but colleague Ryan Harrison showed me a much better way to calculate the height and size of the label and cell:

CustomUITableViewCell

#define BodyLabelYOrigin 30
#define BodyLabelWidth 200

@implementation NotificationCell

@synthesize titleLabel = _titleLabel;
@synthesize bodyLabel = _bodyLabel;
@synthesize iconImageView = _iconImageView;
@synthesize readTransparentView = _readTransparentView;
@synthesize notification = _notification;


+ (NSString *)reuseIdentifier
{
    return @"NotificationCellIdentifier";
}

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        // Initialization code
    }
    return self;
}

- (void)populate
{
    self.titleLabel.text = [LockerUtils formatttedTitleForNotification:self.notification];
    self.bodyLabel.text = self.notification.body;
    [self.iconImageView setImageWithURL:[NSURL URLWithString:self.notification.imageURL]];
    
    //Set height of body and cell
    CGFloat bodyHeight = [NotificationCell bodyLabelHeightForBodyString:self.notification.body];
    self.bodyLabel.frame = CGRectMake(self.bodyLabel.frame.origin.x, self.bodyLabel.frame.origin.y, self.bodyLabel.frame.size.width, bodyHeight);
    
    self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, self.bodyLabel.frame.origin.y + self.bodyLabel.frame.size.height + 10); //10 for padding
}

+ (CGFloat)bodyLabelHeightForBodyString:(NSString *)pBodyString{
    CGSize bodySize = [pBodyString sizeWithFont:[UIFont fontWithName:@"HelveticaNeue" size:14.5f] constrainedToSize:CGSizeMake(BodyLabelWidth, MAXFLOAT) lineBreakMode:UILineBreakModeWordWrap];
    return bodySize.height;
}

+ (CGFloat)calculatedCellHeightForBodyString:(NSString *)pBodyString{
    //Get body height
    CGFloat bodyHeight = [self bodyLabelHeightForBodyString:pBodyString];
    return BodyLabelYOrigin + bodyHeight + 10;
}
 
@end