如何检测UITableView的加载结束


141

我想在加载完成后更改表的偏移量,该偏移量取决于表上加载的单元格数。

无论如何,SDK上是否知道uitableview加载何时完成?我既没有看到委托,也没有看到数据源协议。

我无法使用数据源的计数,因为仅可见单元的加载。


试算数据源的组合和“indexPathsForVisibleRows”
Swapnil Luktuke

1
根据标志中的进一步信息重新打开:“它不是重复的。它询问可见单元的加载,而不是数据询问的完成。请参阅已接受答案的更新”
Kev

这个解决方案对我来说很好。你检查它stackoverflow.com/questions/1483581/...
哈立德Annajar

Answers:


224

改进为@RichX答案: lastRow可以同时为[tableView numberOfRowsInSection: 0] - 1((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row。因此,代码将是:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}

更新: 嗯,@ htafoya的评论是正确的。如果您希望此代码检测从源加载所有数据的结束,则不会,但这不是原始问题。此代码用于检测何时显示所有要显示的单元格。willDisplayCell:用于更流畅的用户界面(单个单元格通常在willDisplay:调用后显示很快)。您也可以尝试使用tableView:didEndDisplayingCell:


知道何时所有可见单元都加载好得多。
埃里克(Eric)

14
但是,只要用户滚动查看更多的单元格,就会调用此方法。
htafoya

问题在于它不考虑页脚视图。对于大多数人来说可能不是问题,但是如果是这样,则可以在viewForFooterInSection方法中应用相同的想法。
凯尔·克莱格

1
@KyleClegg patric.schenke在评论您的答案时提到了原因之一。另外,如果在单元加载结束时页脚不可见怎么办?
folex

27
tableView:didEndDisplayingCell:实际上是在从视图中删除单元格时调用的,而不是在视图中完成其单元格渲染时调用的,因此它将无法正常工作。方法名称不好。要阅读文档。
Andrew Raphael

34

Swift 3&4&5版本:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}

1
首次加载数据时会调用此方法。在我的情况下,只有4个可见的单元格。我加载了8行,但是仍然调用了此函数。我想要的是在用户向下滚动到最后一行时加载更多数据
Muhammad Nayab 18/12/12

嗨,穆罕默德·纳亚布(Muhammad Nayab),也许您可​​以将“ if indexPath == lastVisibleIndexPath”替换为“ if indexPath.row == yourDataSource.count”
Daniele Ceglia,

1
@DanieleCeglia感谢您指出这一点。如果indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count-1这对我有用。干杯!!!
Yogesh Patel

28

我总是使用这个非常简单的解决方案:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == lastRow){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}

如何检测最后一行?多数民众赞成在我的问题..你能解释一下,如果条件,你如何得到最后的。
Sameera Chathuranga

6
最后一行是[tableView numberOfRowsInSection: 0] - 1。您必须替换0为所需的值。但这不是问题。问题是UITableView仅加载可见的。但是((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row解决了问题。
folex 2012年

1
如果我们正在寻找绝对完成,那不是最好使用-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath吗?
morcutt

10

这是另一个似乎对我有用的选择。在viewForFooter委托方法中,检查是否为最后一节,然后在其中添加代码。在意识到willDisplayCell不考虑页脚(如果有)的情况下想到了这种方法。

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section 
{
  // Perform some final layout updates
  if (section == ([tableView numberOfSections] - 1)) {
    [self tableViewWillFinishLoading:tableView];
  }

  // Return nil, or whatever view you were going to return for the footer
  return nil;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
  // Return 0, or the height for your footer view
  return 0.0;
}

- (void)tableViewWillFinishLoading:(UITableView *)tableView
{
  NSLog(@"finished loading");
}

如果您要查找整个UITableView而不是可见单元的最终负载,我发现这种方法最有效。根据您的需要,您可能只需要可见的单元格,在这种情况下,folex的答案是一个不错的选择。


使用自动布局niltableView:viewForFooterInSection:iOS 7中的布局弄乱了返回。
ma11hew28'2013/

有趣。如果将其设置为高度和宽度为0的框架怎么办?return CGRectMake(0,0,0,0);
凯尔·克莱格

我尝试过,甚至设置self.tableView.sectionFooterHeight = 0。无论哪种方式,似乎都插入了一个高度大约为10的部分页脚视图。我敢打赌,可以通过在返回的视图中添加0高度约束来解决此问题。无论如何,我很好,因为我实际上想弄清楚如何在最后一个单元格上启动UITableView,但首先看到了这一点。
ma11hew28 2013年

1
@MattDiPasquale如果您实现viewForFooterInSection,则还必须实现heightForFooterInSection。页脚为零的部分必须返回0。到目前为止,这也在官方文档中。
patric.schenke 2013年

如果将heightForFooterInSection设置为0,则不会调用viewForFooterInSection
Oded Harth 2015年

10

Swift 2解决方案:

// willDisplay function
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    let lastRowIndex = tableView.numberOfRowsInSection(0)
    if indexPath.row == lastRowIndex - 1 {
        fetchNewDataFromServer()
    }
}

// data fetcher function
func fetchNewDataFromServer() {
    if(!loading && !allDataFetched) {
        // call beginUpdates before multiple rows insert operation
        tableView.beginUpdates()
        // for loop
        // insertRowsAtIndexPaths
        tableView.endUpdates()
    }
}

9

使用私有API:

@objc func tableViewDidFinishReload(_ tableView: UITableView) {
    print(#function)
    cellsAreLoaded = true
}

使用公共API:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // cancel the perform request if there is another section
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];

    // create a perform request to call the didLoadRows method on the next event loop.
    [self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];

    return [self.myDataSource numberOfRowsInSection:section];
}

// called after the rows in the last section is loaded
-(void)tableViewDidLoadRows:(UITableView*)tableView{
    self.cellsAreLoaded = YES;
}

可能的更好设计是将可见单元格添加到集合中,然后当您需要检查表是否已加载时,可以在该集合周围执行for循环,例如

var visibleCells = Set<UITableViewCell>()

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.insert(cell)
}

override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.remove(cell)
}

// example property you want to show on a cell that you only want to update the cell after the table is loaded. cellForRow also calls configure too for the initial state.
var count = 5 {
    didSet {
        for cell in visibleCells {
            configureCell(cell)
        }
    }
}

比其他解决方案好得多。它不需要重新加载tableView。这非常简单,并且在每次加载最后一个单元格时都不会调用viewDidLoadRows:。
马特·哈德森

这是迄今为止最好的解决方案,因为其他解决方案
并未

我对这个尖刻的评论感到抱歉,但是这个“魔术”到底有多精确?从委托中调出另一个方法。我不明白
卢卡斯·彼得

@LukasPetr看看取消和afterDelay。这些使方法调用可以在除最后一个(表已完全完成加载)之外的每个节中取消。
malhal

@malhal哦,好的。乍一看,它比我想象的要聪明。
卢卡斯·彼得

8

对于Swift 3中选择的答案版本:

var isLoadingTableView = true

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if tableData.count > 0 && isLoadingTableView {
        if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows, let lastIndexPath = indexPathsForVisibleRows.last, lastIndexPath.row == indexPath.row {
            isLoadingTableView = false
            //do something after table is done loading
        }
    }
}

我需要isLoadingTableView变量,因为我想确保在完成默认单元格选择之前已完成表的加载。如果您不包括它,那么每次滚动表时,它将再次调用您的代码。



3

要了解表格视图何时完成其内容加载,我们首先需要对如何将视图放置在屏幕上有基本的了解。

在应用程序的生命周期中,有4个关键时刻:

  1. 该应用接收事件(触摸,计时器,已分派的块等)
  2. 该应用处理事件(它修改约束,开始动画,更改背景等)
  3. 该应用会计算新的视图层次结构
  4. 该应用程序呈现视图层次结构并显示它

2次和3次完全分开。为什么呢 出于性能原因,我们不希望每次进行修改时都执行时刻3的所有计算。

所以,我认为您正面临这样的情况:

tableView.reloadData()
tableView.visibleCells.count // wrong count oO

这是怎么了

像任何视图一样,表视图会延迟加载其内容。实际上,如果您reloadData多次拨打电话,则不会造成性能问题。表格视图仅基于其委托实现重新计算其内容大小,并等待3时刻加载其单元格。这次称为布局通行证。

好的,如何进入布局通行证?

在布局过程中,应用程序将计算视图层次结构的所有框架。涉足,您可以覆盖专用的方法layoutSubviewsupdateLayoutConstraints等在UIView与等效的方法在一个视图控制器子类。

这正是表视图的功能。它重写layoutSubviews并根据您的委托实现添加或删除单元格。它会cellForRow在添加和布局新单元之前立即调用willDisplay。如果您调用reloadData了表格视图或只是将表格视图添加到层​​次结构中,则表格视图会在此关键时刻添加必要数量的单元格以填充其框架。

好的,但是现在,如何知道表视图何时完成了其内容的重新加载?

现在,我们可以重新表述这个问题:如何知道表视图何时完成其子视图的布局?

最简单的方法是进入表格视图的布局:

class MyTableView: UITableView {
    func layoutSubviews() {
        super.layoutSubviews()
        // the displayed cells are loaded
    }
}

请注意,在表视图的生命周期中多次调用此方法。由于表格视图的滚动和出队行为,经常修改,删除和添加单元格。但是,在super.layoutSubviews()加载了单元格之后,它可以正常工作。此解决方案等效于等待willDisplay最后一个索引路径的事件。layoutSubviews在表视图的执行期间,为添加的每个单元格调用此事件。

另一种方法是在应用程序完成布局传递时被调用。

文档中所述,您可以使用以下选项UIView.animate(withDuration:completion)

tableView.reloadData()
UIView.animate(withDuration: 0) {
    // layout done
}

此解决方案有效,但是在完成布局和调用块之间,屏幕将刷新一次。这等效于DispatchMain.async解决方案,但已指定。

或者,我希望强制使用表格视图的布局

有一种专用方法可以强制任何视图立即计算其子视图帧layoutIfNeeded

tableView.reloadData()
table.layoutIfNeeded()
// layout done

但是请注意,这样做会删除系统使用的延迟加载。反复调用这些方法可能会导致性能问题。在计算表格视图的框架之前,请确保它们不会被调用,否则表格视图将再次加载,并且不会收到通知。


我认为没有完美的解决方案。子类化可能导致混乱。布局遍历从顶部开始一直到达底部,因此在完成所有布局后不容易得到通知。并layoutIfNeeded()可能导致性能问题等,但是了解这些选择,您应该能够想到一种适合您需要的选择。


1

这是您在Swift 3中的操作方法:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    if indexPath.row == 0 {
        // perform your logic here, for the first row in the table
    }

    // ....
}

1

这是我在Swift 3中的操作方法

let threshold: CGFloat = 76.0 // threshold from bottom of tableView

internal func scrollViewDidScroll(_ scrollView: UIScrollView) {

    let contentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    if  (!isLoadingMore) &&  (maximumOffset - contentOffset <= threshold) {
        self.loadVideosList()
    }
}

1

这就是我要做的。

  1. 在您的基类中(可以是rootVC BaseVc等),

    A.编写一个协议以发送“ DidFinishReloading”回调。

    @protocol ReloadComplition <NSObject>
    @required
    - (void)didEndReloading:(UITableView *)tableView;
    @end

    B.编写一个通用方法来重新加载表视图。

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
  2. 在基类方法实现中,先调用reloadData,再调用带有延迟的委托方法。

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController{
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [tableView reloadData];
            if(aViewController && [aViewController respondsToSelector:@selector(didEndReloading:)]){
                [aViewController performSelector:@selector(didEndReloading:) withObject:tableView afterDelay:0];
            }
        }];
    }
  3. 在需要回调的所有视图控制器中确认重新加载完成协议。

    -(void)didEndReloading:(UITableView *)tableView{
        //do your stuff.
    }

参考:https : //discussions.apple.com/thread/2598339?start=0&tstart=0


0

@folex的答案是正确的。

但是,如果tableView 一次显示多个部分,它将失败

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
   if([indexPath isEqual:((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject])]){
    //end of loading

 }
}

0

在Swift中,您可以执行以下操作。每当您到达tableView的末尾时,以下条件将成立

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row+1 == postArray.count {
            println("came to last row")
        }
}


0

如果您有多个部分,请按以下步骤获取最后部分中的最后一行(快速键3):

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let visibleRows = tableView.indexPathsForVisibleRows, let lastRow = visibleRows.last?.row, let lastSection = visibleRows.map({$0.section}).last {
        if indexPath.row == lastRow && indexPath.section == lastSection {
            // Finished loading visible rows

        }
    }
}

0

我很偶然地碰到了这个解决方案:

tableView.tableFooterView = UIView()
tableViewHeight.constant = tableView.contentSize.height

您需要先设置footerView,然后再获取contentSize,例如,在viewDidLoad中。顺便说一句。设置footeView可以删除“未使用的”分隔符


-1

您要查找表中显示的项目总数还是当前可见的项目总数?无论哪种方式..我都相信'viewDidLoad'方法在所有数据源方法都被调用之后执行。但是,这仅在第一次加载数据时有效(如果使用单个alloc ViewController)。


-1

我正在复制安德鲁的代码,并对其进行扩展以解决表中只有1行的情况。到目前为止,对我来说一直有效!

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// detect when all visible cells have been loaded and displayed
// NOTE: iOS7 workaround used - see: http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview?lq=1
NSArray *visibleRows = [tableView indexPathsForVisibleRows];
NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
BOOL isPreviousCallForPreviousCell = self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
BOOL isFinishedLoadingTableView = isLastCell && ([tableView numberOfRowsInSection:0] == 1 || isPreviousCallForPreviousCell);

self.previousDisplayedIndexPath = indexPath;

if (isFinishedLoadingTableView) {
    [self hideSpinner];
}
}

注意:我只是使用Andrew代码中的1部分,因此请记住这一点。


欢迎使用SO @ jpage4500。我已经编辑了您的答案,以删除有关低代表分数和问题凸显的内容。扩展另一个用户的答案本身就是一个完全有效的答案,因此您可以通过将这些内容排除在外而减少混乱。不过,您还是给了安德鲁功劳,这很不错,所以留下了。
skrrgwasme 2014年

-3

在iOS7.0x中,解决方案有所不同。这是我想出的。

    - (void)tableView:(UITableView *)tableView 
      willDisplayCell:(UITableViewCell *)cell 
    forRowAtIndexPath:(NSIndexPath *)indexPath
{
    BOOL isFinishedLoadingTableView = [self isFinishedLoadingTableView:tableView  
                                                             indexPath:indexPath];
    if (isFinishedLoadingTableView) {
        NSLog(@"end loading");
    }
}

- (BOOL)isFinishedLoadingTableView:(UITableView *)tableView 
                         indexPath:(NSIndexPath *)indexPath
{
    // The reason we cannot just look for the last row is because 
    // in iOS7.0x the last row is updated before
    // looping through all the visible rows in ascending order 
    // including the last row again. Strange but true.
    NSArray * visibleRows = [tableView indexPathsForVisibleRows];   // did verify sorted ascending via logging
    NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
    // For tableviews with multiple sections this will be more complicated.
    BOOL isPreviousCallForPreviousCell = 
             self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
    BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
    BOOL isFinishedLoadingTableView = isLastCell && isPreviousCallForPreviousCell;
    self.previousDisplayedIndexPath = indexPath;
    return isFinishedLoadingTableView;
}

-3

目标C

[self.tableView reloadData];
[self.tableView performBatchUpdates:^{}
                              completion:^(BOOL finished) {
                                  /// table-view finished reload
                              }];

迅速

self.tableView?.reloadData()
self.tableView?.performBatchUpdates({ () -> Void in

                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })

1
UICollectionView据我了解,此方法仅可用。
非常好-o

是的,你是对的。UITableView的beginUpdates等于UICollectionView的performBatchUpdates:completion。stackoverflow.com/a/25128583/988169
pkc456

因此,请对该问题给出正确有效的答案。
G.Abhisek '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.