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)
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.
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:
- Calculate the minimum bounding rect for the text in the text view
- Constrain this value to a minimum of 50.0 just for a nicer layout
- Update our text view bounds
- 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