Static Tables and Dynamic Type

Apple introduced static table views in Interface Builder way back in iOS 5 and dynamic type in iOS 7. Unfortunately either through design or bugs the two features do not play well together in iOS 9 (or iOS 8). This post covers how to get dynamic type to work with static table views when deployed to iOS 9.

Last updated: Jan 17, 2020

Note: Most of the issues in this post have been resolved with later versions of Xcode:

  • If you’re targetting at least iOS 10 select the option in Interface Builder to “Automatically Adjust Fonts” for each of the text labels.
  • You should also be able to make the static cells self-size by setting the row height and estimated row height to “Automatic” in Interface Builder. I’ve found that works even for the built-in table view styles (basic, subtitle, etc.).

The workarounds in this post should only be necessary if you still need to deploy back to iOS 9 or earlier.

Creating A Static Table View

To get started I have a simple static table view with two sections and a grouped style:

Static Table Default Text Size

The table view configuration in Interface Builder (I will skip most of the details):

Table view setup

I have two custom table view cells similar to the standard Basic and Subtitle styles. (Don’t use the built-in styles if your targetting iOS 9). For the title text I am using a Headline dynamic type:

title label

For the subtitle text I am using a Subhead dynamic type:

subtitle label

I have set the number of lines for each label to zero and added constraints to pin the labels to the top, bottom, leading and trailing margins of the cell content view. See the sample code for details.

Making the cells self-sizing

As the user changes their preferred text size in the device settings the size of the static table view cells will need to change. The easiest way to support that is to make the table view cells self-sizing.

As with dynamic cells that means setting the row height to automatic and using a non-zero value for the estimated row height. Using Xcode 11 you can set both properties to “Automatic” in Interface Builder:

Automatic table view heights

Unfortunately, this doesn’t seem to work for iOS 9. The only workaround I have found is to override tableView:heightForRowAtIndexPath: and have it return the automatic dimension:

// iOS 9 only
override func tableView(tableView: UITableView,
  heightForRowAtIndexPath indexPath: NSIndexPath)
  -> CGFloat {
  return UITableViewAutomaticDimension
}

The table view controller calls this delegate method for every row when displaying the table so it can be slow for big tables. That should not usually be the case for a static table view.

Handling Dynamic Type Changes (iOS 9 only)

This section is no longer necessary if your minimum deployment target is at least iOS 10. Select the “Automatically Adjusts Font” option in Interface Builder for each label. See Auto Adjusting Fonts for Dynamic Type.

At this point we have table cells with the right size when the app is first run but they do not update if the user changes the text size with the app running. A word of warning at this point: dynamic type does not work in the iOS 9.3 simulator so test on a device.

You may be thinking that we should create an observer for UIContentSizeCategoryDidChangeNotification but that is not necessary. The table view controller is already listening and causes the table to reload automatically for us. What we need to do is override the table view data source method tableView:cellForRowAtIndexPath: to update the preferred font in our cells.

A good time for a protocol

Rather than handle every custom cell type in our data source I am going to create a simple protocol that the cell subclasses can adopt. It has one method to update the view when the content size changes:

protocol UYLPreferredFont {
  func contentSizeChanged()
}

My basic custom table view cell has a single title label connected via an outlet in Interface Builder:

class UYLBasicStaticTableViewCell: UITableViewCell {
  @IBOutlet weak var titleText: UILabel!
}

Adopting the protocol in a class extension and updating the label with the preferred font is trivial:

extension UYLBasicStaticTableViewCell: UYLPreferredFont {
  func contentSizeChanged() {
    titleText.font = UIFont.preferredFont(forTextStyle: .headline)
  }
}

The static table view cell with a title and subtitle is similar. The custom subclass has two UILabel properties:

class UYLSubtitleStaticTableViewCell: UITableViewCell {
  @IBOutlet private weak var titleText: UILabel!
  @IBOutlet private weak var subtitleText: UILabel!
}

This time we update both labels with the preferred font style:

extension UYLSubtitleStaticTableViewCell: UYLPreferredFont {
  func contentSizeChanged() {
    titleText.font = UIFont.preferredFont(forTextStyle: .headline)
    subtitleText.font = UIFont.preferredFont(forTextStyle: .subheadline)
  }
}

Our data source function tableView:cellForRowAtIndexPath: can now test for protocol conformance without knowing about the cell type. If a cell conforms to UYLPreferredFont we call contentSizeChanged to have the cell update itself:

override func tableView(tableView: UITableView,
              cellForRowAtIndexPath indexPath: NSIndexPath)
              -> UITableViewCell {
  let cell = super.tableView(tableView, cellForRowAtIndexPath: indexPath)
  if let cell = cell as? UYLPreferredFont {
    cell.contentSizeChanged()
  }
  return cell
}

Note that we first get the static cell from the super class.

This is easy to extend if we have other custom table view cell subclasses. We should now have a static table view that responds to changes to the text size. There is also nothing here specific to iOS 9 so this should also work with iOS 8. Here is how it looks with the small text size:

Small text

and now with the large text size:

Large text

Sample Code

You can find the code from this post in the StaticTableDynamicType Xcode project in my GitHub Code Examples repository.