Como saber quando o UITableView concluiu o ReloadData?

Eu estou tentando rolar para o fundo de um UITableView depois de terminar de executar [self.tableView reloadData]

Eu originalmente tive

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

Mas então eu li que reloadData é asynchronous, então a rolagem não acontece, já que self.tableView , [self.tableView numberOfSections] e [self.tableView numberOfRowsinSection são todos 0.

Obrigado!

O que é estranho é que estou usando:

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

No console, ele retorna Sections = 1, Row = -1;

Quando eu faço exatamente o mesmo NSLogs em cellForRowAtIndexPath eu recebo Seções = 1 e Row = 8; (8 está certo)

O recarregamento acontece durante a próxima passagem de layout, o que normalmente acontece quando você retorna o controle para o loop de execução (depois, digamos, da ação do botão ou o que quer que retorne).

Portanto, uma maneira de executar algo após a recarga da visualização de tabela é simplesmente forçar a exibição da tabela a executar o layout imediatamente:

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

Outra maneira é agendar o código do layout posterior para ser executado posteriormente usando 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]; }); 

ATUALIZAR

Após investigações posteriores, descobri que a exibição de tabela envia tableView:numberOfSections: e tableView:numberOfRowsInSection: para sua origem de dados antes de retornar de reloadData . Se o delegado implementa tableView:heightForRowAtIndexPath: a exibição de tabela também envia isso (para cada linha) antes de retornar de reloadData .

No entanto, a exibição de tabela não envia tableView:cellForRowAtIndexPath: ou tableView:headerViewForSection até a fase de layout, que ocorre por padrão quando você retorna o controle para o loop de execução.

Eu também acho que em um pequeno programa de teste, o código na sua pergunta é rolado corretamente para a parte inferior da visualização de tabela, sem que eu faça nada de especial (como enviar layoutIfNeeded ou usar dispatch_async ).

Rápido:

 extension UITableView { func reloadData(completion: ()->()) { UIView.animateWithDuration(0, animations: { self.reloadData() }) { _ in completion() } } } ...somewhere later... tableView.reloadData { println("done") } 

Objetivo-C:

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

Não é garantido que o método dispatch_async(dispatch_get_main_queue()) acima funcione . Estou vendo um comportamento não-determinístico com ele, em que, às vezes, o sistema concluiu o layoutSubviews e a renderização da célula antes do bloco de conclusão e, às vezes, depois.

Aqui está uma solução que funciona 100% para mim, no iOS 10. Ela exige a capacidade de instanciar o UITableView ou o UICollectionView como uma subclass personalizada. Aqui está a solução UICollectionView, mas é exatamente a mesma para o UITableView:

CustomCollectionView.h:

 #import  @interface CustomCollectionView: UICollectionView - (void)reloadDataWithCompletion:(void (^)(void))completionBlock; @end 

CustomCollectionView.m:

 #import "CustomCollectionView.h" @interface CustomCollectionView () @property (nonatomic, copy) void (^reloadDataCompletionBlock)(void); @end @implementation CustomCollectionView - (void)reloadDataWithCompletion:(void (^)(void))completionBlock { self.reloadDataCompletionBlock = completionBlock; [super reloadData]; } - (void)layoutSubviews { [super layoutSubviews]; if (self.reloadDataCompletionBlock) { self.reloadDataCompletionBlock(); self.reloadDataCompletionBlock = nil; } } @end 

Exemplo de uso:

 [self.collectionView reloadDataWithCompletion:^{ // reloadData is guaranteed to have completed }]; 

Veja aqui uma versão Swift desta resposta

A partir do Xcode 8.2.1, iOS 10 e Swift 3,

Você pode determinar o fim de tableView.reloadData() facilmente usando um bloco CATransaction:

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

O acima também funciona para determinar o fim do reloadData () do UICollectionView e do reloadAllComponents () do UIPickerView.

Eu tive os mesmos problemas que Tyler Sheaffer.

Eu implementei sua solução no Swift e resolvi meus problemas.

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 super.reloadData() } } 

Swift 2:

 class UITableViewWithReloadCompletion: UITableView { var reloadDataCompletionBlock: (() -> Void)? override func layoutSubviews() { super.layoutSubviews() self.reloadDataCompletionBlock?() self.reloadDataCompletionBlock = nil } func reloadDataWithCompletion(completion:() -> Void) { reloadDataCompletionBlock = completion super.reloadData() } } 

Exemplo de uso:

 tableView.reloadDataWithCompletion() { // reloadData is guaranteed to have completed } 

Parece que as pessoas ainda estão lendo esta pergunta e as respostas. B / c disso, estou editando minha resposta para remover a palavra Synchronous, que é realmente irrelevante para isso.

When [tableView reloadData] retorna, as estruturas de dados internas por trás do tableView foram atualizadas. Portanto, quando o método for concluído, você poderá rolar com segurança para a parte inferior. Eu verifiquei isso no meu próprio aplicativo. A resposta amplamente aceita por @ rob-mayoff, embora também confusa na terminologia, reconhece o mesmo em sua última atualização.

Se o seu tableView não estiver rolando para baixo, você pode ter um problema em outro código que você não tenha postado. Talvez você esteja alterando dados após a conclusão da rolagem e você não esteja recarregando e / ou rolando para a parte inferior então?

Adicione alguns logs da seguinte maneira para verificar se os dados da tabela estão corretos após reloadData . Eu tenho o seguinte código em um aplicativo de exemplo e funciona perfeitamente.

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

E uma versão UICollectionView , baseada na resposta da kolaworld:

https://stackoverflow.com/a/43162226/1452758

Precisa de testes. Funciona até agora no iOS 9.2, Xcode 9.2 beta 2, com rolagem de um collectionView para um índice, como um encerramento.

 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. ie 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() } } 

Uso:

 myCollectionView.reloadDataThenPerform { myCollectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true) } 

Eu uso esse truque, tenho certeza que já postei para uma duplicata desta pergunta:

 -(void)tableViewDidLoadRows:(UITableView *)tableView{ // do something after loading, eg 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; } 

Na verdade, este resolveu meu problema:

 -(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { NSSet *visibleSections = [NSSet setWithArray:[[tableView indexPathsForVisibleRows] valueForKey:@"section"]]; if (visibleSections) { // hide the activityIndicator/Loader }} 

Tente assim, vai funcionar

 [tblViewTerms performSelectorOnMainThread:@selector(dataLoadDoneWithLastTermIndex:) withObject:lastTermIndex waitUntilDone:YES];waitUntilDone:YES]; @interface UITableView (TableViewCompletion) -(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex; @end @implementation UITableView(TableViewCompletion) -(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex { NSLog(@"dataLoadDone"); NSIndexPath* indexPath = [NSIndexPath indexPathForRow: [lastTermIndex integerValue] inSection: 0]; [self selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; } @end 

Vou executar quando a tabela estiver completamente carregada

Outra solução é que você pode subclassificar o UITableView

Acabei usando uma variação da solução de Shawn:

Crie uma class UITableView personalizada com um delegado:

 protocol CustomTableViewDelegate { func CustomTableViewDidLayoutSubviews() } class CustomTableView: UITableView { var customDelegate: CustomTableViewDelegate? override func layoutSubviews() { super.layoutSubviews() self.customDelegate?.CustomTableViewDidLayoutSubviews() } } 

Então no meu código, eu uso

 class SomeClass: UIViewController, CustomTableViewDelegate { @IBOutlet weak var myTableView: CustomTableView! override func viewDidLoad() { super.viewDidLoad() self.myTableView.customDelegate = self } func CustomTableViewDidLayoutSubviews() { print("didlayoutsubviews") // DO other cool things here!! } } 

Além disso, certifique-se de configurar sua visualização de tabela para CustomTableView no construtor de interface:

insira a descrição da imagem aqui

Você pode usá-lo para fazer algo depois de recarregar os dados:

 [UIView animateWithDuration:0 animations:^{ [self.contentTableView reloadData]; } completion:^(BOOL finished) { _isUnderwritingUpdate = NO; }]; 

Tente definir atrasos:

 [_tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0.2]; [_activityIndicator performSelector:@selector(stopAnimating) withObject:nil afterDelay:0.2];