问题
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 :
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:
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:
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 !
回答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(CGPointMake(0, scrollTo), animated: false)
UIView.setAnimationsEnabled(true) // Re-enable animations.
}
回答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)
}
回答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.
回答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
回答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.
回答6:
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];
}
回答7:
Inspired by the two previous answers, I found a way to solve my problem. I think the fact that I had a UITextView was causing some troubles with autoLayout. I added the following two functions to my original code.
- (CGFloat)textViewHeightForAttributedText: (NSAttributedString*)text andWidth: (CGFloat)width {
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *font = [UIFont systemFontOfSize:14.0];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.sampleStrings[indexPath.row] attributes:attrsDictionary];
return [self textViewHeightForAttributedText:attrString andWidth:CGRectGetWidth(self.tableView.bounds)-31]+20;
}
where in the last line 31
is the sum of my constraints to the left and right sides of the cell and 20
is just some arbitrary slack.
I found this solution while reading this this very interesting answer.
回答8:
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
回答9:
The solution almost everyone suggested is the way to go, I will add only a minor improvement. As a recap:
Simply set the estimated height, I do it via storyboard:
Make sure you have the constraints for the UITextView correctly set within the cell.
- In the
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
I simply call:
cell.myTextView.sizeToFit()
回答10:
Check out the Objective C solution I have provided in the following link below. Simple to implement, clean, and no need for auto layout. No constraints needed. Tested in iOS10 and iOS11.
Resize and move UITableViewCell smoothly without dismissing keyboard
来源:https://stackoverflow.com/questions/31595524/resize-uitableviewcell-containing-uitextview-upon-typing