23
Sep
2009
 

UITableViewCell Dynamic Height

by Matt Long

At first glance setting a height dynamically for table view cells seems a little daunting and the first most obvious answers that come to mind are not necessarily correct. In this post I will show you how to set your table view cell heights dynamically based upon the text content without subclassing UITableViewCell. You can subclass it, however, doing so does not make the code much cleaner as setting the height is done in your delegate for the table view itself rather than the cell anyhow. Read on to see what you need to know to make dynamic cell height sizing a breeze.

There are probably numerous reasons why you might want dynamic heights for your table view cells, but the one I’ve run into most is the need to resize because I am displaying lists of text objects with varying lengths. When the text is short, it might fit in the normal cell label, however, if the text gets longer, you will want to resize the cell so that you can display the complete content. I’ve distilled the process of resizing table cells to a few rules of thumb. Here they are:

  • Create, configure, and add a UILabel as a subview of the contentView in the cell.

  • Calculate the height in the UITableView delegate method, – (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
  • Calculate the frame for the UILabel in the UITableView delegate method, – (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

I am going to cover each of these rules in detail, but take a look at the output of the example project in the screenshot.

Dynamic Heights

Add a UILabel to the Cell

In simpler table view based applications, you can simply set the text of the table view cell’s text label like this:

[[cell textLabel] setText:@"Text for the current cell here."];

Doing so might make you think that you can manipulate the UILabel that the cell uses, however, I’ve found my attempts to change the UILabel’s frame get ignored completely, so it is not a good candidate for use with our dynamic resizing code.

Instead what we need to do is programatically create a UILabel and add it to the cell’s content view. Do this in the call to -cellForRowAtIndexPath. Use something like the following code:

- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  UITableViewCell *cell;
  UILabel *label = nil;

  cell = [tv dequeueReusableCellWithIdentifier:@"Cell"];
  if (cell == nil)
  {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"] autorelease];
        
    label = [[UILabel alloc] initWithFrame:CGRectZero];
    [label setLineBreakMode:UILineBreakModeWordWrap];
    [label setMinimumFontSize:FONT_SIZE];
    [label setNumberOfLines:0];
    [label setFont:[UIFont systemFontOfSize:FONT_SIZE]];
    [label setTag:1];

    [[cell contentView] addSubview:label];
  }
}

This is not the completed code as you’ll notice that we have initialized the label only when the cell needs created for the first time, that is if (cell == nil) after a call to -dequeueReusableCellWithIdentifier. There are two points I want to address in relation to this. First, notice the label has a tag associated with it after a call to -setTag:1. This will be used in the case where the cell is not equal to nil after a call to -dequeueReusableCellWithIdentifier. In that case we will need to get a handle to the label by calling [cell viewWithTag:1] which will return the view that we associated with that tag. Second, notice that we have added our label to the cell’s content view with a call to [[cell contentView] addSubview:label]. This is done when the label is initialized and should only be done once. Adding it each time this method is called will add the label again to the subviews array. We will come back to this code to finish it in a minute, but first let’s take a look at how we can set the height for our cell now that our label has been added.

Calculate the Cell Height

In a complex cell, your calculations could get a bit challenging, however, you only need to worry with the items that will change in height. In our example, the only item you need to deal with is the label that we added. We calculate the height of the cell by determining the size of the text based on the length of the text and the font we intend to use. The NSString class provides a method called -sizeWithFont that enables us to obtain this size. The following code show how we implement our call to -heightForRowAtIndexPath:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
{
  NSString *text = [items objectAtIndex:[indexPath row]];
    
  CGSize constraint = CGSizeMake(CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), 20000.0f);
    
  CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
    
  CGFloat height = MAX(size.height, 44.0f);
    
  return height + (CELL_CONTENT_MARGIN * 2);
}

You will notice that we have several constants we are using to calculate the size of our cell. These are defined as follows:

#define FONT_SIZE 14.0f
#define CELL_CONTENT_WIDTH 320.0f
#define CELL_CONTENT_MARGIN 10.0f

The constant CELL_CONTENT_WIDTH is the width of the entire cell. CELL_CONTENT_MARGIN is the margin we want to use all the way around the cell as the content inset, and of course FONT_SIZE is the size of the font we want to use for the label text.

The first place we use these is to create a constraint with the content width. Notice that CGSizeMake takes as its first parameter the total content width minus the margin times 2. This subtracts the margin from the left and the margin from the right from the total width to have the actual width of the label. The second parameter is just a maximum number we provide. The call to -sizeWithFont will set this to the actual height in the next line. This call to -sizeWithFont calculates the size according to the constant UILineBreakModeWordWrap which causes it to return the correct size for word wrap–which is why the width is important to get right. Next we set our height for the cell using a call to the MAX macro. This will ensure that our cell height will never be shorter than the default 44 pixels as MAX returns the larger of the two variables. Finally, we add our margin height back into the height for both top and bottom (hence x 2) and then return the result.

To help visualize how the margin is working, take a look at the following screenshot to see what each label looks like with a border around it. Turning this on with a call to [[label layer] setBorderWidth:2.0f] on the UILabel we added in the previous section makes it clear where the margins are.

Dynamic Heights Outline

Calculate the UILabel Frame and Set It

The same calculation we used to determine the height in the previous section is the code we use to set the frame for the UILabel we added in the beginning. To complete this tutorial we will finish out our implementation of -cellForRowAtIndexPath with the following code.

- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  UITableViewCell *cell;
  UILabel *label = nil;
    
  cell = [tv dequeueReusableCellWithIdentifier:@"Cell"];
  if (cell == nil)
  {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"] autorelease];
        
    label = [[UILabel alloc] initWithFrame:CGRectZero];
    [label setLineBreakMode:UILineBreakModeWordWrap];
    [label setMinimumFontSize:FONT_SIZE];
    [label setNumberOfLines:0];
    [label setFont:[UIFont systemFontOfSize:FONT_SIZE]];
    [label setTag:1];
        
    [[label layer] setBorderWidth:2.0f];
        
    [[cell contentView] addSubview:label];
    
  }
  NSString *text = [items objectAtIndex:[indexPath row]];
    
  CGSize constraint = CGSizeMake(CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), 20000.0f);
    
  CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
    
  if (!label)
    label = (UILabel*)[cell viewWithTag:1];
    
  [label setText:text];
  [label setFrame:CGRectMake(CELL_CONTENT_MARGIN, CELL_CONTENT_MARGIN, CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), MAX(size.height, 44.0f))];
    
  return cell;
}

Just remember that anything done within the if (cell == nil) block is initialization code and should only be done when the cell is first created. Anything done outside of the block will be used every time the -cellForRowAtIndexPath is called, which is any time the data gets reloaded or the view gets scrolled.

That being said, you will see that the only thing we do every time it gets called is setting the text of the current item and setting the label’s frame for the current item (lines 32 and 33). Notice that we got a handle to our UILabel by calling [cell viewWithTag:1] (lines 29 and 30) in the case where the label is nil in subsequent/non-initialization calls to this method. You will notice that our frame calculation code is exactly the same as what we used in the previous section to determine the row height.

Conclusion

Calculating dynamic cell heights is really not too hard. If you have a very complex cell, just remember that all you really need to calculate is the height based up a width that shouldn’t change and the size of the text of a certain font (unless of course you support both portrait and landscape modes–which makes things a little more challenging. I will, however, leave this as an exercise for the reader). If you find yourself wondering where your actual frame is displaying for a given view, just turn on the view border by calling [[view layer] setBorderWidth:2.0f]. This will help you see what is going on and give you the ability get to the bottom of your display problems quicker. Until next time.

DynamicHeights Demo Project

Comments

StijnSpijker says:

Heh, this is a really nice tutorial, I actually only use the label resizing part for some other components, but it’s nice either way!

By the way, you should enable OpenID for WordPress so we don’t have to create (and rememberd our passwords) for your specific site..

Thanks!

Matt Long says:

OpenID has been problematic for some users, so it’s not currently enabled. If you want to only have to remember one password, you should be using 1Password (http://agilewebsolutions.com/products/1Password ) anyhow. It’s a much better way to go IMO. Just FYI. Oh, and it’s worth every penny.

gammapoint says:

Good tutorial.

  • May want to release label after adding to content view
    -Kept getting error on the border setup so used the label background color for testing layouts.

[…] I found some help over at Cocoa is My Girlfriend. Matt Long wrote up a post on how to create UITableViewCell’s with Dynamic Height. I still need to customize for my specific needs, but it was a great help for getting me past this […]

davidm says:

I am trying to create a table cell with a UITextView inside it, so that the cell grows as the user types in more text. This behavior is seen in the Contacts application on the iPad. I’m having all kinds of trouble with this; do you have any pointers?

Matt Long says:

@davidm

I know what you’re referring to, but haven’t actually tried implementing it before. I would guess that you probably have to calculate the current text height yourself and then resize the text view and then resize the table cell accordingly, but I’m not sure. There might be something simpler. You might want to try to ask the question on Stack Overflow. There are lots of very knowledgeable folks there.

glt says:

Hi, I love your tutorial: it’s really useful! I was trying to implement those methods in my project and succeeded. Then I tried to make the TableView sectioned but failed. I know it’s really simple and I know I should know this but I’m having bad times with it :(. Can you help me? How can I change your demo, let’s work on it, to make it a sectioned TableView?

Thanx

Jaime says:

Hi,
I like your tutorial, but I have one problem. I don’t know if it is the newer version of XCode, or what but this line does not work:
[[label layer] SetBorderWidth:2.0f];

It says that “no SetBorderWidth method found. I would like to add a border to my text fields and can’t figure out how to do it. Can you help?

Matt Long says:

@Jaime,

Your capitalization is wrong. It should be [[label layer] setBorderWidth:2.0f]; Notice ‘set’ starts with a lowercase rather than uppercase letter. The sample has this correct so you must be typing it in directly?

-Matt

Jaime says:

You are right Matt, I had a capital on ‘set’. I changed it to lower case and I still get the warning and when I run the program, it doesn’t show the border.

One other question. Why are my table cells over lapping each other? At first I was still seeing the table view lines, but got rid of them. Now when I have a cell that is larger then the normal table view cell, it over laps the next cell.

Any suggestions as to fix both of these things?

Matt Long says:

@Jaime

If you’re calling setBorderWidth on the layer and you’re getting a warning, it means you haven’t added the QuartzCore.framework to your project and you haven’t added #import <QuartzCore/QuartzCore.h> to your source file–I normally place it in my prefix.pch file.

The overlapping you are experiencing could be caused by any number of things. I’m not sure exactly what you mean. Most likely you are not setting your cell height correctly. This is something you have to calculate yourself which is the whole point of this tutorial. Did you download the code example project or are you just copy/pasting from the text of the blog post? It would probably be easier if you just grabbed the whole project and started from there rather than trying to write it from the ground up.

-Matt

Jaime says:

Thanks Matt,
Your suggestions worked. I had typed in all of the information, so I copied from your sample program and the cell heights did what they were supposed to do. I also added the QuartzCore framework, so now the border shows as well.

On to the next challenge!
Jaime

[…] Long ?????http://www.cimgf.com/2009/09/23/uitableviewcell-dynamic-height/ ??? UITableViewCell Dynamic Height At first glance setting a height dynamically for table […]

Ruchi says:

Should we release the label that we created?
I tried doing it but the app became unstable and would freeze on scrolling likely as it was trying to access label that no longer existed. Thanks!

Andrew Roberts says:

Thanks for the great post!

I agree with a couple of the commenters, surely the label should be released after being added to the view? At least I didn’t realise until Xcode’s Analyze spotted the potential leak.

In the last code listings, I set line 21 to [label release]; and that seems to work perfectly, and xcode is happy too. :)

Jaime says:

Hi Matt,
I was working with you at NSCoder last Tuesday. I am using your dynamic height tableCell tutorial to set up the table cells. I am also using SQLite to populate the table cells. We created a NSMutableArray in my class that sets up the field connections to my database.

Basically, I am having a problem with it and wondered if we could talk off-line about the problem and I can give you some code examples.

Thanks,
Jaime

Matt Long says:

@Jaime,

You can shoot me an email at matt at cimgf dot com with any questions you have. I will not be at NSCoder night tomorrow night, but I should be next week.

-Matt