如何检测 UITableView 的加载结束

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

不管怎样,在 SDK 上是否知道 uitableview 加载何时结束?我在委托和数据源协议上都没有看到任何内容。

我不能使用数据源的计数,因为只加载可见单元格。

158180 次浏览

您是在查找将显示在表中的项的总数还是当前可见项的总数?不管怎样。.我相信“ viewDidLoad”方法是在调用所有数据源方法之后执行的。但是,这只能在数据的第一次加载时起作用(如果您使用单个 alloc ViewController)。

我所知道的最好的方法是 Eric 的答案: 当 UITableView 已经完成数据请求时获得通知?

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

[tableView beginUpdates];
[tableView endUpdates];

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

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

改成@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:试试。

这是另一个对我有用的选择。在 viewForFooter 委托方法中,检查它是否是最后一部分,并在那里添加代码。这种方法是在意识到 will DisplayCell 不会计算页脚(如果你有页脚的话)之后想到的。

- (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 的回答是一个很好的路线。

在 iOS7.0 x 中,解决方案有所不同。

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

我正在复制 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 代码中的一个部分,所以请记住这一点。

@ folex 回答正确。

但是,如果 tableView 一次显示 不止一部分,那么它将返回 失败

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


}
}

使用私有 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)
}
}
}

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

在斯威夫特你可以做这样的事情。每次到达 tableView 的末尾时,以下条件将为 true

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

快速解决方案:

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

我知道答案是肯定的,我只是加上一条建议。

根据以下文档

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

修复调度异步的计时问题是一个糟糕的主意。我建议我们应该通过添加 FLAG 或其他方式来处理这个问题。

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

以下是你在 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. 在基类中(可以是 rootVC BaseVc 等) ,

    编写一个协议来发送“ DidFinishReloding”回调。

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

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

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
    
  2. In the base class method implementation, call reloadData followed by delegateMethod with delay.

    -(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. Confirm to the reload completion protocol in all the view controllers where you need the callback.

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

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

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

如果您有多个部分,以下是如何获取最后一部分中的最后一行(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


}
}
}

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

我无意中发现了这个解决方案:

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

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

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

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

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

二次和三次是完全分开的。出于性能原因,我们不希望在每次修改完成时执行所有的计算(在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执行期间调用此事件。

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

文件所述,您可以使用 UIView.animate(withDuration:completion)的一个选项:

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

这个解决方案可以工作,但是在布局完成和块执行之间,屏幕将刷新一次。这相当于 DispatchMain.async解决方案,但指定。

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

有一种专门的方法可以迫使任何视图立即计算其子视图帧 layoutIfNeeded:

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

但是要小心,这样做将删除系统使用的延迟加载。重复调用这些方法可能会造成性能问题。确保在计算表视图的框架之前不会调用它们,否则表视图将再次加载,而且不会通知您。


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

UITableView + Paging 启用 AND 调用 scrollToRow (. .)在该页面上启动。

迄今为止最好的丑陋解决方案:/

override func viewDidLoad() {
super.viewDidLoad()
    

<#UITableView#>.reloadData()
<#IUTableView#>.alpha = .zero
}


override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)


DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.<#IUTableView#>.scrollToRow(at: <#IndexPath#>, at: .none, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self?.<#IUTableView#>.alpha = 1
}
}
}