I'm trying to implement infinite scroll like the Facebook app has.
It appears that Facebook is somehow inserting rows or reloading the table view without stopping the current scroll.
Does anyone know how they're achieving this?
When I call insert rows or reload data during a scroll the scroll is stopped dead. Any help would be appreciated.
Not much code to provide just typical UITableView functions.
[self.tableView insertRowsAtIndexPaths:self.paths withRowAnimation:UITableViewRowAnimationNone];
or
[self.tableView reloadData];
self.paths being the paths recently added via the next page request.
I've also added the following method
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;
Which is working correctly now with reloadData being called. Unfortunately even while this speeds up the addition of cells it still locks the current scroll.
Okay so, as far as the stopping of the currently active scroll is concerned the problem came down to a single line of code. And the solution was just as small.
My TableView has a refresh control on it. In the method in which I receive the next set of data for the TableView I would call the following.
[refreshControl endRefreshing];
My assumption was if it wasn't refreshing that endRefreshing would simply do nothing.
Well... Apparently that's not the case. When you call endRefreshing on a refresh control that isn't refreshing it'll stop the tableview scroll. My fix is as follows.
if(refreshControl.isRefreshing){
[refreshControl endRefreshing];
}
Now the scroll continues after the next load albeit with some delay. That delay of course is easier to debug.
That's just the about of overhead involved in calculating the height of the cells being added which is called on all cells when reload is called unless you have
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;
So now that I have everything sorted out it's time to create an efficient way to calculate a reasonably close estimate height.
Related
I understand the concepts of cell re-usability for Xcode 5.0 table views. However, I have one very weird observation which I don't understand and wish anyone of you could enlighten me here. Thanks.
I have implemented a table view with a search bar utility (just on top of the table view). Under each custom cell (prototype cell), whenever a user clicks on it, it will be marked with a checkmark (UITableViewCellAccessoryCheckmark). The number of cells are more than 10.
Observation:
- Without using any search, marking and unmarking a cell is working as intended. Cells are updated instantly along with their checkmarks.
- When doing a search, from the results given, marking and unmarking a cell is also working as intended.
[Problem] Here comes the weird issue: when cancelling a search, an already marked cell (marked during search) does not refresh itself in the tableview unless scrolling up or down is performed!
And hence, I wrote [tableview reload] at the end of tableview:didSelectRowAtIndexPath: method. Obviously, it doesn't refresh the tableview for me. Without further changing any other code, merely modifying [tableview reload] to [self.tableview reload] under the same method works!
Why is the only addition of "self." able to make the table cells refreshed instantly? I have always thought the first argument, tableView, from the method (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath is as equal to self.tableview. Obviously, my interpretation in this case is wrong.
Thank you. I'm sorry for my lengthy post.
My guess is that this UISearchBar comes from a UISearchDisplayController. Is that correct?
If true, that is a common misconception, but an easy one to understand.
When filtering your UITableView entries and showing results, UISearchDisplayController actually overlays the view with its own tableView, UISearchResultsTableView.
Thus, this overlaid tableView also gets to call data source and delegate methods on your implementation, and this is when the tableView argument from tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath stops being equivalent to self.tableView.
This means that calling [tableView reloadData] during filtering actually asks UISearchResultsTableView to reload its contents, not self.tableView, a property of your viewController.
I've implemented 'infinite scrolling' on one of my projects and I was playing around with the new estimatedHeightForRowAtIndexPath: delegate method. Once I implemented that delegate method, my tableView jumps (a.k.a scroll to the bottom) whenever i call reloadData (which happens when i add a new set of rows.)
Without that method, my tableView stays in place and it adds the additional rows to the bottom of the tableView without any scrolling.
I'm calling the [tableView reloadData] and not the other methods (insertRowsAtIndexPaths:). I don't call the beginUpdates or endUpdates since I'm reloading the whole table.
Has anyone experienced this ? I
So here's what i did to reduce the jumping while reloading the tableView.
In the estimatedHeightForRowAtIndexPath: I started returning a better estimated height. The more accurate the height is, lesser the jumping while adding new rows to the bottom.
I also started caching the calculated height of the cell from heightForRowAtIndexPath: in a dictionary and return the cached value the next time it calls the estimatedHeightForRowAtIndexPath:.
Hope this helps anyone who encounters this issue.
I filed a BugReport quite a while ago (16472265) and today got the response that it's fixed in iOS8. Tried it out and it works now as expected. No more jumping of the tableview! Wohoooo
Check your condition in this scrollView Delagete method,
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
I have a UITableView that I modify (delete one row and insert one row) when the user clicks on any of the rows. Memory deallocation works great in iOS6 and earlier, but iOS7 will not release the tableview. Here is the basics of my code. I have reduced the code hugely, but the issue is still there.
- (void)tableView:(UITableView *)intableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[intableView beginUpdates];
[intableView endUpdates];
}
The begin/end updates cause the memory to get held somewhere. It's the same if I insert or delete rows (along with the background data array) inside this function. I subclassed the UITableView and cells with just adding:
-(void)dealloc
{
NSLog(#"deallocated %#",[self class]);
}
So that I can see when they get deallocated. Are we not supposed to update the tableview on selection anymore? Is it in a different thread where something gets held? (I have tried dispatch_async into different threads)
I could post more code, but my current code looks just like what I posted and it is still holding on to the tableview somehow and not deallocating it.
I am currently writing an app, using storyboards, that contains two UITableViews within the same window.
When running the application, the first UITableView will populate with a number of "Registration Numbers".
When the user taps on a cell, then the second UITableView is supposed to populate with detail about the selected cell.
When tapping a number in the first table I have a method that drives the:
[mySecondTableView reloadData]
Which it does. I am under the impression that when invoking the reloadData command, it should then call both:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
and
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
The first fires, but the second won't.
I have both the data source and delegate wired to self.
I am not sure what I am missing.
Are both methods meant to fire?
When things like this happen to me, it's usually because I'm calling -reloadData outside of the main thread.
After reloadData, try this:
NSIndexSet * sections = [NSIndexSet indexSetWithIndex:0];
[self.tableView reloadSections:sections withRowAnimation:UITableViewRowAnimationNone];
I found if the data change is subtle, the table view seems to optimize away the need to refresh.
Pointed answers doesn't solve my table view problem. It was still calling
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
however cell drawing method wasn't fired:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
My problem was that(because of my mistake of course) table view height was 0. So as table displays only cells that are visible, it never tries to display a cell. When I changed height value drawing cell method started to fire again.
Botom line double check your table frame and ensure you have non-zero width and height values!
I just incurred this same issue. I used a performSelectorOnMainThread to call a method which then calls the reloadData. This was necessary since I was trying to update outside the main thread as suggested by hatfinch.
Make sure you call performSelectorOnMainThread with waitUntilDone:NO, otherwise you may still have the same issue
Found another cause of cellForRowAtIndexPath not being called.
If numberOfRowsInSection returns 0, cellForRowAtIndexPath won't be called.
This happened in my case because of an NSArray being de-allocated during a view controller change and its consequent count (since it was nil) returning 0.
After banging my head against a wall for days this solved my problem:
Set the All Exceptions breakpoint and see if you are getting an out of bounds exception in your datasource. Without setting the breakpoint there is no crash and the tableview will simply fail to reload.
Full answer here
I was debugging a similar issue for hours before I realized my .reloadData() call was being executed by a callback of a previous instance of the view controller.
So my breakpoint was hitting on the .reloadData() call of my old instance of that view controller, but the new instance that was actually shown wasn't reloading because it wasn't the one executing the callback that called .reloadData().
tl;dr
When deleting a section in a UITableView while the scroll offset is somewhere in the middle of the table the flow of animation goes like this -
The contentOffset is set to (0,0) immediately (no animation, just pops up)
The section fades away nicely
I'd like to make this animation flow a bit better - fade away the section and only afterwards (or simultaneously in a smooth way) scroll the "dead zone" of the table back up.
A bit more explaining
I'm using NSFetchedResultsController as a data source for UITableView to display rows and update the table when changes occur in the NSManagedObjectContext - like this (I removed unrelated code) -
- (void)controller: (NSFetchedResultsController *)controller
didChangeObject: (id)anObject
atIndexPath: (NSIndexPath *)indexPath
forChangeType: (NSFetchedResultsChangeType)type
newIndexPath: (NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableController.tableView;
switch(type) {
....
NSFetchedResultsChangeDelete:[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
...
}
}
I got all the boilerplate of controllerWillChangeContent and controllerDidChangeContent, the result of this code is that if all the rows in a specific section are removed - the section is also removed.
The problem (as I specified in the tl;dr section) is that the animation doesn't work as expected -
If the section removal happen while scrolled half-way into the removed section, the scroll content changed immediately and the section fades away, which looks pretty broken.
Anyone ever stumbled on a situation like this? I'm sure I can narrow it down to a generic problem without using NSFetchedResultsController, that's jus the code I'm currently using.
I'd gladly add more information if needed.
Thanks!
Update 1
So after a bit playing with the contentOffset manually I can get something partly working when doing this flow -
When the NSFetchedResultsController calls controllerWillChangeContent I save the UITableView contentOffset (before beginUpdates)
When the controllerDidChangeContent is called and right after I call endUpdates I save the contentOffset of the table (this is the offset that wasn't animated)
I scroll back to the original contentOffset I saved in part 1 and use [tableView setContentOffset:offsetAfterEndUpdates animated:YES] to scroll to the new offset
This cannot be the best solution / what Apple ment.
A UITableViewDelegate is also conform to UIScrollViewDelegate protocol so you could delay that section removal action to
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
// try to perform your action here,
// for instance you could re-attach your NSFetchedResultController delegate here
}
One approach that works (at least in my experience) is to a) delete the section and b) don't delete the individual rows.
For reference, take a look at the TLIndexPathUpdates initializer in TLIndexPathTools. It calculates batch updates and I think it works well for the scenario you've described.
Okay!
So after intensive digging I found that the problem lies in my code (like I was commented right after posting). Was pretty hard to find but somewhere between the beginUpdates and endUpdates I change the UITableView footer view, that messes with the contentOffset animation.
So that's it, my fault, not Apple.
Thanks for the help Jonathan Cichon!