Reputation: 1926
I have a list of data that I'm pulling from a web service. I refresh the data and I want to insert the data in the table view above the current data, but I want to keep my current scroll position in the tableview.
Right now I accomplish this by inserting a section above my current section, but it actually inserts, scrolls up, and then I have to manually scroll down. I tried disabling scrolling on the table before this, but that didn't work either.
This looks choppy and seems hacky. What is a better way to do this?
[tableView beginUpdates];
[tableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
NSUInteger iContentOffset = 200; //height of inserted rows
[tableView setContentOffset:CGPointMake(0, iContentOffset)];
Upvotes: 17
Views: 22680
Reputation: 7922
None of the answers here really worked for me, so I came up with my solution. The idea is that when you pull down to refresh a table view (or load it asynchronously with new data) the new cells should silently come and sit on top of the tableview without disturbing the user's current offset. So here goes a solution that works (pretty much, with a caveat)
var indexpathsToReload: [IndexPath] = [] //this should contain the new indexPaths
var height: CGFloat = 0.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
UIView.performWithoutAnimation {
self.tableview.reloadSections(NSIndexSet(index: 0) as IndexSet, with: .none)
self.tableview.layoutIfNeeded()
indexpathsToReload.forEach({ (idx) in
height += self.feed.rectForRow(at: idx).height
})
let afterContentOffset = self.tableview.contentOffset
let newContentOffset = CGPoint(x: afterContentOffset.x, y: afterContentOffset.y + height)
self.tableview.setContentOffset(newContentOffset, animated: false)
}
}
CAVEAT (WARNING)
This technique will not work if your tableview is not "full" i.e. it only has a couple of cells in it. In that case you would need to also increase the contentSize
of the tableview along with the contentOffset
. I will update this answer once I figure that one out.
EXPLANATION:
Basically, we need to set the contentOffset
of the tableView
to a position where it was before the reload. To do this we simply calculate the total height of all the new cells that were added using a pre populated indexPath
array (can be prepared when you obtain the new data and add them to the datasource), these are the indexPaths
for the new cells. We then use the total height of all these new cell using rectForRow(at: indexPath)
, and set that as the y
position of the contentOffset
of the tableView
after the reload. The DispatchQueue.main.asyncAfter
is not necessary but I put it there because I just need to give the tableview some time to bounce back to it's original position since I am doing it on a "pull to refresh" way. Also note that in my case the afterContentOffset.y
value is always 0
so I could have hard coded 0
there instead.
Upvotes: 1
Reputation: 17132
For further spectators looking for a Swift 3+ solution:
You need to save the current offset of the UITableView
, then reload and then set the offset back on the UITableView
.
func reloadTableView(_ tableView: UITableView) {
let contentOffset = tableView.contentOffset
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(contentOffset, animated: false)
}
Called by: reloadTableView(self.tableView)
Upvotes: 2
Reputation: 363
I am using self sizing cells and the estimated row height was pretty useless because the cells can vary significantly in size. So calculating the contentOffset wasn't working for me.
The solution that I ended up with was quite simple and works perfectly. So first up I should mention that I have some helper methods that allow me to get the data element for an index path, and the opposite - the index path for a data element.
-(void) loadMoreElements:(UIRefreshControl *) refreshControl {
NSIndexPath *topIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]
id topElement = [myModel elementAtIndexPath:topIndexPath];
// Somewhere here you'll need to tell your model to get more data
[self.tableView reloadData];
NSIndexPath *indexPath = [myModel indexPathForElement:topElement];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[refreshControl endRefreshing];
}
Upvotes: 1
Reputation: 93
Just call setContentOffset
before endUpdates
, that works for me.
[tableView setContentOffset:CGPointMake(0, iContentOffset)];
[tableView endUpdates];
Upvotes: 1
Reputation: 2287
If I understand your mission correctly,
I did it in this way:
if(self.tableView.contentOffset.y > ONE_OR_X_ROWS_HEIGHT_YOUDECIDE
{
self.delayOffset = self.tableView.contentOffset;
self.delayOffset = CGPointMake(self.delayOffset.x, self.delayOffset.y+ insertedRowCount * ONE_ROW_HEIGHT);
[self.tableView reloadData];
[self.tableView setContentOffset:self.delayOffset animated:NO];
}else
{
[self.tableView insertRowsAtIndexPath:indexPathArray WithRowAnimation:UITableViewRowAnimationTop];
}
With this code, If user is in the middle of the table and not the top, the uitableview will reload the new rows without animation and no scrolling. If user is on the top of the table, he will see row insert animation.
Just pay attention in the code, I'm assuming the row's height are equal, if not , just calculate the height of all the new rows you are going to insert. Hope that helps.
Upvotes: 17
Reputation: 1926
The best way I found to get my desired behavior is to not animate the insertion at all. The animations were causing the choppyness.
Instead I am calling:
[tableView reloadData];
// set the content offset to the height of inserted rows
// (2 rows * 44 points = 88 in this example)
[tableView setContentOffset:CGPointMake(0, 88)];
This makes the reload appear at the same time as the content offset change.
Upvotes: 8