I have a UITableViewController and when the user scrolls to the top, I want to add a bunch of cells above the first cell without affecting the current scroll position (just like iMessage)
I know I can use insertRowsAtIndexPaths to do this without reloading the data, but I just want to do it the simplest way possible first. So I am overwriting the data and calling reload, then calling setContentOffset, but no matter what I set the offset to, the table always reverts to the top.
// in an NSNotification handler
//...
dispatch_async(dispatch_get_main_queue(), ^{
// get old content height
CGSize oldContentSize = self.tableView.contentSize;
NSLog(#"old content height: %f", oldContentSize.height);
// update tableView data
self.messages = updatedMessages;
[self.tableView reloadData];
// get new content height
CGSize newContentSize = self.tableView.contentSize;
NSLog(#"new content height: %f", newContentSize.height);
// move to user scroll position
CGPoint newContentOffset = CGPointMake(0, newContentSize.height - oldContentSize.height);
[self.tableView setContentOffset:newContentOffset];
});
When I run this I get:
old content height: 557.765625
new content height: 1249.050781
but the tableView reverts to the top instead of maintaining its scroll position.
I found that if I call [self.tableView setContentOffset:newContentOffset animated:YES]; It always scrolls down to the correct position, but the movement is unacceptable.
How can I maintain the scroll position after reloading the tableView data?
I've looked at these:
setContentOffset only works if animated is set to YES
UIScrollView setContentOffset: animated: not working
UITableView contentOffSet is not working properly
but their solutions don't work in my case.
Try this
[self.tableView setContentOffset:newContentOffset animated:NO]
Related
I have a UITableView with a UIView on top. I want the UIView to stick to the top as the tableView cells scroll over it.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (self.tableView.contentOffset.y > 0) {
CGRect newframe = self.publicTopView.frame;
newframe.origin.y = -self.tableView.contentOffset.y;
self.publicTopView.frame = newframe;
NSLog(#"After: %f", self.publicTopView.frame.origin.y);
}
}
You need to set your table view header view to the view you want on top.
Add this code to you viewDidLoad
self.tableView.tableHeaderView = self.publicTopView
I'm not certain what you're trying to accomplish, but I have a guess at what is wrong. As you scroll your contentOffset will continue to change and let's say your tableView has a content size height of 1500, then your contentOffset will eventually be larger than the height of your view controllers view. Now see that you are putting that contentOffset into the origin.y of your publicTopView. So your publicTopView could possibly be moving too much, even offscreen depending on how large your tableview's content size is.
I need to reload tableview, but in somehow I want to keep the scroll position. Any way to do it? Maybe store content offset and in viewDidLayoutSubviews I should reset it?
TableView does not reset scroll position. It maintains content offset even after reloadData function is called. So no need to do anything, if you want to keep pixel position.
If you are changing row height at the time of reloading data or changing datasource then you can use scrollToRowAtIndexPath:atScrollPosition:animated: method.
You can save your current offset before reloading the table use this method to save your current offset
self.tableView.contentOffset.y
Or you can Reload like this it will Reload your table from your current Offset
Try this:
[UIView animateWithDuration:0.1 delay:0 options:0 animations:^{
self.tableView.contentOffset = CGPointMake(0, currentOffset);
} completion:^(BOOL finished) {
[self.tableView reloadData];
}];
After reloading the table view you need to call the TableView's method scrollToRowAtIndexPath:atScrollPosition:animated:
Just provide the index path, scroll position which could be one of the following
UITableViewScrollPositionNone,
UITableViewScrollPositionTop,
UITableViewScrollPositionMiddle,
UITableViewScrollPositionBottom
and YES/ NO in animated parameter.
just call scrollToRowAtIndexPath:atScrollPosition:animated: before you use reloadData (with animation:NO)
Also, be carefull in case you have less cells in the updated tableview to prevent crashes.
Hope it helps
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.
I know how to animate the height change of a UITableViewCell using the method seen here: Can you animate a height change on a UITableViewCell when selected?
However, using that method, the UITableView will scroll at the same time, which I don't want it to do.
I have a UITableView with very few cells; it takes up less than the screen height. The bottom cell has a UITextField and, when it starts editing, I manually set the content offset of the UITableView so that the cell with the UITextField is scrolled to the top. Then, based on the input in the UITextField, I may want to increase the size of the UITableViewCell to show extra options, more or less.
The problem is that, when animating this change, it will reposition the UITableView so that my UITextField is no longer at the top.
This is what I'm doing, more or less:
self.customAmountCellSize = height;
[self.tableView beginUpdates];
[self.tableView endUpdates];
I have also tried
self.customAmountCellSize = height;
CGPoint originalOffset = self.tableView.contentOffset;
[self.tableView beginUpdates];
[self.tableView endUpdates];
[self.tableView setContentOffset:originalOffset animated:NO];
I want the row height animation, I do not want the UITableView to scroll as a result.
Any ideas?
It appears the problem you're encountering is that your table view is scrolled past the bottom so when you update its content it will attempt to fix that.
There are two approaches you could take to prevent scrolling:
Set the table view's content inset to be the height of the initial white space:
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, verticalGap, 0);
Add an empty footer view with the same height as the vertical gap:
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, verticalGap)];
In both cases, you will need to calculate the vertical space you are trying to achieve. You then need to restore the contentInset or tableFooterView to its original state when you are done.
I think the table view is scrolling because your text field is becoming the first responder, and not because of the cell height change. Try keeping the cell height the same and just adjusting the offset to be sure.
If I am correct, than here's the solution: UITableView automatically tries to scroll when your keyboard appears. To fix this, set the content offset to your desired offset in a dispatch to the main queue, which will fire at the beginning of the next runloop. Put the following code inside your response to a UIKeyboardWillShowNotification, or in a UITextFieldDelegate shouldBeginEditing method:
// Get the current offset prior to the keyboard animation
CGPoint currentOffset = self.tableView.contentOffset;
UIEdgeInsets currentInsets = self.tableView.contentInset;
__weak SomeTableViewControllerClass *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animationWithDuration:0 animations:{
// set the content offset back to what it was
weakSelf.tableView.contentOffset = currentOffset;
weakSelf.tableView.contentInset = currentInsets;
} completion:nil];
});
Similar fixes are sometimes necessary for the contentInset.bottom of a table view, depending on the frame of your UITableView and other factors.
I have a horizontal UITableView where I would like to set the paging distance. I tried this approach,
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
_tableView.decelerationRate = UIScrollViewDecelerationRateFast;
CGFloat pageSize = 320.f / 3.f;
CGPoint contentOffset = _tableView.contentOffset;
contentOffset.y = roundf(contentOffset.y / pageSize) * pageSize;
[_tableView setContentOffset:contentOffset animated:YES];
}
This works when you scroll slowly and let go but if you scroll fast, there's a lot of popping. So after wrestling with it for a bit, I'm trying a different approach...
I'm simply enabling paging on the tableView and then setting the width of the table to my desired paging size (a 3rd of the screen's width). I also set clipsToBounds = NO. When I use this approach, the scrolling works as expected but now cells outside of my smaller table width do not draw. What I would like is to force the cell on the left and right of my current cell to draw, even though they are outside of the UITableView's frame. I know cellForRowAtIndexPath get's called from a deeper level but is there some way I can trigger it myself for the cell's I selected?
I've tried using,
[[_tableView cellForRowAtIndexPath:indexPath] setNeedsDisplay];
but it does nothinggggggg!