Autoresizing table cells

Extending the autoresizing of UITextView to table cells, this is how that looks.

First, let’s create a trivial model class and a storage class for that model:

class Item: NSObject {

  var text: String


  init(text: String) {

    self.text = text





class ItemStore {

  var allItems = [Item]()


  func createItem(text: String) -> Item {

    let newItem = Item(text: text)


    return newItem



  init() {

    for_in 0..<20 {

      createItem(“For ever and ever, this may be a great filler text. We will never know with absolute certainty, will we? Horray!”)




As you can see, I create 20 items all with the same text, just to be able to fill more than a screen in the table view. In the main storyboard, delete the default window and replace it all with a table view controller. Subclass the controller:

import UIKit

protocol RefreshableTableViewController {

  func refresh()


class MyTableViewController: UITableViewController, RefreshableTableViewController {

  var itemStore: ItemStore!


  override func viewDidLoad() {



    // move down the table view contents a bit


    let statusBarHeight = UIApplication.sharedApplication().statusBarFrame.height

    let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)

    tableView.contentInset = insets

    tableView.scrollIndicatorInsets = insets


    tableView.rowHeight = UITableViewAutomaticDimension

    tableView.estimatedRowHeight = 65



  override func tableView(tableView: UITableView,

               numberOfRowsInSection section: Int) -> Int {

    return itemStore.allItems.count



  override func tableView(tableView: UITableView,

                cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier(“ItemCell”,

                forIndexPath: indexPath) as! ItemCell


    // cell needs a delegate link back to the tableView controller, so it

    // can trigger refresh of row heights

    cell.tableController = self


    // hand the item to the cell. as it updates the item, the model store

    // is automatically kept up to date since it holds a reference to the item

    let item = itemStore.allItems[indexPath.row]



    return cell


  func refresh() {

    // after updating the size and constraints of the table view cell,

    // the table itself

    // must refresh to adjust the distance between the rows






  override func didRotateFromInterfaceOrientation(

                 fromInterfaceOrientation: UIInterfaceOrientation) {

    // updating all constraints and table rows after a rotation requires the heavy

    // hand of a full reload





From the application delegate, create the store, get the view controller and feed it the store:

  func application(application: UIApplication, didFinishLaunchingWithOptions

             launchOptions: [NSObject: AnyObject]?) -> Bool {

    // Override point for customization after application launch.


    let itemStore = ItemStore()


    let itemController = window!.rootViewController as! MyTableViewController

    itemController.itemStore = itemStore


    return true


Now, subclass UITableViewCell:

class ItemCell: UITableViewCell, UITextViewDelegate {


  @IBOutlet var textView: UITextView!

  @IBOutlet var heightConstraint: NSLayoutConstraint!

  var lastCalculatedTextHeight: CGFloat = 0.0

  var tableController: RefreshableTableViewController?

  var item: Item!     // must be class for reference semantics


  func useItem(item: Item) {

    self.item = item

    self.textView.text = item.text




  func updateMeasurements() {

    let newHeight = calculateHeightTextView(textView, forText: textView.text)

    heightConstraint.constant = newHeight


  func textViewDidChange(textView: UITextView) {

    // update the model with the current text

    item.text = textView.text



  func textView(textView: UITextView, shouldChangeTextInRange range: NSRange,

                    replacementText text: String) -> Bool {

    // it’s important that we measure the text and set the constraint *before*

    // the textview actually sets the new

    // text, else the scroll view will jitter and jump.

    // that’s why we catch “shouldChange” to get in ahead of that


    let newText = buildNewText(textView.text, range: range, replacementText: text)

    let newHeight = calculateHeightTextView(textView, forText: newText)


    return true



  func refreshIfNeeded(newHeight: CGFloat) {

    if newHeight != lastCalculatedTextHeight {

      lastCalculatedTextHeight = newHeight

      heightConstraint.constant = newHeight





  func buildNewText(oldText: String, range: NSRange,

                   replacementText: String) -> String {

    if let nsRange = oldText.rangeFromNSRange(range) {

      let newText = oldText.stringByReplacingCharactersInRange(nsRange,

                   withString: replacementText)

      return newText


    // if the range was invalid, just return the old text

    return oldText



  func calculateHeightTextView(textView: UITextView, forText: String) -> CGFloat {


    if let font = textView.font {

      let padding = textView.textContainer.lineFragmentPadding

      let width = textView.contentSize.width – 2 * padding


      let attributes = [NSFontAttributeName: font]


      let newRect = forText.boundingRectWithSize(

               CGSize(width: width, height: CGFloat.max),

               options: [.UsesLineFragmentOrigin, .UsesFontLeading],

               attributes: attributes, context: nil)

      let textInset = textView.textContainerInset

      let newHeight = newRect.height + textInset.bottom +


      return newHeight


    // if we don’t know what else to say, just return the old height,

    // probably better than nothing

    NSLog(“failed to calculate height, no font?”)

    return textView.contentSize.height



This cell is designed in IB as the prototype cell. It needs to get constraints as follows:
The height constraint on the text view needs to get a priority less than 1000, e.g. 999, else it will give a conflict with a constraint that iOS will add on the fly when instantiating the cell. Interestingly, even though your own height constraint gets a lower priority than the system created constraint, still it will work fine.
You then need to bind the height constraint to the outlet in the ItemCell, and the text view to its outlet in the ItemCell, as well. The result looks like in this very brief movie I posted on Vimeo.
Interestingly, the size change happens with a smooth animation even though I didn’t animate the change in the size of the text view as I had to do in the previous project. The animation originates in the table view itself, so it comes for free.
Because there are a number of things you need to set correctly in Interface Builder, and some details I may have glossed over here, I’ve posted the entire project for your downloading pleasure. Again, as in the previous post, this is all Swift 2.x and XCode 7.3.

Leave a Reply

Your email address will not be published. Required fields are marked *