Resize UITableViewCell containing UITextView upon typing

21,014

Solution 1

Here is a swift solution that is working fine for me. Provided you are using auto layout, you need assign a value to estimatedRowHeight and then return UITableViewAutomaticDimension for the row height. Finally do something similar to below in the text view delegate.

override func viewDidLoad() {
    super.viewDidLoad()
    self.tableView.estimatedRowHeight = 44.0
}

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

// MARK: UITextViewDelegate
func textViewDidChange(textView: UITextView) {

    // Calculate if the text view will change height, then only force 
    // the table to update if it does.  Also disable animations to
    // prevent "jankiness".

    let startHeight = textView.frame.size.height
    let calcHeight = textView.sizeThatFits(textView.frame.size).height  //iOS 8+ only

    if startHeight != calcHeight {

        UIView.setAnimationsEnabled(false) // Disable animations
        self.tableView.beginUpdates()
        self.tableView.endUpdates()

        // Might need to insert additional stuff here if scrolls
        // table in an unexpected way.  This scrolls to the bottom
        // of the table. (Though you might need something more
        // complicated if editing in the middle.)

        let scrollTo = self.tableView.contentSize.height - self.tableView.frame.size.height
        self.tableView.setContentOffset(CGPoint(x: 0, y: scrollTo), animated: false)

        UIView.setAnimationsEnabled(true)  // Re-enable animations.
}

Solution 2

My solution is similar to @atlwx but a bit shorter. Tested with static table. UIView.setAnimationsEnabled(false) is needed to prevent cell's contents "jumping" while table updates that cell's height

override func viewDidLoad() {
    super.viewDidLoad()
    self.tableView.estimatedRowHeight = 44.0
}

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

func textViewDidChange(_ textView: UITextView) {
    UIView.setAnimationsEnabled(false)
    textView.sizeToFit()
    self.tableView.beginUpdates()
    self.tableView.endUpdates()
    UIView.setAnimationsEnabled(true)
}

Solution 3

Tested on iOS 12

I really tried a lot of solutions and finally found a good one here

This works with animation and looks beautiful. The trick was the DispatchQueue.async block. I also used TPKeyboardAvoidingTableView to make sure the keyboard doesn't overlap anything.

func textViewDidChange(_ textView: UITextView) {
    // Animated height update
    DispatchQueue.main.async {
        self.tableView?.beginUpdates()
        self.tableView?.endUpdates()
    }        
}

UPDATE

I got strange jumping issues because of TPKeyboardAvoidingTableView. Especially when I scrolled to the bottom and then a UITextView got active. So I replaced TPKeyboardAvoidingTableView by native UITableView and handle the insets myself. The table view is does the scrolling natively.

Solution 4

The following example works for dynamic row height as the user types text into the cell. Even if you use auto layout you still have to implement the heightForRowAtIndexPath method. For this example to work constraints must be set to textView in such a way that if cell height increases textView will also grow in height. This can be achieved by adding a top constraint and bottom constraint from textView to cell content view. But do not set height constraint for textView itself. Also enable scrolling for the textView so that textView's content size will be updated as the user enters text. Then we use this content size to calculate the new row height. As long as the row height is long enough to vertically stretch the textView to equal to or greater than its content size the text view will not scroll even if scroll is enabled and that is what you need I believe.

In this example I have only a single row and I use only a single variable to keep track of the row height. But when we have multiple rows we need a variable for each row otherwise all the rows will have the same height. An array of rowHeight that corresponds to the tableView data source array may be used in that case.

@interface ViewController ()

@property (nonatomic, assign)CGFloat rowHeight;;
@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.rowHeight = 60;
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell1"];
    return cell;
}

#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return self.rowHeight;
}


#pragma mark - UITextViewDelegate

- (void)textViewDidChange:(UITextView *)textView {
    [self.tableView beginUpdates];
    CGFloat paddingForTextView = 40; //Padding varies depending on your cell design
    self.rowHeight = textView.contentSize.height + paddingForTextView;
    [self.tableView endUpdates];
}

@end

Solution 5

Using Swift 2.2 (earlier versions would likely work too), if you set the TableView to use auto dimensions (assuming you're working in a subclassed UITableViewController, like so:

    self.tableView.rowHeight = UITableViewAutomaticDimension
    self.tableView.estimatedRowHeight = 50 // or something

You just need to implement the delegate in this file, UITextViewDelegate, and add the below function, and it should work. Just remember to set your textView's delegate to self (so, perhaps after you've dequeued the cell, cell.myTextView.delegate = self)

func textViewDidChange(textView: UITextView) {
    self.tableView.beginUpdates()
    textView.frame = CGRectMake(textView.frame.minX, textView.frame.minY, textView.frame.width, textView.contentSize.height + 40)
    self.tableView.endUpdates()
}

Thanks to "Jose Tomy Joseph" for inspiring (enabling, really) this answer.

Share:
21,014
vib
Author by

vib

Updated on August 05, 2022

Comments

  • vib
    vib almost 2 years

    I have an UITableViewController that contains a custom cell. Each cell was created using a nib and contains a single non-scrollable UITextView. I have added constraints inside each cell so that the cell adapts its height to the content of the UITextView. So initially my controller looks like this :

    initial state

    Now I want that when the user types something in a cell its content automatically adapts. This question has been asked many times, see in particular this or the second answer here. I have thus written the following delegate in my code :

    - (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text {
        [self.tableView beginUpdates];
        [self.tableView endUpdates];
        return YES;
    }
    

    However it leads to the following strange behavior : all constraints are ignored and all cells height collapse to the minimal value. See the picture below:

    wrong behavior

    If I scroll down and up the tableView in order to force for a new call of cellForRowAtIndexPath, I recover the correct heights for the cells:

    correct behavior

    Note that I did not implement heightForRowAtIndexPath as I expect autoLayout to take care of this.

    Could someone tell me what I did wrong or help me out here ? Thank you very much !