如何判断UITableView何时完成了ReloadData?

Ala*_*lan 167 objective-c uitableview reloaddata

我想在完成表演后滚动到UITableView的底部 [self.tableView reloadData]

我原本有

 [self.tableView reloadData]
 NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];

[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Run Code Online (Sandbox Code Playgroud)

但后来我读了reloadData是异步的,所以滚动不因为发生self.tableView,[self.tableView numberOfSections]并且[self.tableView numberOfRowsinSection都是0.

谢谢!

有什么奇怪的是我正在使用:

[self.tableView reloadData];
NSLog(@"Number of Sections %d", [self.tableView numberOfSections]);
NSLog(@"Number of Rows %d", [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1);
Run Code Online (Sandbox Code Playgroud)

在控制台中它返回Sections = 1,Row = -1;

当我完成相同的NSLogs时,cellForRowAtIndexPath我得到Sections = 1和Row = 8; (8是对的)

rob*_*off 273

接下来的布局传递,当您返回控制运行循环(后,说,你的按钮操作或任何返回),这通常发生在重载情况.

因此,在表视图重新加载后运行某些东西的一种方法就是强制表视图立即执行布局:

[self.tableView reloadData];
[self.tableView layoutIfNeeded];
 NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Run Code Online (Sandbox Code Playgroud)

另一种方法是使用以下方法安排后布局代码以便以后运行dispatch_async:

[self.tableView reloadData];

dispatch_async(dispatch_get_main_queue(), ^{
     NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection:([self.tableView numberOfSections]-1)];

    [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
});
Run Code Online (Sandbox Code Playgroud)

UPDATE

经进一步调查,我发现表视图将tableView:numberOfSections:tableView:numberOfRowsInSection:它的数据源来自返回之前reloadData.如果委托实现tableView:heightForRowAtIndexPath:,表视图也会在返回之前发送(对于每一行)reloadData.

但是,表视图不会发送tableView:cellForRowAtIndexPath:tableView:headerViewForSection直到布局阶段,默认情况下,当您将控制返回到运行循环时.

我还发现,在一个很小的测试程序中,你问题中的代码正确地滚动到表视图的底部,没有我做任何特殊的事情(比如发送layoutIfNeeded或使用dispatch_async).

  • 方法2因某些未知原因对我不起作用,但选择了第一种方法. (6认同)
  • `dispatch_async(dispatch_get_main_queue())`方法不能保证工作.我看到它的非确定性行为,有时系统已经完成了layoutSubviews和完成块之前的单元格渲染,有时甚至完成.我将在下面发布一个对我有用的答案. (4认同)
  • @rob,根据表数据源的大小,您可以在同一个运行循环中动画到tableview的底部.如果你用一个巨大的表尝试你的测试代码,你使用GCD延迟滚动直到下一个运行循环的技巧将起作用,而立即滚动将失败.但无论如何,谢谢你的伎俩!! (3认同)
  • 同意`dispatch_async(dispatch_get_main_queue())`并不总是有效.在这里看到随机结果. (3认同)

Avi*_*oss 100

迅速:

extension UITableView {
    func reloadData(completion: ()->()) {
        UIView.animateWithDuration(0, animations: { self.reloadData() })
            { _ in completion() }
    }
}

...somewhere later...

tableView.reloadData {
    println("done")
}
Run Code Online (Sandbox Code Playgroud)

Objective-C的:

[UIView animateWithDuration:0 animations:^{
    [myTableView reloadData];
} completion:^(BOOL finished) {
    //Do something after that...
}];
Run Code Online (Sandbox Code Playgroud)

  • 这相当于在"不久的将来"在主线程上调度一些东西.您可能只是看到表视图在主线程取消完成块之前呈现对象.不建议首先进行这种黑客攻击,但无论如何,如果你打算尝试这个,你应该使用dispatch_after. (16认同)

小智 43

从Xcode 8.2.1,iOS 10和swift 3开始,

您可以tableView.reloadData()使用CATransaction块轻松确定结束:

CATransaction.begin()
CATransaction.setCompletionBlock({
    print("reload completed")
    //Your completion code here
})
print("reloading")
tableView.reloadData()
CATransaction.commit()
Run Code Online (Sandbox Code Playgroud)

以上也适用于确定UICollectionView的reloadData()和UIPickerView的reloadAllComponents()的结束.


Tyl*_*fer 31

上述dispatch_async(dispatch_get_main_queue())方法无法保证正常工作.我看到它的非确定性行为,有时系统已经完成了layoutSubviews和完成块之前的单元格渲染,有时甚至完成.

这是一个在iOS 10上为我100%工作的解决方案.它需要能够将UITableView或UICollectionView实例化为自定义子类.这是UICollectionView解决方案,但它与UITableView完全相同:

CustomCollectionView.h:

#import <UIKit/UIKit.h>

@interface CustomCollectionView: UICollectionView

- (void)reloadDataWithCompletion:(void (^)(void))completionBlock;

@end
Run Code Online (Sandbox Code Playgroud)

CustomCollectionView.m:

#import "CustomCollectionView.h"

@interface CustomCollectionView ()

@property (nonatomic, copy) void (^reloadDataCompletionBlock)(void);

@end

@implementation CustomCollectionView

- (void)reloadDataWithCompletion:(void (^)(void))completionBlock
{
    self.reloadDataCompletionBlock = completionBlock;
    [self reloadData];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (self.reloadDataCompletionBlock) {
        self.reloadDataCompletionBlock();
        self.reloadDataCompletionBlock = nil;
    }
}

@end
Run Code Online (Sandbox Code Playgroud)

用法示例:

[self.collectionView reloadDataWithCompletion:^{
    // reloadData is guaranteed to have completed
}];
Run Code Online (Sandbox Code Playgroud)

请参阅此处获取此答案的Swift版本

  • 在`layoutSubviews`中调用块之后,它应该设置为`nil`,因为后续调用`layoutSubviews`,不一定是因为`reloadData`被调用,将导致块被执行,因为有一个强引用被保持,这不是理想的行为. (2认同)
  • 这个答案的一个补充是,如果只有一个回调可能会破坏现有的回调,这意味着多个呼叫者将有竞争条件.解决方案是使`reloadDataCompletionBlock`成为一个块数组,并在执行时迭代它们并在此之后清空数组. (2认同)

小智 27

我和Tyler Sheaffer有同样的问题.

我在Swift中实现了他的解决方案,它解决了我的问题.

Swift 3.0:

final class UITableViewWithReloadCompletion: UITableView {
  private var reloadDataCompletionBlock: (() -> Void)?

  override func layoutSubviews() {
    super.layoutSubviews()

    reloadDataCompletionBlock?()
    reloadDataCompletionBlock = nil
  }


  func reloadDataWithCompletion(completion: @escaping () -> Void) {
    reloadDataCompletionBlock = completion
    self.reloadData()
  }
}
Run Code Online (Sandbox Code Playgroud)

斯威夫特2:

class UITableViewWithReloadCompletion: UITableView {

  var reloadDataCompletionBlock: (() -> Void)?

  override func layoutSubviews() {
    super.layoutSubviews()

    self.reloadDataCompletionBlock?()
    self.reloadDataCompletionBlock = nil
  }

  func reloadDataWithCompletion(completion:() -> Void) {
      reloadDataCompletionBlock = completion
      self.reloadData()
  }
}
Run Code Online (Sandbox Code Playgroud)

用法示例:

tableView.reloadDataWithCompletion() {
 // reloadData is guaranteed to have completed
}
Run Code Online (Sandbox Code Playgroud)


XJo*_*nes 6

看来人们仍在阅读这个问题和答案.B/c,我正在编辑我的答案,删除Synchronous这个与此无关的词.

When [tableView reloadData]返回后,tableView后面的内部数据结构已经更新.因此,当方法完成时,您可以安全地滚动到底部.我在自己的应用程序中验证了这一点 @ rob-mayoff广泛接受的答案虽然在术语上也令人困惑,但在他的上一次更新中也承认了这一点.

如果您tableView没有滚动到底部,则可能在您尚未发布的其他代码中存在问题.也许您在滚动完成后更改数据并且您没有重新加载和/或滚动到底部?

添加一些日志记录如下,以验证表数据是否正确reloadData.我在示例应用程序中有以下代码,它运行良好.

// change the data source

NSLog(@"Before reload / sections = %d, last row = %d",
      [self.tableView numberOfSections],
      [self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);

[self.tableView reloadData];

NSLog(@"After reload / sections = %d, last row = %d",
      [self.tableView numberOfSections],
      [self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);

[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]-1
                                                          inSection:[self.tableView numberOfSections] - 1]
                      atScrollPosition:UITableViewScrollPositionBottom
                              animated:YES];
Run Code Online (Sandbox Code Playgroud)

  • `reloadData`不是同步的.它曾经是 - 看到这个答案:http://stackoverflow.com/a/16071589/193896 (8认同)
  • 多么有趣的时光.这是2014年,并且存在关于某些方法是同步还是异步的争论.感觉像猜测.所有实现细节在该方法名称后面完全不透明.编程不是很好吗? (3认同)

Wom*_*ble 6

还有一个UICollectionView基于kolaworld答案的版本:

/sf/answers/3021355851/

需要测试.到目前为止,在iOS 9.2,Xcode 9.2 beta 2上工作,将collectionView滚动到索引,作为闭包.

extension UICollectionView
{
    /// Calls reloadsData() on self, and ensures that the given closure is
    /// called after reloadData() has been completed.
    ///
    /// Discussion: reloadData() appears to be asynchronous. i.e. the
    /// reloading actually happens during the next layout pass. So, doing
    /// things like scrolling the collectionView immediately after a
    /// call to reloadData() can cause trouble.
    ///
    /// This method uses CATransaction to schedule the closure.

    func reloadDataThenPerform(_ closure: @escaping (() -> Void))
    {       
        CATransaction.begin()
            CATransaction.setCompletionBlock(closure)
            self.reloadData()
        CATransaction.commit()
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

myCollectionView.reloadDataThenPerform {
    myCollectionView.scrollToItem(at: indexPath,
            at: .centeredVertically,
            animated: true)
}
Run Code Online (Sandbox Code Playgroud)


mal*_*hal 5

我使用这个技巧,很确定我已将它发布到这个问题的副本:

-(void)tableViewDidLoadRows:(UITableView *)tableView{
    // do something after loading, e.g. select a cell.
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // trick to detect when table view has finished loading.
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];
    [self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];

    // specific to your controller
    return self.objects.count;
}
Run Code Online (Sandbox Code Playgroud)