Auto-expanding Text Input within UITableViewCell in iOS

Dynamically expanding view has never been easy in iOS. As we have seen in early versions of iOS, if we need an UILabel that calculates it's height from it's content height, we could do that but needed some manual height calculation based on content text, font, UILabel width etc. Then we used to make it grow by calling sizeToFit method.

For text input it was never easy. If we use UITextField, we know it can't take more than a single line input. For multiline input, we use UITextView and make it editable. But problem is, we have to set it's height or a bounding constraint. If input content height exceeds that UITextView height, then It does not expand, instead the content becomes scrollable within that height. Dynamic height calculation for UITableViewCell has been more complicated. But after iOS 8 with auto-layout, life became easier because Apple finally had a look on it.

Well, here requirement is a bit different. We need a text input View that grows/shrinks automatically with it's change of content height. Here's how to do it.

First of all, lets create a Xcode project and add a new subclass of UITableViewCell. Say, our SubClass name is QACell. So, let's open QACell.xib and Add a UITextView or any subclass of UITextView according to your requirements.

Let's add bounding constraints at top,bottom,left,right to the UITextView so that It's height can grow/shrinks when It's parent i,e UITableViewCell's height changes. So, It should look like this after we add the constraints.

Now open the Attribute Inspector of UITextView and disable Scrolling and Bounces. We have to disable Scrolling because when scrolling is enabled, frame of UITextView is independent of it's content size. But when disabled, there's relationship between them. And I recommend to disable bounces because we need to update UITableViewCell on every change happening to UITextView, when bounce is enabled and TextView height gets changed we'll have unexpected bounces that we don't want here.

Now, let's open QACell.swift and add an IBOutLet for the UITextView we are using. Then comes the tricky part. We need to observe content change in our UITextView, that means when user types in our UITextView instance we need to know. We know how to do it, right ? We need to implement the UITextViewDelegate protocol and override the textViewDidChange(textView: UITextView) method. At this stage we know when user types something in it through the delegate.

As we can observe the change, when a change takes place, We have to inform our UITableView to update the height of the current UITableViewCell instance. But it's not just reloading the cell as we normally do, because If we reload a cell then, after each character typed, cell will be reloaded and focus will be lost from current UITextView instance. So, we'll not be able to type properly. And also It's definitely a costly operation to update Cell so many times. So, lets solve the problem.

As we need to pass the information of input change to our UITableView, We'll do it through a delegate. So, let's write a protocol and a delegate method in it.

protocol ExpandingCellDelegate {  
    func updateCellHeight (indexPath: NSIndexPath, comment:String)
}

Now, when content of UITextView instance changes, we'll call the delegate method. Thus, the class (Our ViewController) which implements this protocol gets the message that it needs to update that particular UITableViewCell instance. It will look like this in our code.

func textViewDidChange(textView: UITextView) {  
        self.delegate.updateCellHeight(self.cellIndexPath, comment: textView.text)
    }

At this stage, we are done making our building blocks. Now we have to put this thing into action. Let's open our ViewController having a UITableView and implement UITableViewDataSource, UITableViewDelegate protocol. And also, implement the ExpandingCellDelegate we have created earlier.

Set necessary data sources and attributes within cellForRowAtIndexPath for the QACell and set the delegate like the following code.

cell.answerTextView.scrollEnabled = false  
cell.headerLabel.text = self.questions[indexPath.row]  
cell.cellIndexPath = indexPath  
cell.delegate = self  

As we have set the delegate and implemented updateCellHeight delegate method, so our ViewController now gets notified when content of UITextView gets changed. So, only thing that we have to do, we need to update or refresh the Cell with the following code.

func updateCellHeight(indexPath: NSIndexPath, comment: String) {  
        self.answers[indexPath.row] = comment
        self.uiTableView.beginUpdates()
        self.uiTableView.endUpdates()
    }

We are not done yet, a small thing is still remaining. Our UITableViewCells must have dynamic height for this method. Before iOS 8.0 it was difficult, but not anymore. Let's add the following 2 lines in ViewDidLoad method and don't implement any Height Delegate for UITableView.

self.uiTableView.estimatedRowHeight = 50  
        self.uiTableView.rowHeight = UITableViewAutomaticDimension

Let's run it and see the result.

Finally, if we have followed the instructions carefully, we should get a Auto-Expanding UITextView within UITableViewCell like the following image.

Sample Code

I have written a simple iOS app that demonstrates everything I have described above.It can be found in this Github Repository.