I try to implement a tableView where rows additional rows are inserted with an animation while the rows are visible. I update the array for the datasource and call:
[self.tableView insertRowsAtIndexPaths:addedIndexPaths withRowAnimation:(UITableViewRowAnimationMiddle)];
I want the tableview to not scroll at all and the rows to be inserted on screen (no over the visible are).
In some section especially the last section of the tableview the cells are inserted the tableview scrolls to completely different section.
I tried different row animation, calling layoutIfNeeded inside of and beginUpdate / endUpdate block, set the correct content offset inside of the beginUpdate / endUpdate block or reload the entire section.
Nothing works. The scrollview always scrolls up.
The cells size them selves using autolayout-constraints. On the top half of the tableview the cells are inserted as I expected.
How can I fix the content Offset while cells are inserted with an animation?
How can I debug that animation?
I faced same issue and after trying a lot found solution that if I remove section header height as UITableViewAutomaticDimension then adding and removing rows from UITableView works perfectly and table view also fixed at same position.
tableView.sectionHeaderHeight = 10;
tableView.estimatedSectionHeaderHeight = UITableViewAutomaticDimension;
remove above 2 lines if added.
tableView.estimatedRowHeight = 15;
tableView.rowHeight = UITableViewAutomaticDimension;*
Row with Automatic Dimension will not create any issues.
But section header view with Automatic Dimension will create issue.
Thanks.
This is a know bug with the UITableViews, it's been there quite a long time and hasn't been fix (by the looks of it, it never will).
The root of this is because your cells are of dynamic sizes and (I'm assuming) you are using the default 'UITableViewAutomaticDimension'. To fix this you need to drop the use of 'UITableViewAutomaticDimension' and calculate the size of each cell. This should help with the bounciness of the table view but probably wont be perfect.
Another way is to rely on CATransaction to lock and keep the table view position after the insert.
It should look something like this (probably, I didn't test it):
CGFloat offset = self.tableView.contentSize.height - self.tableView.contentOffset.y;
[CATransaction begin];
[CATransaction setDisableActions:YES];
[CATransaction setCompletionBlock: ^{
// Code to be executed upon completion
}];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths: indexPaths
withRowAnimation: UITableViewRowAnimationAutomatic];
self.tableView.contentOffset = CGPointMake(0,
self.tableView.contentSize.height - bottomOffset);
[self.tableView endUpdates];
[CATransaction commit];
Related
I'm making a UIViewController to manage a messaging screen. I'm doing this using a UITableView and some custom cells.
To make things simpler, each cell contains:
Its chat "bubble" (a UIView subclass)
Its chat text (a UILabel)
A timestamp header label (which might be hidden)
A bottom footer label (for "Sending...", "Delivered", etc.; also might be hidden)
Because of performance concerns, I am not using auto-sizing of cell heights, but caching cell heights into an NSMutableDictionary.
When the user sends or receives a new message, I want the following to occur:
The current last message cell is reloaded, hiding its bottom label, if needed.
The new last message cell is appended at the bottom of the UITableView.
The UITableView is scrolled so that the new last cell is visible.
I can get it to where the end state of the screen is as desired, but the animations in between are really kinda funky. I have tried a whole lot of different approaches to get the animations to behave. Basically, it seems like some major reloading is happening, even though the only cell that could possibly change its height is the last cell (prior to the insertion of the new cell). Plus, I'd like to have the last cell simply "appear" in place without animation. If it does, it should be off-screen, and then I should be able to animate it on-screen.
Here's my current "user sent a new message" method:
- (IBAction)sendButtonPressed {
//Creation of the new message, into 'message' variable
[self.messages addObject:message];
int thisIndex = (int)self.messages.count - 1;
NSIndexPath *this = [NSIndexPath indexPathForRow:thisIndex inSection:0];
int prevIndex = (int)self.messages.count - 2;
NSIndexPath *prev = [NSIndexPath indexPathForRow:prevIndex inSection:0];
[self removeCachedHeightForIndex:prevIndex];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[prev] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView insertRowsAtIndexPaths:#[this] withRowAnimation:UITableViewRowAnimationTop];
[CATransaction setCompletionBlock:^{
[self scrollToBottomAnimated:YES];
}];
[self.tableView endUpdates];
}
This appears to reload the last several cells in the UITableView. Or, at least, every cell that is visible when this is called seems to be animated in some way. Only the cell at prev is actually changing in any way.
Longterm, I might pull out the header and footer labels into different cells, but is there a way to fix this animation glitch as-is?
I have a tableviewcell, that on tap, grows in size, height wise, by updating the frame.
The problem is, the cells below don't adjust, move down to make it visible underneath, and the select row events are still based on the old sizes, before tap. I am using Facebook POP - which is handling animation, so tableview.beginUpdates() is out of the question, maybe?
You cannot manually update the frame of a UITableViewCell by changing its frame or bounds. Instead, you need to change the value returned by -tableView:heightForRowAtIndexPath: for that indexPath and then perform:
[self.tableView beginUpdates];
[self.tableView endUpdates];
This will cause the tableView to recalculate the heights of all the rows.
First, you need to make sure that you return the new height in heightForRowAtIndexPath, then you need to make the tableview update the cell. If you don't care about animation just call [tableview reloadData]. If you want animation you need to call [tableview reloadRowAtIndexPath: indexpath_of_your_cell]
I have a tableview with dynamically sized cells, and a button that toggles the sort order of these cells. I'd like to scroll to the top every time the sort order is toggled, but when I set the content offset to the top, it seems to only scroll ~90% of the way there.
The offsetting code is simple enough and has served me well on different projects, so I seriously doubt the problem is here:
- (void) scrollToTop
{
CGPoint offset = CGPointMake(0, -self.tableView.contentInset.top);
[self.tableView setContentOffset:offset animated:YES];
}
[self.tableView reloadData]; // Lets update with whatever info we have
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
reloading and then scrolling resolved for me.
Didn't think I would find the answer so soon.
I was using UITableView's tableView:estimatedHeightForRowAtIndexPath: to return my minimum cell height, and it seems that the tableview uses this inside reloadData to create an idea of how big the content is before actually dequeuing the cells and caching their height. Being halfway down the content and reloading the data causes the tableview to think the distance to the top is the (number of cells offscreen above the current visible * the minimum height from estimatedHeightForRow), causing the tableview to only offset itself as if all cells were the minimum height. My solution was just to avoid using the estimated height, since my tableview isn't excessively long anyway. If you do have a large tableview (approaching 1000+ rows) that actually needs to use the estimated values for performance reasons, you might want to find a way to make the estimated values as close to the runtime values as possible, or look into more detailed solutions.
tl;dr - Remove tableView:estimatedHeightForRowAtIndexPath: and just allow the tableView to size itself from heightForRowAtIndexPath
What about something like this instead?
NSIndexPath *start = = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView scrollToRowAtIndexPath:start atScrollPosition:UITableViewScrollPositionTop animated:YES];
None of these worked for me. The solution was to call layoutIfNeeded() before setting the content offset:
tableView.reloadData();
tableView.layoutIfNeeded();
tableView.setContentOffset(CGPoint.Empty, animated: true);
I have a UITableView with two sections. Items can move from one section to another, as seen in the image below.
The basic interaction is as follows:
you swipe a cell/row
that cell is removed from its section
that cell is added to the bottom of the other section
Note, I'm using a sequence of deleteRowsAtIndexPaths and insertRowsAtIndexPaths with CATransactions. Remove followed by add isn't the only table operation in place; for the general approach I want chained insert/delete/update animations to start when the preceding operation has finished. Here's an example:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[table beginUpdates];
// update data source...
// insertRows...
[table endUpdates];
}];
[table beginUpdates];
// update data source...
// deleteRows...
[table endUpdates];
[CATransaction commit];
When there are enough items in the table view to take up more than a screen's worth of space, (what I think is) the default UITableView scrolling kicks in when cells are removed from one section and added to another.
My assumption (as I haven't found any reference to scroll behaviour in the docs) is that the UITableView scrolls to place/keep the inserted row in view.
Consider the example of swiping a cell to remove it from one section and have it added to the other:
the cell is currently in view because the user is swiping it (so perhaps there is no change/scroll required for the deleteRows...
(in some circumstances) the table view scrolls for the subsequent insertRows...; the scroll happens before the insert so that the animated insert is observed
For example, in the image below, swipe one of the cells in the bottom section, table view scrolls, then inserts at the bottom of the top section...
I say some circumstances because this behaviour is observed if the insert takes place in the top section (section 0). When the delete / insert is from section 0 to section 1, the table view does not scroll at all.
I'm also seeing some other behaviour I don't understand. In the case where some of the cells have more than one line of text (cells aren't all the same size) - the scroll seems to "not work". At first the scroll amount simply appeared wrong; however, testing with all the cells being single line revealed consistent scrolling when the insert occurred in section 0.
The following image shows the wrong scrolling:
I have two specific questions and one general question.
Specific question #1: is there a hierarchy for UITableViews such that section 0 is preferred, so the table view will scroll to section 0 so that inserts are observed, but not scroll to other sections?
Specific question #2: is it the height of the cells in the above example that is causing the wrong scrolling to be observed?
This is the code I'm using for calculating the height of the table view cells:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger index = indexPath.row;
ParsedItem* item;
if (indexPath.section == 0) {
item = [_parsedItemList.toDoList objectAtIndex:index];
}
else {
item = [_parsedItemList.doneList objectAtIndex:index];
}
NSAttributedString* attrString = [[NSAttributedString alloc] initWithString:item.displayText attributes:_tableViewTextAttrNormal];
CGFloat height = [self textViewHeightForAttributedText:attrString andWidth:300.00];
return fmaxf(50.0, height);
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0;
}
- (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;
}
General question: as I mentioned above, I haven't found anything in the docs that describes how the scrolling is supposed to work. Is there a general rule or principal that I've missed? I don't want to add explicit scroll operations if I'm simply doing it wrong...
Here is the code I use:
//inserting a row at the bottom first
_numberOfRecords++;
[_tableView beginUpdates];
[_tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:_numberOfRecords-1 inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
[_tableView endUpdates];
//clear text
_inputField.text = #"";
//then scroll to bottom
CGPoint bottomOffset = CGPointMake(0, _tableView.contentSize.height + 44.0 + _tableView.contentInset.top - _tableView.bounds.size.height);
NSLog(#"%f", _tableView.contentSize.height + 44.0 + _tableView.contentInset.top - _tableView.bounds.size.height);
[_tableView setContentOffset:bottomOffset animated:YES];
This would scroll the tableview in a very strange way.
But if I put the scrolling code BEFORE the insertion, it works fine except that it ignores the latest inserted row. That is, it scrolls to the second last row instead of scrolling to the very last row (of course, because it scrolls before inserting a new roll.)
So I believe this code has no problem of the position where it should scroll to.
The problem probably comes from row insertion to tableview.
It violates the animation of scrolling the table view.
I am doing this to make a chatting view.
Each time the user sends or receives a message, I insert a row containing the message to a table view, and scrolls it to the bottom. That's why I use tableView here. I tried to use scrollView with label, it works fine, but tableView seems more popular in a chatting view.
I was thinking to use scrollView or tableView, and I found the built-in message app of Apple is using a tableView, so I adopt tableView. Let me know if a scrollView with Label is better than a tableView.
Anyway, how can I scroll a tableView to the bottom after inserting a new row?
Try using UITableView's scrollToRowAtIndexPath::
[self.tableView scrollToRowAtIndexPath: atScrollPosition: animated:];
This is my own solution:
[_tableView reloadData];
//scroll to bottom
double y = _tableView.contentSize.height - _tableView.bounds.size.height;
CGPoint bottomOffset = CGPointMake(0, y);
NSLog(#"after = %f", y);
if (y > -_tableView.contentInset.top)
[_tableView setContentOffset:bottomOffset animated:YES];
Firstly reloadData after endUpdates. This ensures the tableView contentSize is updated after inserting a new row. Then check if the scrolling distance is greater than the contentInset.top (this is for avoiding the tableview hiding behind the status bar and navigation bar) then to scroll down, otherwise not to scroll because of some weird animation.
Alternatively, you can simply use
[self.tableView scrollToRowAtIndexPath: inSection: atScrollPosition: animated:];
to scroll to the row you want. But this doesn't handle cells with sections and footers very well. For plain tableViewCell, you can just use this to do the magic. Otherwise you may find my trick solution performs better.
Anyway, thanks for all your answers.