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
super.init()
}
}
class ItemStore {
var allItems = [Item]()
func createItem(text: String) -> Item {
let newItem = Item(text: text)
allItems.append(newItem)
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!”)
}
}
}
import UIKit
protocol RefreshableTableViewController {
func refresh()
}
class MyTableViewController: UITableViewController, RefreshableTableViewController {
var itemStore: ItemStore!
override func viewDidLoad() {
super.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]
cell.useItem(item)
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
tableView.beginUpdates()
tableView.endUpdates()
}
override func didRotateFromInterfaceOrientation(
fromInterfaceOrientation: UIInterfaceOrientation) {
// updating all constraints and table rows after a rotation requires the heavy
// hand of a full reload
tableView.reloadData()
}
}
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
}
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
updateMeasurements()
}
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)
refreshIfNeeded(newHeight)
return true
}
func refreshIfNeeded(newHeight: CGFloat) {
if newHeight != lastCalculatedTextHeight {
lastCalculatedTextHeight = newHeight
heightConstraint.constant = newHeight
tableController?.refresh()
}
}
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 + textInset.top
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
}
}