iOS 8: Self Sizing Table View Cells with Dynamic Type

When Dynamic Type came out in iOS7, I was hoping to get to use it. After all, who doesn’t want to make their app more readable for users who need it?

Well, it seemed really difficult to get started with it – I’m still figuring the best way to resize my iOS7 cells with AutoLayout!, so I never “had time” to work on it much, especially with more “important” features taking a priority.

If you’re still curious / want to compare, the best example of how to do Dynamic Type in iOS7 is by far James Dempsey’s WordWithoutFriends example he presented at CocoaConf San Jose I was lucky to attend in April this year.

iOS8 is a completely different story. With self-sizing tableView and collectionView cells, dynamic type is so easy, a cave man can do it. Seriously, though, you really have no excuse NOT to do it moving forward.

To show you how easy it is, I built a simple Seinfeld Quotes app. Here is how it works with Dynamic Type:

Here is all I had to do to make it happen:

Use Self-Sizing Cells

I don’t know about you, but for me, self-sizing tableView and collectionView cells are seriously the most exciting feature of iOS8. No way I’m going back to calculating my cell heights the old way!

To set up your self-sizing cell, just add AutoLayout to your cell, specify your tableView’s estimatedRowHeight, and set the tableView’s rowHeight to UITableViewAutomaticDimension (this will be the default setting in future XCode6 versions).

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.estimatedRowHeight = 89
        tableView.rowHeight = UITableViewAutomaticDimension
    }

That’s it! You should be using AutoLayout anyway, so this is basically replacing all your row height calculations in tableView:heightForRowAtIndexPath: with just 2 lines of code (oh wait, make it 1 line of code in a few XCode6 betas!).

Use preferredFontForTextStyle

Again, using preferredFontForTextStyle is something you should be doing anyway. Instead of thinking of your fonts as hard coded for each screen and scenario, you can use the built-in styles as appropriate:

UIFontTextStyleHeadline
UIFontTextStyleBody
UIFontTextStyleSubheadline
UIFontTextStyleFootnote
UIFontTextStyleCaption1
UIFontTextStyleCaption2

You can also specify your own text styles as your designer defines. Take a look at this example UIFont category for specifying your own text styles.

When assigning the preferredFontForTextStyle font to the labels in your cell, make sure it is assigned somewhere where it is recalculated every time the cell data gets re-calculated.

For example, if you set your font in the awakeFromNib: function override, it’ll be set only when the cell is first displayed on the screen, but not when it is recycled by the tableView as the user scrolls. In one project, I set up my font information in a static struct (along with text information, and font color) – this again will not work, since the font will not be recalculated after it is set the first time.

In my Seinfeld Quotes app. I created a custom QuoteTableViewCell, with a configure function that gets called every time tableView:cellForRowAtIndexPath: is called. This is where I placed my font configuration:

    // QuoteTableViewCell
    func configure(#quote : Quote) {
        
        quoteContentLabel.text = quote.content
        quoteContentLabel.accessibilityLabel = "Quote Content"
        quoteContentLabel.accessibilityValue = quote.content
        
        scenarioLabel.text = "- \(quote.scenario)"
        scenarioLabel.accessibilityLabel = "Quote Scenario"
        scenarioLabel.accessibilityValue = quote.scenario
        
        // font is set here!
        quoteContentLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
        scenarioLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
    }

Remember, it’s dynamic type, not static. So treat your font as if it can be recalculated to a different UIFont value every time!

Reload Data on Content Size Change

UPDATE: As of XCode 6 GM Release, this step is no longer necessary! You can see the most updated sample project on Github here.

If you completed the last two steps – using self-sizing cells and preferredFontForTextStyle for your Font – Dynamic Type will now work if the user hard-quits your app after resetting the text size on her phone.

Of course, you don’t want the user to have to quit your app to get the correct font sizes, so all you have to do is reload your tableView on the UIContentSizeCategoryDidChangeNotification:

 override func viewDidLoad() {
        
        super.viewDidLoad()
        
        NSNotificationCenter.defaultCenter().addObserver(self,
            selector: "onContentSizeChange:",
            name: UIContentSizeCategoryDidChangeNotification,
            object: nil)
        
        tableView.estimatedRowHeight = 89
        tableView.rowHeight = UITableViewAutomaticDimension

    }
    
    override func viewDidDisappear(animated: Bool)  {
        super.viewDidDisappear(animated)
        
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
    
    func onContentSizeChange(notification: NSNotification) {
        tableView.reloadData()
    }

That’s it! Like I said, there is really no reason NOT to support Dynamic Type. With just a few simple modifications to your app (which are best practice anyway), you can easily make your vision-challenged users A LOT happier.

You can view the full source code example on Github here.

Enjoy the article? Join over 14,500+ Swift developers and enthusiasts who get my weekly updates.

  • aobo711

    Thanks for this post, it help me out with some problems.
    But Chinese words don’t work here, why is that?

  • SoniXAnT

    Hi! Thanks for the post, very useful! One comment though, I don’t think removing the observer on viewDidDisappear is correct in this case. If you present a new view controller from here you will lose your observation and never regain it when coming back from the presented view controller.

  • ronnieliew

    Hey Natasha, thanks for sharing this. I was looking at several different articles on doing self-sizing in iOS 8. Your code seemed to work for the most part but actually the height of the multi-line UILabel would be wrong for some string. For example if this sentence would fail: “There was things which he stretched, but mainly he told the truth.”

  • Bucky Barnes

    Hi Natasha thanks for the tutorial ,.n the app that we are creating I have a scenario where labels should be hidden in the row of a table and when I do that the height doesn’t adjust, any nifty solution would be much helpful.

  • Joe Miller

    Natasha, thank you very much for your work. I’ve been pulling my hair out trying to get dynamic text to render properly in a table view – I’ve watched the WWDC14 video a couple times and tried *many* SO questions/answers. It turns out I was doing everything basically correctly – BUT I didn’t realize it until I looked at your project that adding an accessory (Disclosure Indicator) blows up the sizing of the cell. Of course I was adding an accessory all the time without thinking about it..! I verified it happens in your project too by adding an Accessory to quoteCell. However, I see in WordsWithoutFriends that he has an accessory on his custom cell and the constraints look basically the same. Do you have any idea at all what’s going on here? Do you think it’s a defect or simply a matter of adjusting constraints? I’m running XCode 6.1.1 deploying 8.1.
    Thanks again, any ideas are *greatly* appreciated.

  • Yariv

    I would register the notification in viewWillAppear (or Did). Whenever this view controller goes off screen but not dealloc-ed, the observer will be lost.

  • 能蟹仔

    i set the dynamic type, and then back to my application , all text is large,but sizing not work,

  • Paril Shah

    Hi Natasha, Thanks for this great tutorial. Can you please guide how to make UITableviewCell which works fine with both iOS versions, iOS 7.0 and iOS 8.0 ? I know the both way for calculating the height of cell. But the solution of iOS 7.0 does not work fine with iOS 8.0.

  • Frederik A. Winkelsdorf

    Checked your GH demo. Just a note, there are two issues with your way of removing the Observer.

    1) Calling NSNotificationCenter.defaultCenter().removeObserver(self) is highly error-prone. You not only remove your Dynamic Type observer but all observers the (Table)ViewController has under the hood. Expect issues that are difficult to track when showing/reusing the View again.
    2) As soon as viewDidDisappear is triggered the observation is stopped and never started again if the View is going to reappear. In other scenarios viewDidDisappear will likely be triggered, e.g. by showing a Modal View Controller.

    The demo works fine without but might be worth noting if somebody uses this code in a more complex situation and wonders about side-effects.

    To be on the safe side, remove the call to removeObserver from viewDidDisappear and implement something like:
    deinit {
    NSNotificationCenter.defaultCenter().removeObserver(self, name: UIContentSizeCategoryDidChangeNotification, object: nil)
    }

    • Frederik A. Winkelsdorf

      Or, if you prefer to not receive Notifications while View is invisible, add/remove the Observers in viewWillAppear/viewWillDisappear or viewDidAppear/viewDidDisappear. Then again you have to reload the table if you want to adjust for Dynamic Type changes taken place while View was invisible.

  • paul

    Not sure if you are still monitoring this thread?

    I have a contentview in tableviewcell with an imageview to the left, to labels to its immediate right and then a label underneath them all.

    I have setup auto resize as you say but all I get is the same default iOS 44 cell row height with this warning:

    Detected a case where constraints ambiguously suggest a height of zero for a tableview cell’s content view. We’re considering the collapse unintentional and using standard height instead

    … help?