Resize UITableViewCell containing UITextView upon typing
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.
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
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.
}
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)
}