Variable Height Table View Header

Making a table view header that automatically adjusts (self-sizes) its height to allow its contents to fit should not be so hard. Unfortunately it is a problem that has been with us for a number of years which means much of the advice on how to do it is out of date. This is what works for me.

Last updated: Jun 12, 2020

The Problem

I want to show a text message as a header to a table view. The size of the table view header needs to be dynamic to avoid chopping the text label even if the text size or the table view size changes. For example, when the user changes their preferred text size or on rotation.

Unfortunately if you add a header view to a table this is how it ends up looking by default. Note how the header is too small so truncates the text to fit.

Clipped Table View Header

The header view never resizes to allow the label to fit without clipping. Here is what it should like:

Table View Header

Creating a Table View Header

First a quick recap on how to add a header view to a table view. Assuming I start with a table view controller. Drag a UIView onto the view controller aiming for the space above the prototype cells:

Adding a header view

To make it easier to see I have set the background of the header view to red and then added a UILabel containing the text I want in the header. I have manually set the header view height in the Storyboard:

Interface Builder

I added constraints for the UILabel to pin it to the header view leading/trailing margins:

View Layout

I made the UILabel multi-line by setting the number of lines to zero. I have also used a dynamic font (Title 3):

UILabel configuration

That’s the basic setup of a table view header. Unfortunately if the height needs to be dynamic we are not yet done.

The Fix - viewDidLayoutSubviews

When the system loads the table view from the Storyboard the header view takes the size we specify in Interface Builder. Unfortunately the table view does nothing to make sure it is always sized to fit the contents. So how do we size the table view header so that the text fits?

Anytime the bounds of the view controller root view changes we get a chance to make changes to the subviews if we override viewDidLayoutSubviews. We can use this to work out and set the right height for the header view.

Let’s walk through how we might do that in our table view controller. First we make sure to call super:

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()

The tableHeaderView is an optional property of UITableView so we can use a guard to safely unwrap it or give up if it is missing:

  guard let headerView = tableView.tableHeaderView else {
    return
  }

Calculating the right size of the header view is easy. We ask the header view to return the smallest size it can be and still meet its constraints.

  let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

We can now set the height of the header view but we need to be careful. The system has just finished laying out the subviews of our view controller. When we change the frame of the table view header we will trigger a new layout cycle. To make sure we do not end up with a layout loop we want to make sure we only dirty the layout when the height of the view has changed:

  if headerView.frame.size.height != size.height {
    headerView.frame.size.height = size.height

We need to reassign the table view header for the new frame size to take effect:

    tableView.tableHeaderView = headerView

Finally we make the table view redo its layout to position the cells for the new header size. This last step only seems to be necessary for iOS 9.

    tableView.layoutIfNeeded()
  }
}

If you omit that last step with iOS 9 the table view does not move the position of the first row in the table to take into account the changed size of the header. For example when changing from a large to a small preferred font size the header view shrinks leaving a gap to the first row:

iOS 9 header resize

The table view fixes the layout as soon as you start to scroll. Calling layoutIfNeeded does not seem to be required with iOS 10. That is a common issue with the more complex UIKit classes like UITableViewController. It is often not clear how Apple intends we use the API’s so you need to discover your own workarounds which may change with each new release.

One final tip. Make sure you do not set an explicit value for the preferredMaxLayoutWidth of the text label. Leave the value blank in Interface Builder so that you get the automatic setting:

Desired width - automatic

Sample Code

The TableHeader Xcode project for this post is in my GitHub repository and includes both Swift and Objective-C versions. The projects have been updated for Xcode 11, iOS 13 and Swift 5.

Learn More

Want to learn more about using table views to build adaptive layouts? Get my book on Auto Layout: