8
votes

I have a UITableView with two UITextViews side by side inside each of the cells. I want both the UITableViewCell and UITextView to increase in height so that the user doesn't need to scroll within the UITextView. Here is what I've tried:

In the TableViewController Class:

    self.tableView.estimatedRowHeight = 44
    self.tableView.rowHeight = UITableViewAutomaticDimension

In the TableViewCell Class (got this from here) :

func textViewDidChange(textView: UITextView) {

    var frame : CGRect = textView.frame
    frame.size.height = textView.contentSize.height
    textView.frame = frame
}

When the user types beyond the set width of the UITextView, the UITableView increases height from 44 to about 100 and the UITextView doesn't increase in height. I have the constraints set up so that the UITextView's height is equal to that of the UITableViewCell.

Any ideas why this is happening and how to correctly dynamically change the UITextView and UITableView's heights?

2

2 Answers

15
votes

My answer is based on what we exactly use in production of our social app Impether, since you asked me on Twitter that you used the app and you saw expanding UITextView there.

First of all, we have a custom UITableViewCell based class containing the UITextView, which will be expanded (this class has a corresponding xib file also, which you can design on your own):

class MultiLineTextInputTableViewCell: UITableViewCell {

    //our cell has also a title, but you
    //can get rid of it
    @IBOutlet weak var titleLabel: UILabel!
    //UITextView we want to expand
    @IBOutlet weak 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 initialize the height of the text view
    var textString: String {
        get {
            return textView?.text ?? ""
        }
        set {
            if let textView = textView {
                textView.text = newValue
                textView.delegate?.textViewDidChange?(textView)
            }
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()        
        // Disable scrolling inside the text view so we enlarge to fitted size
        textView?.scrollEnabled = false        
    }

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

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

Having a custom cell defined, you can use it in a UITableViewController based class like that:

class YourTableViewController: UITableViewController {

    //in case where you want to have
    //multiple expanding text views
    var activeTextView: UITextView?

    override func viewDidLoad() {
        super.viewDidLoad()

        //registering nib for a cell to reuse
        tableView.registerNib(
            UINib(nibName: "MultiLineTextInputTableViewCell", bundle: nil),
            forCellReuseIdentifier: "MultiLineTextInputTableViewCell")

    }

    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        //hide keyboard when view controller disappeared
        if let textView = activeTextView {
            textView.resignFirstResponder()
        }
    }

    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        //put your value here
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //put your value here
        return 2
    }

    override func tableView(tableView: UITableView,
                            cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let row = indexPath.row
        let cell = tableView.dequeueReusableCellWithIdentifier(
            "MultiLineTextInputTableViewCell",
            forIndexPath: indexPath) as! MultiLineTextInputTableViewCell

        let titleText = "Title label for your cell"
        let textValue = "Text value you want for your text view"

        cell.titleLabel.text = titleText
        cell.textView.text = textValue

        //store row of a cell as a tag, so you can know
        //which row to reload when the text view is expanded
        cell.textView.tag = row
        cell.textView.delegate = self

        return cell

    }

    override func tableView(tableView: UITableView,
                            estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        //standard row height
        return 44.0
    }

    override func tableView(tableView: UITableView,
                            heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
    }

    // Override to support conditional editing of the table view.
    override func tableView(tableView: UITableView,
                            canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        return true
    }
}

//extension containing method responsible for expanding text view
extension YourTableViewController: UITextViewDelegate {

    func textViewDidEndEditing(textView: UITextView) {
        let value = textView.text
        //you can do something here when editing is ended
    }

    func textView(textView: UITextView, shouldChangeTextInRange range: NSRange,
                  replacementText text: String) -> Bool {
        //if you hit "Enter" you resign first responder
        //and don't put this character into text view text
        if text == "\n" {
            textView.resignFirstResponder()
            return false
        }
        return true
    }

    func textViewDidBeginEditing(textView: UITextView) {
        activeTextView = textView
    }

    //this actually resize a text view
    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)

            let thisIndexPath = NSIndexPath(forRow: textView.tag, inSection: 0)
            tableView?.scrollToRowAtIndexPath(thisIndexPath,
                                              atScrollPosition: .Bottom,
                                              animated: false)
        }
    }
} 
1
votes

First make sure your auto layout constraint doesn't conflict with frame you set. (If everything is OK but still doesn't work) Then try changing frame to bounds. A view's frame (CGRect) is the position of its rectangle in the superview's coordinate system. Using frame may cause strange problem sometimes in my experience.

func textViewDidChange(textView: UITextView) {

    var frame : CGRect = textView.bounds
    frame.size.height = textView.contentSize.height
    textView.bounds = frame
}