UITableView防止在滚动时重新加载数据

7
我已经实现了一个带有“加载更多”功能的 UITableView。tableView 从有时较慢的服务器加载大图像。我为每个图像启动一个 URLConnection,并重新加载与连接对象保存的 URLConnection 相应的 indexPath。连接本身在 tableView 上调用 -reloadData
现在,当点击“加载更多”按钮时,我会滚动到新数据集的第一行,位置在底部。这很好地解决了我的异步加载系统。
但我遇到了以下问题:当连接“太快”时,tableView 在仍然滚动到新数据集的第一行时就在给定的 indexPath 重新加载数据,tableView 会向后滚动该单元格高度的一半。
这是它应该看起来的样子和实际上的样子: what it should look like what it actually looks like 代码如下:
[[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:方法会在数据源数组中的对象为字符串时启动JWURLConnection,并在完成块中用UIImage实例替换它。然后重新加载给定的单元格:

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];
}

我能否在tableView滚动完成之前防止执行-reloadRowsAtIndexPaths:withRowAnimation:调用?或者你能想象一种好的方法来防止这种行为吗?

3个回答

7
基于Maltesavner的想法(请也点赞他的答案),我可以实现一个解决方案。虽然他的答案没有起作用,但是它是正确的指引。
我不得不实现-scrollViewDidEndScrollingAnimation:。我创建了一个布尔属性叫做_autoScrolling和一个NSMutableArray属性来存储在滚动时重新加载的索引路径。在URLConnections完成块儿中,我做了这个:
if (_autoScrolling) {
    if (!_indexPathsToReload) {
        _indexPathsToReload = [NSMutableArray array];
    }
    [_indexPathsToReload addObject:indexPath];
}
else {
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}

然后是这个:

- (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;
}

我花了很长时间才找到使用-performSelector:withObject:afterDelay:的技巧,但我仍然不知道为什么需要它。

我以为这个方法可能会被过早地调用。所以我实现了一秒钟的延迟,并试图将延迟时间缩短。使用0.0仍然可以工作,但是如果我直接调用该方法或使用-performSelector:withObject:就无法工作。

真希望有人能解释一下这个问题。

编辑

几年后重新审视这个问题,我可以解释一下发生了什么:

调用 -[NSObject (NSDelayedPerforming) performSelector:withObject:afterDelay:]保证在下一个运行循环迭代中执行该调用。

因此,一个更好的甚至是我个人认为更优美的解决方案是:

[[NSOperationQueue currentQueue] addOperationWithBlock:^{
    [self performRelodingAfterAutoScroll];
}];

我在这个答案中写了更详细的解释。


6

抱歉,我没有足够的声望添加评论,因此,对于您最后一个问题的答案在另一个回答中。

-performSelector:withObject:afterDelay: 带有 0.0 秒的延迟不会立即执行给定的选择器,而是在当前 Runloop 循环完成并延迟后执行它。

-performSelector:withObject: 添加到并在当前 Runloop 循环中执行。这与直接调用该方法相同。

因此,使用 -performSelector:withObject:afterDelay: UI 将在当前 Runloop 循环中更新,即在此情况下滚动动画可以完成,然后执行您的选择器(并再次重新加载 UI)。

来源: 苹果开发文档此线程答案


太好了!感谢您的澄清!我想接受,但不幸的是它并不是我的PO的答案 =) - Julian F. Weinert
1
没关系。至少我离能够添加注释更近了一步。 :) 很高兴我能帮到你。 - Malte

3
您可以使用UIScrollViewDelegate协议(通过使用UITableViewDelegate免费获得),利用-scrollViewDidScroll或-scrollViewWillBeginDragging:方法来检测滚动是否已经开始或停止。使用这些回调控制何时加载/停止加载单元格数据即可。

这是一个好的方向,但不幸的是不是我正在寻找的答案,所以我不能接受你的回答。还是谢谢你的努力 :) - Julian F. Weinert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接