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
Advertisements