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 insi
I've implemented a similar approach using a UITextView however to do so I had to implement heightForRowAtIndexPath
#pragma mark - SizingCell
- (USNTextViewTableViewCell *)sizingCell
{
if (!_sizingCell)
{
_sizingCell = [[USNTextViewTableViewCell alloc] initWithFrame:CGRectMake(0.0f,
0.0f,
self.tableView.frame.size.width,
0.0f)];
}
return _sizingCell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
self.sizingCell.textView.text = self.profileUpdate.bio;
[self.sizingCell setNeedsUpdateConstraints];
[self.sizingCell updateConstraintsIfNeeded];
[self.sizingCell setNeedsLayout];
[self.sizingCell layoutIfNeeded];
CGSize cellSize = [self.sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return cellSize.height;
}
sizingCell is an instance of the cell that is only used for sizing calculations.
What's important to note is that you need to attach the UITextView's upper and lower edge to the UITableViewCells contentView's upper and lower edge so that as the UITableViewCell changes in height the UITextView also changes in height.
For constraint layout I use a PureLayout (https://github.com/smileyborg/PureLayout) so the following constraint layout code may be unusual for you:
#pragma mark - Init
- (id)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style
reuseIdentifier:reuseIdentifier];
if (self)
{
[self.contentView addSubview:self.textView];
}
return self;
}
#pragma mark - AutoLayout
- (void)updateConstraints
{
[super updateConstraints];
/*-------------*/
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeLeft
withInset:10.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop
withInset:5.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom
withInset:5.0f];
[self.textView autoSetDimension:ALDimensionWidth
toSize:200.0f];
}
The trick to immediately update the tableview cells height in a smooth way without dismissing the keyboard is to run the following snippet to be called in the textViewDidChange event after you set the size of the textView or other contents you have in the cell:
[tableView beginUpdates];
[tableView endUpdates];
However this will may not be enough. You should also make sure the tableView has enough elasticity to keep the same contentOffset. You get that elasticity by setting the tableView contentInset bottom. I suggest this elasticity value to be at least the maximum distance you need from the bottom of the last cell to the bottom of the tableView. For instance, it could be the height of the keyboard.
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0);
For more details and some useful extra features around this matter please check out the following link: Resize and move UITableViewCell smoothly without dismissing keyboard
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)
}
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(CGPointMake(0, scrollTo), animated: false)
UIView.setAnimationsEnabled(true) // Re-enable animations.
}
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