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.

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 based on the standard Basic and Subtitle styles. 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 an estimated row height for the table. We cannot do that in Interface Builder but we can do it in viewDidLoad for our static table view controller:

override func viewDidLoad() {
  super.viewDidLoad()
  tableView.estimatedRowHeight = 56.0
}

Our cell row heights are hard coded in Interface Builder. To get self-sizing cells we need to change that to UITableViewAutomaticDimension. Unfortunately setting tableView.rowHeight does not work in viewDidLoad for a static table. The only workaround I have found is to override tableView:heightForRowAtIndexPath: and have it return the automatic dimension:

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

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 with Xcode 7.3 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.preferredFontForTextStyle(UIFontTextStyleHeadline)
  }
}

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.preferredFontForTextStyle(UIFontTextStyleHeadline)
    subtitleText.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
  }
}

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.

Never miss a post!

iOS Size Classes Cheat Sheet

Subscribe and get my free iOS Size Classes Cheat Sheet

Success! Now check your email to confirm your subscription and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
No time to watch WWDC videos?

Sign up to get my iOS posts direct to your inbox and I will send you a free PDF of my iOS Size Classes Cheat Sheet.

OK! Check your inbox (or spam folder) for an email to confirm your details and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
Archives Categories