【问题标题】:How to detect the end of loading of UITableView如何检测 UITableView 加载结束
【发布时间】:2011-05-08 23:51:01
【问题描述】:

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

SDK 是否可以知道 uitableview 加载何时完成?我在委托和数据源协议上都看不到任何东西。

由于仅加载可见单元格,我无法使用数据源的计数。

【问题讨论】:

  • 尝试数据源计数和'indexPathsForVisibleRows'的组合
  • 根据标志中的进一步信息重新打开:“它不是重复的。它询问加载可见单元格,而不是询问数据的完成。请参阅接受答案的更新”
  • 这个解决方案对我来说很好。你检查一下stackoverflow.com/questions/1483581/…

标签: ios iphone uitableview


【解决方案1】:

改进@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: 用于更流畅的 UI(单个单元格通常在 willDisplay: 调用后快速显示)。你也可以用tableView:didEndDisplayingCell:试试。

【讨论】:

  • 更好地了解何时加载所有可见的单元格。
  • 但是,只要用户滚动查看更多单元格,就会调用它。
  • 这个问题是它没有考虑页脚视图。对大多数人来说可能不是问题,但如果是,您可以应用相同的想法,但在 viewForFooterInSection 方法中。
  • @KyleClegg patric.schenke 在评论您的回答时提到了其中一个原因。另外,如果在单元格加载结束时页脚不可见怎么办?
  • tableView:didEndDisplayingCell: 实际上是在从视图中删除单元格时调用的,而不是在视图中的渲染完成时调用,这样就不起作用了。不是一个好的方法名称。必须阅读文档。
【解决方案2】:

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...
        }
    }
}

【讨论】:

  • 这个方法在我们第一次加载数据时被调用。就我而言,只有 4 个可见单元格。我加载了 8 行,但仍然调用了这个函数。我想要的是当用户向下滚动到最后一行时加载更多数据
  • 嗨 Muhammad Nayab,也许您可​​以将“if indexPath == lastVisibleIndexPath”替换为“if indexPath.row == yourDataSource.count”
  • @DanieleCeglia 感谢您指出这一点。如果 indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 这对我有用。干杯!!!
【解决方案3】:

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

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

【讨论】:

  • 如何检测最后一行?这就是我的问题..你能解释一下你是如何让最后一个进入那个 if 条件的吗?
  • 最后一行是[tableView numberOfRowsInSection: 0] - 1。您必须将 0 替换为所需的值。但这不是问题。问题是 UITableView 加载只可见。不过((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row 解决了这个问题。
  • 如果我们正在寻找绝对完成不是最好使用 - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath?
【解决方案4】:

这是另一个似乎对我有用的选项。在 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's answer 是一个不错的途径。

【讨论】:

  • tableView:viewForFooterInSection: 返回 nil 扰乱了我在 iOS 7 中使用自动布局的布局。
  • 有趣。如果将其设置为高度和宽度为 0 的框架怎么办? return CGRectMake(0,0,0,0);
  • 我试过了,甚至设置了self.tableView.sectionFooterHeight = 0。无论哪种方式,它似乎都插入了一个高度约为 10 的节页脚视图。我敢打赌,我可以通过向返回的视图添加 0 高度约束来解决这个问题。无论如何,我很好,因为我实际上想弄清楚how to start UITableView on the last cell,但首先看到了这个。
  • @MattDiPasquale 如果你实现 viewForFooterInSection,你还必须实现 heightForFooterInSection。对于页脚为零的部分,它必须返回 0。这也在官方文档中。
  • 如果将 heightForFooterInSection 设置为 0,则不会调用 viewForFooterInSection
【解决方案5】:

使用私有 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 哦,好的。乍一看,它比我想象的要聪明一些。
【解决方案6】:

快速解决方案:

// 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()
    }
}

【讨论】:

    【解决方案7】:

    对于 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 变量,因为我想在进行默认单元格选择之前确保表格已完成加载。如果你不包括这个,那么每次你滚动表格时它都会再次调用你的代码。

    【讨论】:

      【解决方案8】:

      我所知道的最佳方法是 Eric 的回答:Get notified when UITableView has finished asking for data?

      更新:为了让它工作,我必须把这些电话放在-tableView:cellForRowAtIndexPath:

      [tableView beginUpdates];
      [tableView endUpdates];
      

      【讨论】:

      • 不管怎样,这很值得,几个月来我一直在所有项目中使用这种非常简单的方法,并且效果很好。
      【解决方案9】:

      要知道表格视图何时完成加载其内容,我们首先需要对视图在屏幕上的显示方式有一个基本的了解。

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

      1. 应用收到事件(触摸、定时器、块调度等)
      2. 应用处理事件(它修改约束、启动动画、更改背景等)
      3. 应用计算新的视图层次结构
      4. 应用呈现视图层次结构并显示它

      第 2 次和第 3 次是完全分开的。 为什么?出于性能原因,我们不想在每次修改完成时都执行所有计算(在 3 完成)。

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

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

      这里出了什么问题?

      表格视图会延迟重新加载其内容。实际上,如果你多次调用reloadData 不会产生性能问题。表格视图仅根据其委托实现重新计算其内容大小,并等待时刻 3 加载其单元格。这一次称为布局传递。

      好的,如何参与布局关卡?

      在布局过程中,应用计算视图层次结构的所有帧。要参与其中,您可以覆盖 UIView 子类中的专用方法 layoutSubviewsupdateLayoutConstraints 等以及视图控制器子类中的等效方法。

      这正是表格视图的作用。它覆盖 layoutSubviews 并根据您的委托实现添加或删除单元格。它在添加和布局新单元之前调用cellForRow,之后调用willDisplay。如果您调用了reloadData 或只是将表格视图添加到层​​次结构中,表格视图会根据需要添加尽可能多的单元格以在此关键时刻填充其框架。

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

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

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

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

      注意,这个方法在表格视图的生命周期中会被多次调用。由于表格视图的滚动和出列行为,单元格经常被修改、删除和添加。但它可以在super.layoutSubviews() 之后立即加载单元格。这个解决方案相当于等待最后一个索引路径的willDisplay事件。添加单元格时,在表格视图的layoutSubviews 执行期间调用此事件。

      另一种方法是在应用完成布局传递时收到通知。

      documentation 中所述,您可以使用UIView.animate(withDuration:completion) 的选项:

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

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

      或者,我更愿意强制表格视图的布局

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

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

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


      我认为没有完美的解决方案。子类化可能会导致麻烦。布局通道从顶部开始到底部,因此当所有布局完成时不容易收到通知。而layoutIfNeeded() 可能会造成性能问题等。

      【讨论】:

        【解决方案10】:

        以下是您在 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
            }
        
            // ....
        }
        

        【讨论】:

          【解决方案11】:

          这是我在 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()
              }
          }
          

          【讨论】:

            【解决方案12】:

            这就是我要做的。

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

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

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

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

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

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

            【讨论】:

              【解决方案13】:

              我正在复制 Andrew 的代码并将其扩展以解决表格中只有 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。我已经编辑了您的答案,以删除有关低代表点和问题凸点的内容。扩展另一个用户的答案本身就是一个完全有效的答案,因此您可以通过将这些内容排除在外来减少混乱。很高兴你给了 Andrew 荣誉,所以留下了。
              【解决方案14】:

              @folex 的回答是对的。

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

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

              【讨论】:

                【解决方案15】:

                在 Swift 中你可以做这样的事情。每次到达 tableView 末尾时,以下条件都会为真

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

                【讨论】:

                  【解决方案16】:

                  如果你有多个部分,这里是如何获取最后一个部分的最后一行(Swift 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
                  
                          }
                      }
                  }
                  

                  【讨论】:

                    【解决方案17】:

                    我很不小心碰到了这个解决方案:

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

                    您需要在获取 contentSize 之前设置 footerView,例如在 viewDidLoad 中。 顺便提一句。设置 footeView 可以让您删除“未使用”的分隔符

                    【讨论】:

                      【解决方案18】:

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

                      【讨论】:

                        【解决方案19】:

                        我知道这已经回答了,我只是添加一个建议。

                        根据以下文档

                        https://www.objc.io/issues/2-concurrency/thread-safe-class-design/

                        使用 dispatch_async 修复时间问题是个坏主意。我建议我们应该通过添加 FLAG 或其他东西来处理这个问题。

                        【讨论】:

                          【解决方案20】:

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

                          【讨论】:

                            【解决方案21】:

                            目标 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
                                                        })
                            

                            【讨论】:

                            • 据我了解,此方法仅适用于UICollectionView
                            • 是的,你是对的。 UITableView 的 beginUpdates 等于 UICollectionView 的 performBatchUpdates:completion。 stackoverflow.com/a/25128583/988169
                            • 所以给这个问题一个正确有效的答案。
                            猜你喜欢
                            • 1970-01-01
                            • 2011-11-29
                            • 2012-06-22
                            • 1970-01-01
                            • 2012-10-30
                            • 1970-01-01
                            • 1970-01-01
                            相关资源
                            最近更新 更多