How to make a UITableView cell dynamically change its height while user is typing?

Viewed 926

I am currently working on a feature for dynamically changing a UITableView cell's height while typing. So, if the user has typed in a item that extends the current max width for a cell, then a the cell's height should increase. My specialized TableViewCell contains a TextView and an UILabel and is also, connected to another cell below it. So, there is a TableView with two TableViewCells within that one TableView. I've been trying to experiment ways that it could be solved but nothing has worked so far. Does anyone have any conceptual ideas to get it done?

2 Answers

First set your tableView rowHeight to UITableViewAutomaticDimension, in your table view cell add UITextView with top, leading, bottom and trailing constraints. Set textView.isEditable to true and textView.isScrollEnabled to false, then implement textView delegate method textViewDidChange. When text changes you need to report to view controller to update the tableview, one way is by delegation. Implement your table view cell delegate in your view controller, from where you'll call tableView.beginUpdates() and tableView.endUpdates(). That's it. Below is a tested and working example.

your view controller:

import UIKit
class ViewController: UIViewController, UITableViewDataSource, ExpandingTableViewCellDelegate {

    @IBOutlet weak var tableView: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.register(UINib(nibName: "ExpandingTableViewCell", bundle: nil), forCellReuseIdentifier: "ExpandingTableViewCellIdentifier")
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ExpandingTableViewCellIdentifier", for: indexPath) as! ExpandingTableViewCell
        cell.delegate = self
        return cell
    }

    // Expanding teable view cell delegate
    func didChangeText(text: String?, cell: ExpandingTableViewCell) {
        tableView.beginUpdates()
        tableView.endUpdates()
    }
}

your cell:

import UIKit
protocol ExpandingTableViewCellDelegate: class {
    func didChangeText(text: String?, cell: ExpandingTableViewCell)
}

class ExpandingTableViewCell: UITableViewCell, UITextViewDelegate {
    @IBOutlet weak var textView: UITextView!
    weak var delegate: ExpandingTableViewCellDelegate?

    override func awakeFromNib() {
        super.awakeFromNib()

        textView.delegate = self
        textView.isEditable = true
        textView.text = "Start typing: ..."
        textView.isScrollEnabled = false
    }

    func textViewDidChange(_ textView: UITextView) {
        delegate?.didChangeText(text: textView.text, cell: self)
    }
}

Example project is here: https://github.com/bavarskis/ExpandingTableViewCell.git

As you are asking for a conceptual understanding of what’s going on you might want to look at a project in my github page:

https://github.com/mpj-chandler/variableSizeTextTableView

The project contains a subclassed UITableViewController controller that is also the delegate for the text views. The key functions are as follows:

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    var textFrame : CGRect = textView.frame
    let newHeight : CGFloat = ceil(textView.contentSize.height)
    if newHeight - textFrame.height > 0 {
        textFrame.size.height = ceil(textView.contentSize.height)
        textView.frame = textFrame
        textView.setContentOffset(CGPoint(x: 0, y: 5), animated: false)
        self.updateLayout(textView)
    }

    textView.selectedRange = NSRange(location: textView.text.characters.count, length: 0)
    textView.becomeFirstResponder()
    return true
}


func updateLayout(_ forTextView: UITextView) -> Void {
    guard forTextView.tag < self.data.count else { return }

    self.data[forTextView.tag] = forTextView.text

    let newHeight : CGFloat = forTextView.frame.height
    let oldHeight : CGFloat = self.tableView.cellForRow(at: IndexPath(row: forTextView.tag, section: 0))!.frame.height

    self.heights[forTextView.tag] = newHeight + 2
    UIView.animate(withDuration: 0.25, animations: { 
        for cell in self.tableView.visibleCells {
            if let indexPath = self.tableView.indexPath(for: cell) {
                if (indexPath as NSIndexPath).row > forTextView.tag {
                    cell.frame = CGRect(origin: CGPoint(x: cell.frame.origin.x, y: cell.frame.origin.y + forTextView.frame.height + 2 - oldHeight), size: CGSize(width: cell.frame.width, height: self.heights[(indexPath as NSIndexPath).row]))
                }
                else if (indexPath as NSIndexPath).row == forTextView.tag
                {
                    cell.frame = CGRect(origin: cell.frame.origin, size: CGSize(width: cell.frame.width, height: self.heights[(indexPath as NSIndexPath).row]))
                }
            }
        }
    }) 
}

Caveats: this was written in Swift 2, but the translation to Swift 4 should be trivial. Also, it’s not a good idea to reference cells with tags if you want the table view to be editable, but hopefully you get the gist.

Hope it helps.

Related