Maintaining scroll position after reload in tableView - ios

I've got a tableView. When user taps one of records, I check it's property and basing on this I'm filtering results and showing only similar results - so I need to refresh the tableView.
The problem is that user can scroll up/down the tableView. I need to scroll the tableView so the cell is exactly at the same UITableViewScrollPosition as before the refresh.
Obviously, I'm saving last tapped item
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
_lastSelectedItem = [self itemForIndexPath:indexPath];
}
Then after reloading the tableView:
NSIndexPath *indexPath = [self indexPathForItem:_lastSelectedItem];
if (_tableView.numberOfSections > indexPath.section && [_tableView numberOfRowsInSection:indexPath.section] > indexPath.row) {
[_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
_lastSelectedItem = nil;
}
It would be good but... user could not finish at UITableViewScrollPositionMiddle. He could have finish scrolling at UITableViewScrollPositionTop or UITableViewScrollPositionBottom or even somewhere between.
-- edit --
Calculating via offset is also problematic, as the offset is the difference between start of view and top table scroll position.. while I'm not interested in this value :/.

Because my structure was quite complicated I finished to do it this way (eg. expandable sections etc). Please do mind, that I'm saving _lastSelectedItem, which is my object at datasource, as indexPath will change after refresh.
- (void)refresh {
[self saveLastScrollPosition];
[self reloadData]; // inside we reload data + reload tableView
[self scrollToLastScrollPosition];
}
- (NSInteger)heightToMinYOfCellAtIndexPath:(NSIndexPath *)indexPath {
NSInteger sections = indexPath.section;
NSInteger totalRows = 0;
for (NSInteger section = 0; section < indexPath.section; section++) {
totalRows += [self.tableView numberOfRowsInSection:section];
}
totalRows += indexPath.row;
return ((sections + 1) * kHeaderHeight + totalRows * kRowHeight);
}
- (void)saveLastScrollPosition {
if (_lastSelectedItem) { // make sure we have that item selected
NSIndexPath *indexPath = [self itemForItem:_lastSelectedItem];
NSInteger aboveItemHeight = [self heightToMinYOfCellAtIndexPath:indexPath];
CGFloat contentOffsetY = self.tableView.contentOffset.y;
_previousOffset = aboveItemHeight - contentOffsetY;
}
}
- (void)scrollToLastScrollPostion {
if (_lastSelectedItem) { // make sure we have that item selected
NSIndexPath *indexPath = [self itemForItem:_lastSelectedItem];
if (self.tableView.numberOfSections > indexPath.section && [self.tableView numberOfRowsInSection:indexPath.section] > indexPath.row) { // make sure the indexPath still exist after reload
NSInteger aboveItemHeight = [self heightToMinYOfCellAtIndexPath:indexPath]; // height of items above selected index
CGPoint offset = CGPointMake(0.f, aboveItemHeight - _previousOffset);
// in case index should be higher: eg earlier item was at index (4,8) now is at (0,0)
if (offset.y < 0) {
offset.y = 0;
}
[self.tableView setContentOffset:offset animated:NO]; // just jump, user shouldn't see this
_previousOffset = 0; // forget the calculated values
_lastSelectedItem = nil;
}
}
}

Related

Insert UITableView row without animation

I'm using insertRowsAtIndexPaths:withRowAnimation, and setting with UITableViewRowAnimationNone. Then i use the the mehod below
Insert UITableView row without ANY animation
But it causes a flash when reload the tableview. How can i fix the flash?
Thank you。
My demo is want to pull to load more local data and show on the tableview,but the last position is previous contentOffset.So i overwrite the UITableView's setContentSize function.
- (void)setContentSize:(CGSize)contentSize {
if (!CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
if (contentSize.height> self.contentSize.height) {
CGPoint offset = self.contentOffset;
offset.y += contentSize.height - self.contentSize.height;
self.contentOffset = offset;
}
}
[super setContentSize:contentSize];
}
When pull to refresh, load the data and refresh tableview:
for (long j = 0; j < result.count; j++) {
NSIndexPath *index = [NSIndexPath indexPathForRow:j inSection:0];
[array addObject:index];
}
[UIView performWithoutAnimation:^{
[_mainTableView beginUpdates];
[_mainTableView insertRowsAtIndexPaths:array withRowAnimation:UITableViewRowAnimationNone];
[_mainTableView endUpdates];
}];

cellForRowAtIndexPath: returning nil

So I've got a universal app, and I'm trying to hide a border of a cell, if the next cell in the tableView has an error state. I use this code in tableView:cellForRowAtIndexPath: to determine if the cell should hide it's bottom border or not:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
DCSMCDynamicBaseCell * cell = [self.cellManager dynamicCellForComponent:[self componentForIndexPath:indexPath] atIndexPath:indexPath canShowErrors:self.shouldDisplayErrors];
NSIndexPath *nextIndexPath = [self.tableView nextRowForIndexPath:indexPath];
if (nextIndexPath) {
DCSMCDynamicBaseCell *nextCell = [self.tableView cellForRowAtIndexPath:nextIndexPath];
DCSMCDynamicBaseCell *currentCell = [self.tableView cellForRowAtIndexPath:indexPath];
if (nextCell.invalidState) {
cell.shouldHideBottomBorder = YES;
NSLog(#"%#", currentCell);
}
}
if ([cell isKindOfClass:[DCSMCDynamicCopyCell class]]) {
[((DCSMCDynamicCopyCell *)cell).webView setNavigationController:self.navigationController];
}
[cell setDelegate:self];
self.previousIndexPath = indexPath;
return cell;
}
My nextRowForIndexPath looks like this:
- (NSIndexPath *)nextRowForIndexPath:(NSIndexPath *)indexPath {
NSInteger currentRow = indexPath.row;
NSInteger currentSection = indexPath.section;
NSInteger numberOfRows = [self numberOfRowsInSection:indexPath.section];
NSInteger numberOfSections = [self numberOfSections];
NSIndexPath *newIndexPath;
if (currentRow + 1 < numberOfRows) {
newIndexPath = [NSIndexPath indexPathForRow:currentRow + 1 inSection:currentSection];
} else if (currentSection + 1 > numberOfSections) {
newIndexPath = [NSIndexPath indexPathForRow:0 inSection:currentSection + 1];
} else {
newIndexPath = nil;
}
return newIndexPath;
}
On my iPhone 6 simulator, everything works fine. It gets the cell and hides the bottom border of it. I may refactor this a bit in order to make sure it works in all scenarios, but for a first pass before testing, it works great.
The issue is on the iPad, every single cell returns nil when using the cellForRowAtIndexPath: method. Even weirder, on iPad, the screen is big enough so that all cells are visible at all times, so when I call reload data, the cells are already visible and should get returned by this method.
Does anyone know why a cell wouldn't get returned by cellForRowAtIndexPath:on iPad but would on iPhone?

Unable to add and delete rows simultaneously

I am following a tutorial,where I can expand the table view by adding some sub-cells and collapse the table by removing the sub-cells. I am trying to change how the expand operation should execute. When I tap on a row,it expand and shows the sub-cells,and when I tap on other row,the previous expanded row should close. I am not able to do this . I tried the following but I couldnt make the one row expandable at a time,and the other row should close when one expands.
Note: This code works fine,but to collapse the row,we need to tap twice on the same row. I am trying to collapse when other parent is tapped.
Here is the code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
Node *node = [self nodeForIndexPath:indexPath];
// differenet table view cell prototypes for different node levels
UITableViewCell *cell;
if(node.level == 0)
{
cell = [tableView dequeueReusableCellWithIdentifier:#"level1cell" forIndexPath:indexPath];
}
else
{
cell = [tableView dequeueReusableCellWithIdentifier:#"level2cell" forIndexPath:indexPath];
}
// set the nodes title as row text
cell.textLabel.text = node.title;
// attach the nodeId for later lookup when selected
cell.tag = node.nodeId;
// Configure the cell...
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
Node *node = [self nodeForIndexPath:indexPath];
// NSLog(#"node id is %ld level is %ld and indexpath.row is %d",(long)node.nodeId,(long)node.level,indexPath.row);
node.expanded = !node.expanded;
if (node.level==0) {
NSLog(#"you tapped parent");
//now check other parents are expanded or not
}else{
NSLog(#"you tapped child");
}
//insertion always happen after deletion
// NSLog(#"you touched at %#,index row is %d",indexPath,indexPath.row);
if(node.expanded )
{
// add n rows
NSMutableArray *indexPaths = [NSMutableArray array];
for(NSInteger i=indexPath.row; i< indexPath.row+node.subNodes.count; i++)
{
[indexPaths addObject:[NSIndexPath indexPathForRow:i+1 inSection:0]];
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
else
{
//you store the node refe after row is expanded
// delete n rows
NSMutableArray *indexPaths = [NSMutableArray array];
for(NSInteger i=indexPath.row; i< indexPath.row+node.subNodes.count; i++)
{
[indexPaths addObject:[NSIndexPath indexPathForRow:i+1 inSection:0]];
}
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
}
#pragma mark - Private helper
-(Node*) nodeForIndexPath:(NSIndexPath*)indexPath
{
int idx = 0;
for(Node *node in _nodes)
{
if(idx == indexPath.row)
{
return node;
}
idx++;
if(node.expanded)
{
for (Node *subNode in node.subNodes)
{
if(idx == indexPath.row)
{
return subNode;
}
idx++;
}
}
}
return nil;
}
KMAccordionTableViewController can helps you?
https://github.com/klevison/KMAccordionTableViewController
Hm... If your current code is working, but you need to tap twice to collapse the open/selected row, perhaps it's because didDeselectRowAtIndexPath: is being called during that first tap in place of didSelectRowAtIndexPath: in order to deselect the selected row. I'd recommend configuring your tableview to allow for multiple selection so that didSelectRowAtIndexPath: is called every time, ex:
- (void)viewDidLoad {
[super viewDidLoad];
tableView.allowsMultipleSelection = YES;
}

ios8 UITableView scrolling jumps back when scrolling up

I have an odd situation with my UItableView.
I am loading images from the internet and presenting them in a uitableviewcell in an asynchronous manner. When I scroll down (i.e. row 1 to 6) the scroll scrolls down smoothly just as I expect it to.
However, then I scroll up (i.g. row 6 to 1) the scroll jumps back. for example if I scroll from row 6 to row 5, it will jump back to row 6. The second time I try to scroll up it lets me go up to row 4, but then it scrolls me back to row 5 or 6 but usually just 5.
What I don't understand is why this is happening in only one direction.
It seems to be effecting ios 8 but not ios 7.
Therefore it is an implementation difference in how ios8 and 7 handle the uitableview.
How can I fix this?
Here is some code to give you some context
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CVFullImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
CVComicRecord *comicRecord = self.comicRecords[indexPath.row];
[cell.loaderGear stopAnimating];
[cell.text setText:comicRecord.title];
NSString *UUID = comicRecord.fullImagePageURL.absoluteString;
[cell setUUID:UUID];
//Check if image is in the cache
UIImage *fullImage = [self.contentViewCache objectForKey:UUID];
[cell setComicFullImage:fullImage];
if (fullImage) {
return cell;
}
[cell.loaderGear startAnimating];
[self requestImageForIndexPath:indexPath];
return cell;
}
- (void)requestImageForIndexPath:(NSIndexPath *)indexPath {
CVComicRecord *comicRecord = self.comicRecords[indexPath.row];
NSString *UUID = comicRecord.fullImagePageURL.absoluteString;
if ([self.contentViewCache objectForKey:UUID]) {
//if it is already cached, I do not need to make a request.
return;
}
id fd = CVPendingOperations.sharedInstance.fullDownloadersInProgress[UUID];
if (fd) {
//if it is in the queue you do no need to make a request
return;
}
comicRecord.failedFull = NO;
CVFullImageDownloader *downloader = [[CVFullImageDownloader alloc] initWithComicRecord:comicRecord withUUID:UUID];
[CVPendingOperations.sharedInstance.fullDownloaderOperationQueue addOperation:downloader];
//when operation completes it will post a notification that will trigger an observer to call fullImageDidFinishDownloading
}
- (void)fullImageDidFinishDownloading:(NSNotification *)notification {
CVComicRecord *comicRecord = notification.userInfo[#"comicRecord"];
NSString *UUID = notification.userInfo[#"UUID"];
UIImage *fullImage = notification.userInfo[#"fullImage"];
comicRecord.failedFull = NO;
[self.contentViewCache setObject:fullImage forKey:UUID];
dispatch_async(dispatch_get_main_queue(), ^{
for (NSIndexPath *indexPath in [self.tableView indexPathsForVisibleRows]) {
CVFullImageTableViewCell *cell = (id)[self.tableView cellForRowAtIndexPath:indexPath];
if (cell) {
if ([cell.UUID isEqualToString:UUID]) {
[cell.loaderGear stopAnimating];
[cell setComicFullImage:fullImage];
[cell layoutIfNeeded];
}
}
}
});
}
#pragma mark - Scroll
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
static int oldOffset = 0;
int newOffset = scrollView.contentOffset.y;
int dy = newOffset- oldOffset;
if (dy > 0) {
[self hideNavigationbar:YES animationDuration:0.5];
} else if (dy < 0) {
[self hideNavigationbar:NO animationDuration:0.5];
}
oldOffset = newOffset;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (decelerate == NO) {
[self prioritizeVisisbleCells];
//currentPage is a property that stores that last row that the user has seen
[self setCurrentPage:[self currentlyViewedComicIndexPath].row];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self prioritizeVisisbleCells];
//currentPage is a property that stores that last row that the user has seen
[self setCurrentPage:[self currentlyViewedComicIndexPath].row];
}
- (void)prioritizeVisisbleCells {
NSArray *ips = [self.tableView indexPathsForVisibleRows];
NSArray *activeIndexPaths = [CVPendingOperations.sharedInstance.fullDownloadersInProgress allKeys];
//add visible cells to queue first
NSSet *visible = [NSSet setWithArray:ips];
NSMutableSet *invisible = [NSMutableSet setWithArray:activeIndexPaths];
[invisible minusSet:visible];
for (NSIndexPath *ip in invisible) {
NSOperation *op = CVPendingOperations.sharedInstance.fullDownloadersInProgress[ip];
[op setQueuePriority:NSOperationQueuePriorityNormal];
}
for (NSIndexPath *ip in visible) {
NSOperation *op = CVPendingOperations.sharedInstance.fullDownloadersInProgress[ip];
[op setQueuePriority:NSOperationQueuePriorityHigh];
}
}
I found the answer in this post very helpful: Table View Cells jump when selected on iOS 8
Try to implement the tableView:estimatedHeightForRowAtIndexPath: in the UITableViewDelegate protocol. It works for me.
Implement the self-sizing and correct view constraints in your storyboard.
in your code put this together
tableView.rowHeight = UITableViewAutomaticDimension;
tableView.estimatedRowHeight = CGFloat value (the initial value)
This exact problem was happening to me while I was using the UITableViewAutomaticDimension and the estimatedRowHeight, as #nferocious76 suggested.
The issue was that my estimatedRowHeight was 400.0 while the actual row height was more like 80.0 (I copy/pasted it from another table view in my app with large cells). I never bothered to change it because I had read somewhere that the estimatedRowHeight didn't really matter as long as it was greater than the actual row height, but apparently this is not the case.
You should calculate your row height yourself and you should remove everything about estimated row height in your code.

How to focus on last cell in a UITableview with scrolling animation?

I have a UITableview in my xcode project.In that Comments are listed.How can i focus on a last cell of my TableView with scrolling animation?
Below method will find the last index of you table view and will focus to that cell with an animation
-(void)goToBottom
{
NSIndexPath *lastIndexPath = [self lastIndexPath];
[<YOUR TABLE VIEW> scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
Code to find the last index of your tableview.
-(NSIndexPath *)lastIndexPath
{
NSInteger lastSectionIndex = MAX(0, [<YOUR TABLE VIEW> numberOfSections] - 1);
NSInteger lastRowIndex = MAX(0, [<YOUR TABLE VIEW> numberOfRowsInSection:lastSectionIndex] - 1);
return [NSIndexPath indexPathForRow:lastRowIndex inSection:lastSectionIndex];
}
Add this code somewhere in your view controller
[self performSelector:#selector(goToBottom) withObject:nil afterDelay:1.0];
Keep in mind that a UITableView inherits from UIScrollView
-(void)scrollToBottom:(id)sender
{
CGSize r = self.tableView.contentSize;
[self.tableView scrollRectToVisible:CGRectMake(0, r.height-10, r.width, 10) animated:YES];
}
execute it like
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectMake(…);
[button addTarget:self action:#selector(scrollToBottom:) forControlEvents:UIControlEventTouchUpInside];
(You dont need to use performSelector:…)
Another solution would be selecting the last row. The table view will take care of the rest.
-(void)scrollToBottom:(id)sender
{
NSInteger lasSection = [self.tableView numberOfSections] - 1;
NSInteger lastRow = [self.tableView numberOfRowsInSection:lasSection]-1;
//this while loops searches for the last section that has more than 0 rows.
// maybe you dont need this check
while (lastRow < 0 && lasSection > 0) {
--lasSection;
lastRow = [self.tableView numberOfRowsInSection:lasSection]-1;
}
//if there is no section with any row. if your data source is sane,
// this is not needed.
if (lasSection < 0 && lastRow < 0)
return;
NSIndexPath *lastRowIndexPath =[NSIndexPath indexPathForRow:lastRow inSection:lasSection];
[self.tableView selectRowAtIndexPath:lastRowIndexPath animated:YES scrollPosition:UITableViewScrollPositionBottom];
}
the button is the same.
Swift 3:
let indexPath = IndexPath(row: /*ROW TO SCROLL TO*/, section: /*SECTION TO SCROLL TO*/)
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)

Resources