I have implemented an UITableView
with load more functionality. The tableView loads big images from a sometimes slow server. I'm starting an URLConnection for each image and reload the indexPath corresponding to the URLConnection (saved with the connection object). The connections themselves call -reloadData
on the tableView.
Now when clicking the load more button, I scroll to the first row of the new data set with position bottom. This works great and also my asynchronous loading system.
I faced the following issue: When the connection is "too fast", the tableView is reloading the data at a given indexPath while the tableView is still scrolling to the first cell of the new data set, the tableView scrolls back half the height of that cell.
This is what it should look like and what it actually does:
^^^^^^^^^^^^ should ^^^^^^^^^^^^ ^^^^^^^^^^^^^ does ^^^^^^^^^^^^^
And here is some code:
[[self tableView] beginUpdates];
for (NSMutableDictionary *post in object) {
[_dataSource addObject:post];
[[self tableView] insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:[_dataSource indexOfObject:post] inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
}
[[self tableView] endUpdates];
[[self tableView] scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[_dataSource indexOfObject:[object firstObject]] inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
-tableView:cellForRowAtIndexPath:
starts a JWURLConnection if the object in the data source array is a string, and replaces it with an instance of UIImage
in the completion block. Then it reloads the given cell:
id image = [post objectForKey:@"thumbnail_image"];
if ([image isKindOfClass:[NSString class]]) {
JWURLConnection *connection = [JWURLConnection connectionWithGETRequestToURL:[NSURL URLWithString:image] delegate:nil startImmediately:NO];
[connection setFinished:^(NSData *data, NSStringEncoding encoding) {
[post setObject:[UIImage imageWithData:data] forKey:@"thumbnail_image"];
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}];
[cell startLoading];
[connection start];
}
else if ([image isKindOfClass:[UIImage class]]) {
[cell stopLoading];
[cell setImage:image];
}
else {
[cell setImage:nil];
}
Can I prevent the tableView from performing the -reloadRowsAtIndexPaths:withRowAnimation:
calls until the tableView scrolling is done? Or can you imagine a good way to prevent this behavior?
Based on the ideas of Malte and savner (please upvote his answer as well) I could implement a solution. His answer didn't do the trick, but it was the right direction.
I had to implement -scrollViewDidEndScrollingAnimation:
. I created a bool property called _autoScrolling
and an NSMutableArray
property for the index paths that got reloaded while scrolling. In the URLConnections finish block I did this:
if (_autoScrolling) {
if (!_indexPathsToReload) {
_indexPathsToReload = [NSMutableArray array];
}
[_indexPathsToReload addObject:indexPath];
}
else {
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
And then this:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[self performSelector:@selector(performRelodingAfterAutoScroll) withObject:nil afterDelay:0.0];
}
- (void)performRelodingAfterAutoScroll {
_autoScrolling = NO;
if (_indexPathsToReload) {
[[self tableView] reloadRowsAtIndexPaths:_indexPathsToReload withRowAnimation:UITableViewRowAnimationFade];
}
_indexPathsToReload = nil;
}
It took me quite a long time to find the trick with -performSelector:withObject:afterDelay:
and I still don't know why I need it.
I thought the method might got called too early. So I implemented a delay of a second and tried how far I can take it down. It still works with 0.0
but not if I call the method directly or use -performSelector:withObject:
.
I really hope someone can explain that.
EDIT
After revisiting this a few years later I can explain what's going on here:
Calling -[NSObject (NSDelayedPerforming) performSelector:withObject:afterDelay:]
guarantees the call to be performed in the next runloop iteration.
So an even better or IMHO more beautiful solution would be:
[[NSOperationQueue currentQueue] addOperationWithBlock:^{
[self performRelodingAfterAutoScroll];
}];
I wrote a more detailed explanation in this answer.