7
votes

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:

what it should look likewhat it actually looks like

^^^^^^^^^^^^ 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?

3

3 Answers

7
votes

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.

6
votes

Sorry i don't have enough reputation to add a comment, hence the answer to your last question in a separate answer.

-performSelector:withObject:afterDelay: with a delay of 0.0 seconds does not execute the given selector immediately but instead performs it after the current Runloop Cycle finishes and after the given delay.

Where as -performSelector:withObject: is added to and executed in the current Runloop Cycle. Which is the same as directly calling the method.

Therefore using -performSelector:withObject:afterDelay: the UI will get updated in the current Runloop Cycle i.e in this case the scrolling animation can finish, before your selector is performed(and reloads the UI once more).

Source: Apple Dev Docs and this Thread Answer

3
votes

You can use the UIScrollViewDelegate protocols (which you get for free using UITableViewDelegate) and utilize the -scrollViewDidScroll or -scrollViewWillBeginDragging: methods to detect scrolling has started or stopped. Work with those callbacks to control when you want to load/stop loading cell data.