18

Currently I have a UITableView with a resizing UITextView in it. The cell is resizing automatically using beginUpdates/endUpdates, but when it does it the table view stutters (See the gif below).

The end result is a UITableViewCell that has a textview in it that resizes based on it's content. Here is the code within the custom UITableViewCell class that causes the UITableView to update itself.

- (void)textViewDidChange:(UITextView *)textView {
    // This is a category on UITableViewCell to get the [self superView] as the UITableView
    UITableView *tableView = [self tableView];
    if (tableView){
        [tableView beginUpdates];
        [tableView endUpdates];
    }
}

Here are the things that I have already tried:

Get the current contentOffset and resetting it after the endUpdates but didn't work Disabling scrolling on the UITableView before updates and then enabling afterwards I tried returning NO always from - (BOOL)textViewShouldEndEditing:(UITextView *)textView My UITableView cell height is using UITableViewAutomaticDimension. Any other ideas or thoughts are welcome.

Here is a sample of what it looks like:

Table View Stutter

I am not looking to use any libraries so please no suggestions for that.

Thanks

Edit: Solution

Found Here: I do not want animation in the begin updates, end updates block for uitableview?

Credit to @JeffBowen for a great find (although hacky it is workable and allows me to still implement the UITableViewDelegate methods for supporting iOS 7). Turn animations off prior to performing update and then enable after update to prevent the UITableView from stuttering.

[UIView setAnimationsEnabled:NO];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled:YES];

If you don't need to use the Delegate methods and want a less hacky solution for iOS 8+ only then go with @Massmaker's answer below.

Community
  • 1
  • 1
DMCApps
  • 2,110
  • 4
  • 20
  • 36
  • Gotcha... so the table is shifting back down each time you update it... Is that the problem? – Lyndsey Scott Dec 13 '14 at 15:59
  • But you have nothing between beginUpdates and endUpdates... You're not inserting, deleting or selecting as beginUpdates and endUpdates are intended for. – Lyndsey Scott Dec 13 '14 at 16:01
  • What happens if you change those lines to `[tableView reloadData];` same issue? – Lyndsey Scott Dec 13 '14 at 16:02
  • reloadData will not work as that will cause the TextView to lose focus. begin/end updates allows the cell to update it's height while maintaining focus. It seems like there is something with the offset causing the tableview to not realize there is a keyboard and resetting to it's offset without the keyboard. – DMCApps Dec 13 '14 at 16:04
  • But you haven't specified a cell to update... – Lyndsey Scott Dec 13 '14 at 16:05
  • Yes but calling begin/end updates causes the table to recalculate height without dequeing all the cells again which is what I need to do – DMCApps Dec 13 '14 at 16:06
  • Oh, yes. Good point. I see what you're doing now. I'll look over your code again... one sec... – Lyndsey Scott Dec 13 '14 at 16:07
  • @LyndseyScott one thing to note. If the tableview has enough data in it that there the tableview fills the full viewport (you can see the gap at the start of the gif) then the tableview acts as expected (the textview stays in focus and does not stutter on the begin/end updates) – DMCApps Dec 13 '14 at 16:10
  • @DMCApps please check my last update - I found solution, which works – Vitalii Gozhenko Dec 22 '14 at 22:27
  • I filed a Radar on this, as it could be a bug - http://openradar.appspot.com/radar?id=6381017677955072 – Joshua Dance Mar 10 '15 at 16:42

4 Answers4

4

Just disable animation before calling beginUpdates and re-enable it after calling endUpdates.

[UIView setAnimationsEnabled:NO];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled:YES];

Definitely a hack but works for now. Credit to my friend Beau who pointed me to this.

Community
  • 1
  • 1
Jeff Bowen
  • 5,904
  • 1
  • 28
  • 41
1

My solution (for iOS 8) was first set in my viewController viewDidLoad

self.tableView.rowHeight = UITableViewAutomaticDimension;
// this line is needed to cell`s textview change cause resize the tableview`s cell
self.tableView.estimatedRowHeight = 50.0;

then, combining this article solution in Swift and some dirty thoughts I`ve set in my cell a property, called

@property (nonatomic, strong) UITableView *hostTableView;

and in cell-s -(void) textViewDidChange:(UITextView *)textView

 CGFloat currentTextViewHeight = _textContainer.bounds.size.height;
CGFloat toConstant = ceilf([_textContainer sizeThatFits:CGSizeMake(_textContainer.frame.size.width, FLT_MAX)].height);
if (toConstant  >  currentTextViewHeight)
{
    [_hostTableView beginUpdates];
    [_hostTableView endUpdates];
}

then in viewController

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

textCell.hostTableView = self.tableView;

after height change

Massmaker
  • 698
  • 5
  • 7
  • I am doing pretty much what you are describing. Minor differences. I use the tableview's delegate functions `- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath` and same for the estimate `- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath`. Instead of assigning the tableview to the cell I just grab it from the cell as it's parent view using a category. What happens when your resizing textview is farther down in the list (i.e. add 10 rows above then retry the resizing row as the 11th element)? – DMCApps Feb 25 '15 at 19:11
  • The purpose was to NOT to use theese methods because of iOS 8 cell`s auto height, depending on contents. Otherwise I`d use traditional methods mentioned by you. Thanks for your comment). – Massmaker Feb 27 '15 at 09:47
  • crazy I removed those method calls and it worked (Do you have any explanation as to why. It seems to me that it shouldn't make a different if you declare it on the tableview vs in the delegate methods)! My only concern with this is that sometimes I need my rows to actually return a specific height (i.e. supporting iOS7) ... Do you know of anything I can do to allow that? – DMCApps Feb 27 '15 at 14:00
  • Once again. You will need those methods if you support iOS prior to 8. In iOS 8 Apple has made some changes to cells displaying mechanism, which involves autolayout and dependencies of cell's labels or textViews or other content that can have intrinsic size. In other words, now the cell has more powers than hosting table view, and not vice versa. – Massmaker Feb 27 '15 at 19:12
  • @DMCApps, I have the same problem and this solution doesn't work for me. I set `rowHeight` and `estimatedRowHeight` the same as @Massmaker and don't use the `tableView:heightForRowAtIndexPath:` or `tableView:estimatedHeightForRowAtIndexPath:` methods. Do you have any more insights into what specifically fixed this for you? – Jeff Bowen Mar 06 '15 at 23:49
  • @JeffBowen Unfortunately I don't have anymore insight then this. I basically commented out my tableView delegate methods and then added in the `viewDidLoad` my `self.tableView.rowHeight`/`estimatedRowHeight` with appropriate values (`UITableViewAutomaticDimensions`/`50.0f`) and the stuttering stopped. – DMCApps Mar 10 '15 at 16:07
  • Glad you got it working, @DMCApps. Not sure why the same steps didn't do it for me. A friend of mine suggested a hack that worked for me though. Just disable animation before calling `beginUpdates` and re-enable it after calling `endUpdates` (http://stackoverflow.com/a/9310886/426839). Definitely a hack but works for now. – Jeff Bowen Mar 10 '15 at 21:17
  • @JeffBowen Nice Find. For my case I actually like that solution better (As much as it is a hack for something that seems to be an OS issue hopefully fixed in the near future). At least it allows me to still implement the `UITableViewDelegate` methods on the tableview which will allow me to back support to iOS 7 as originally intended (where `UITableViewAutomaticDimensions` is not supported). Thanks a lot for the information! I will update my question with that solution incase the link you provided goes dead at a later time. – DMCApps Mar 11 '15 at 13:31
  • I was talking about automatic dimension and estimated row height on the very begining of my answer))) – Massmaker Mar 11 '15 at 14:20
  • @DMCApps Glad it helped. I added it as an answer so it will be more discoverable for others. – Jeff Bowen Mar 11 '15 at 23:52
  • 1
    You might want to make the `hostTableView` weak instead of strong. Won't this lead to a retain cycle? – John Gibb Sep 09 '15 at 12:50
  • Right you are. Even if no retain cycle will happen, we still would be better doing `weak` TableView property on the cell. Or, better, post a notification, that cell wants to resize, and in view controller or other datasource refresh cell with `beginUpdates` - `endUpdates` messages. This way we will not need a `tableVIew` property on the target cell. – Massmaker Sep 10 '15 at 07:24
  • **If it's still fluttering** after applying this hack - consider rounding up the currentTextViewHeight value, too. In my case it was 0.5 lower than toConstant, but only sometimes (I think alternating rows) – Sebastian Apr 23 '16 at 09:18
0

I don't find a way to achieve it because: When you trigger [tableView endUpdates], table recalculate contentSize and re-set it. And this cause resetting contentOffset to default value.

This is behaviour inherited from UIScrollView, and I tried to avoid it via:

  • Subclassing UITableView and overriding setContentOffset: and setContentOffset:animated functions. But they don't called when table view change contentSize.
  • Subclassing UITableView, overriding setContentSize: function and setting contentOffset to old value, after content size updating, but it not work for me
  • using KVO, and setting old value for contentOffset right after it reset, but anyway I have this animated issue
  • setting scrollEnabled to NO and scrollToTop to NO, but it also not help

If anybody find solution for this problem welcome. Maybe possible solution - disable autolayout: iOS: setContentSize is causing my entire UIScrollView frame to jump

UPDATE: I find solution: direct changing cell height, content size and content offset: This works for me (table view is delegate of cell's UITextView)

- (void)textViewDidChange:(UITextView *)textView
{
    CGFloat textHeight = [textView sizeThatFits:CGSizeMake(self.width, MAXFLOAT)].height;
    if (self.previousTextHeight != textHeight && self.previousTextHeight > 0) {
        CGFloat difference = textHeight - self.previousTextHeight;
        CGRect cellFrame = self.editedCell.frame;
        cellFrame.size.height += difference;
        self.editedCell.frame = cellFrame;
        self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height + difference);
        self.contentOffset = CGPointMake(self.contentOffset.x, self.contentOffset.y + difference);
    }
    self.editedNote.comments = textView.text;
}
Community
  • 1
  • 1
Vitalii Gozhenko
  • 9,220
  • 2
  • 48
  • 66
  • I am using iOS 8.0+ for the versioning so for the table height I am using UITableViewAutomaticDimension and hence do not need to recalculate. I did however add in - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath and returned an arbitrary row height, the table still stutters like in the gif EXCEPT that the textview comes completely into view (above the keyboard) instead of staying behind the keyboard so I am going to play with that more tomorrow. – DMCApps Dec 14 '14 at 05:02
  • I've checked, and looks like we can't achieve it due to UIScrollView restrictions. Look at the updated answer – Vitalii Gozhenko Dec 14 '14 at 11:18
  • would something along the lines of this work? I don't quite understand how to get the 'verticalGap' value that they are explaining to try and use. http://stackoverflow.com/questions/18775294/animate-height-change-for-uitableviewcell-without-scrolling – DMCApps Dec 14 '14 at 14:47
  • in this case you can't scroll in the table while keyboard is shown – Vitalii Gozhenko Dec 14 '14 at 22:41
  • what is self in this case? The UITableViewCell? I have a custom cell class that handles the textViewDidChange event (vs within the UITableViewController class. How do I alter this solution to work within that? Also with this solution I do not need to begin/end update on the table view to animate the cell height change? – DMCApps Dec 30 '14 at 19:57
  • self is `UITableView` in this case, I access to inherited UIScrollView properties contentOffset and contentSize and increase current cell's height manually – Vitalii Gozhenko Dec 30 '14 at 20:12
  • 1
    height not an assignable value on `UITableViewCell` – DMCApps Dec 31 '14 at 03:59
0

In Textview value change delegate, use this code to resize particular cell without any flickering and all. But before that, make sure you have used dynamic tableview cell height.

UIView.setAnimationsEnabled(false)
DispatchQueue.main.async {
   self.tableView.beginUpdates()
   self.tableView.endUpdates()
   self.tableView.layer.removeAllAnimations()
}
UIView.setAnimationsEnabled(true)
Balaji G
  • 77
  • 5