Self Sizing Table View Cells

Last updated: Jun 12, 2020

It only took five years but this is much easier to do with Interface Builder in Xcode 11. See Self Sizing Table View Cells in Interface Builder

Dynamic Type Everywhere

I covered the basics of supporting dynamic type some time ago. Introduced in iOS 7, dynamic type allows the user to specify a preferred reading size for text. In the upcoming release of iOS 8 the Apple internal apps will adopt dynamic type and it seems they are making a big push to encourage all apps to do the same.

Supporting dynamic type is not without cost. You need to consider the impact when a user changes their text size preference. For example you may need to adjust the row height of a table view as the text size changes. I covered one approach to create Table View Cells With Varying Row Heights a while ago.

That example targeted iOS 7 and used auto layout for the cell content view. It needed a prototype cell to layout and size a cell from within the table view delegate method tableView:heightForRowAtIndexPath:. You can go back and read that earlier post or browse the example code if you want to learn more. In this post we will look at how to simplify that example code with some changes that Apple is introducing in iOS 8.

Self Sizing Cells

The WWDC 2014 Session 226 What’s New in Table and Collection Views covers a way starting with iOS 8 to create self-sizing cells in both collection and table views. This removes the need to use tricks such as a prototype cell to get cell heights. I will walk through a working example but if you want the one line summary:

Use estimatedRowHeight not rowHeight

Sample Code

I am going to use the original example code as the starting point so go back and read that post if you want the step by step instructions. As we will see the only code that will change is the table view controller and the storyboard. In brief, I am using the first fifteen chapters of Huckleberry Finn as a source of text with varying line length. A table view shows each line of text together with a label showing the line number:

Both text fields are using a dynamic type font style so the table needs to adjust as the text size changes. Don’t forget to set the number of lines property of the text label to zero as it will have a variable number of lines. Also make sure that you do not have an explicit value set for the preferred width of the label (see one more problem below)

Universal Storyboard

The original sample code was an iPhone only project with an iPhone only storyboard. In another move to make it easier to support different sizes of device iOS 8 introduces a universal storyboard. If you look carefully at the screenshot of the new storyboard below you will notice that the views are no longer iPhone or iPad size.

Universal storyboard

Instead of creating device specific layouts iOS 8 allows you to create a more generic default layout that uses auto layout to adapt to the actual device dimensions. To customise for specific device sizes iOS 8 defines two size classes: regular and compact. The default layout constraints can be overridden in the storyboard for any combination of the size class both horizontally and vertically. I will not go into detail here as the default layout is all we need and gives us a universal app without any extra code.

Setting the Estimated Row Height

Before iOS 8 if you had a table view that had cells with varying row heights you needed to use the table view delegate method tableView:heightForRowAtIndexPath: to return the height of each row. Loading the table view calls this method for every row in the table which for large tables can be slow. In iOS 7, Apple introduced tableView:estimatedHeightForRowAtIndexPath: to give an estimate of the row height which improved table load speed but you still need the height of each cell.

In iOS 8 we no longer need either of these methods as we can directly set the estimatedRowHeight in the table view controller and rely on auto layout to do the rest. In the WWDC session they use viewDidLoad for this:

- (void)viewDidLoad {
   ...
   ...
   self.tableView.estimatedRowHeight = 100.0;
}

Since we no longer need to waste time with tableView:heightForRowAtIndexPath: we can also remove the dummy prototype cell from the table view controller.

Loading a Table View From a Storyboard

Unfortunately if you build and run at this point it does not work. The cell height is incorrect causing a truncated text label.

The root cause of the problem is that when the storyboard loads the table view it sets the rowHeight property to the value from interface builder. To make use of self-sizing cells we need to set estimatedRowHeight and leave rowHeight to what should be the default in iOS 8 - UITableViewAutomaticDimension.

The WWDC session mentions this problem and even that the next seed fixes it. Later versions of Xcode did finally fix this problem but you can workaround the problem in viewDidLoad:

- (void)viewDidLoad {
   ...
   ...
   self.tableView.estimatedRowHeight = 100.0;
   self.tableView.rowHeight = UITableViewAutomaticDimension;
}

One More Problem

When I first tried this I had somehow managed to set an explicit content width for the line label in Interface Builder:

This caused the line length to be wrong when the cells were first loaded. If you’re experiencing problems check the size inspector and make sure you have preferred width set to “Automatic”:

Wrapping Up

That was a lot of words and not much code so I hope you made it this far. This is one of those cases where SDK improvements saves us code. The combination of universal storyboards, auto layout and self-sizing cells makes it easy to support different device dimensions with almost no code.

The new and improved auto-sizing table view code is in my GitHub repository. The name of the sample project is SelfSize.