Self-sizing UITableViewCell with UITextView in iOS8

Ever since watching the session ‘What’s New in Table and Collection Views’ from WWDC 2014, I have tried to create all of my tables and collections use self-sizing and update for dynamic typing. Straight from the presentation, this sums up a pretty good motivation:

This strategy is really going to improve the way that we architect our table views, number 1, because you can encapsulate logic right in the cells, and 2, the fact that you can do that will make it so much easier to have cells that are not a hard coded compile time known height which means you can easily adopt the dynamic font changes. -- Luke Hiesterman

Having a multi line text input seemed like the perfect case for this. The aim was pretty simple:

  • Have a UITableViewCell instance that would automatically resize to the input text

Here’s a little sample of the product (because everything’s better with GIFs) Multi line text sample There are plenty of examples out there to do this with UILabel instances such as the post on Self Sizing Table View Cells - Use Your Loaf. First things first, to enable self-sizing cells on your UITableView, set the estimatedRowHeight and rowHeight properties of you table as so (I do mine in viewDidLoad):

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 44.0 // Replace with your actual estimation
    // Automatic dimensions to tell the table view to use dynamic height
    tableView.rowHeight = UITableViewAutomaticDimension
}

Next create a custom UITableViewCell xib file and setup constraints to layout the UITextView. I have basically just wrapped the UITextView in constraints with zero constant to the container view as seen below. Constraints On Cell Next we basically have to calculate the height we want as we modify the text. Setting ourselves as the delegate of the text view, we can do this in textViewDidChange to get the changes as the user types. The process is:

  1. Calculate the minimum bounding rect for the text in the text view
  2. Constrain this value to a minimum of 50.0 just for a nicer layout
  3. Update our text view bounds
  4. Let the table view update itself/relayout
import UIKit

class MultiLineTextInputTableViewCell: UITableViewCell {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet var textView: UITextView!

    override init?(style: UITableViewCellStyle, reuseIdentifier: String!) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    /// Custom setter so we can initialise the height of the text view
    var textString: String {
        get {
            return textView.text
        }
        set {
            textView.text = newValue

            textViewDidChange(textView)
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        // Disable scrolling inside the text view so we enlarge to fitted size
        textView.scrollEnabled = false
        textView.delegate = self
    }

    override func setSelected(selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        if selected {
            textView.becomeFirstResponder()
        } else {
            textView.resignFirstResponder()
        }
    }
}

extension MultiLineTextInputTableViewCell: UITextViewDelegate {
    func textViewDidChange(textView: UITextView!) {

        let size = textView.bounds.size
        let newSize = textView.sizeThatFits(CGSize(width: size.width, height: CGFloat.max))

        // Resize the cell only when cell's size is changed
        if size.height != newSize.height {
            UIView.setAnimationsEnabled(false)
            tableView?.beginUpdates()
            tableView?.endUpdates()
            UIView.setAnimationsEnabled(true)

            if let thisIndexPath = tableView?.indexPathForCell(self) {
                tableView?.scrollToRowAtIndexPath(thisIndexPath, atScrollPosition: .Bottom, animated: false)
            }
        }
    }
}

The tableview is accessed inside the cell by an extension:

extension UITableViewCell {
    /// Search up the view hierarchy of the table view cell to find the containing table view
    var tableView: UITableView? {
        get {
            var table: UIView? = superview
            while !(table is UITableView) && table != nil {
                table = table?.superview
            }

            return table as? UITableView
        }
    }
}

I feel like it’s a bit dirty calling tableView.beginUpdates(); tableView.endUpdates() but I have tried all of layoutSubviews(), setNeedsLayout(), updateConstraints(), setNeedsDisplay(), but none of these actually seem to update the cells layout. All the code for this project is available on my BlogCodeSamples GitHub Repo Edit (23 Dec 2014): Removed some uneeded bounds changes from helpful comment suggestions. Edit (15 May 2015): Changes in textViewDidChange to fix some issues with the text view jumping on each character after some awesome discussions on GitHub about the issue

Licensed under CC BY-NC-SA 4.0
Last updated on Jan 01, 0001 00:00 UTC
Built with Hugo
Theme Stack designed by Jimmy