Como detectar o fim do carregamento do UITableView

141

Desejo alterar o deslocamento da tabela quando a carga terminar e esse deslocamento depende do número de células carregadas na tabela.

De qualquer forma, no SDK é possível saber quando um carregamento da visualização da uitabl foi concluído? Não vejo nada nos delegados nem nos protocolos de fonte de dados.

Não posso usar a contagem das fontes de dados devido apenas ao carregamento das células visíveis.

Montenegro
fonte
tentar uma combinação de fonte de dados de contagem e 'indexPathsForVisibleRows'
Swapnil Luktuke
1
Re-inaugurado com base em novas informações na bandeira: "Não é duplicar Ele pergunta sobre o carregamento de células visíveis, e não sobre acabamento de dados pedindo Veja atualização para resposta aceita.."
Kev
Esta solução funciona bem para mim. Você verifica isso stackoverflow.com/questions/1483581/…
Khaled Annajar

Respostas:

224

Melhore para a resposta @RichX: lastRowpode ser ambos [tableView numberOfRowsInSection: 0] - 1ou ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. Portanto, o código será:

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

ATUALIZAÇÃO: Bem, o comentário de @ htafoya está certo. Se você deseja que esse código detecte o fim do carregamento de todos os dados da fonte, não seria, mas essa não é a pergunta original. Este código é para detectar quando todas as células que deveriam estar visíveis são exibidas. willDisplayCell:usado aqui para uma interface do usuário mais suave (a única célula geralmente é exibida rapidamente após a willDisplay:chamada). Você também pode tentar tableView:didEndDisplayingCell:.

folex
fonte
Muito melhor para saber quando todas as células carregam que são visíveis.
Eric Eric
14
No entanto, isso será chamado sempre que o usuário rolar para exibir mais células.
Htafoya # 7/13
O problema é que ele não é responsável por uma visualização de rodapé. Pode não ser um problema para a maioria, mas se for, você pode aplicar a mesma idéia, mas no viewForFooterInSectionmétodo.
Kyle Clegg
1
@KyleClegg patric.schenke mencionou uma das razões no comentário à sua resposta. Além disso, e se o rodapé não estiver visível no final do carregamento da célula?
Folex 01/03
27
tableView: didEndDisplayingCell: é realmente chamado ao remover uma célula da exibição, não quando sua renderização na exibição estiver concluída para que não funcione. Não é um bom nome de método. Tenho que ler os documentos.
Andrew Raphael
34

Versão Swift 3 e 4 e 5:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}
Daniele Ceglia
fonte
1
esse método é chamado quando carregamos dados pela primeira vez. No meu caso, existem apenas 4 células visíveis. Carreguei 8 linhas, mas ainda assim essa função estava sendo chamada. O que eu quero é para carregar mais dados quando o usuário rolar para baixo a última linha
Muhammad Nayab
Olá Muhammad Nayab, talvez você possa substituir "if indexPath == lastVisibleIndexPath" por "if indexPath.row == yourDataSource.count"
Daniele Ceglia
1
@DanieleCeglia Obrigado por apontar isso. if indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 isso funcionará para mim. Felicidades!!!
Yogesh Patel
28

Eu sempre uso esta solução muito simples:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == lastRow){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}
RichX
fonte
como detectar a última linha? esse é o problema para mim .. você pode explicar como você coloca essa lastrow nessa condição if?
Sameera Chathuranga
6
A última linha é o [tableView numberOfRowsInSection: 0] - 1. Você deve substituir 0pelo valor necessário. Mas esse não é o problema. O problema é que o UITableView carrega apenas visível. No entanto, ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowresolve o problema.
Folex
1
Se estamos procurando uma conclusão absoluta, não seria melhor usar - (void) tableView: (UITableView *) tableView didEndDisplayingCell: (UITableViewCell *) para forRowAtIndexPath: (NSIndexPath *) indexPath?
morcutt
10

Aqui está outra opção que parece funcionar para mim. No método delegado viewForFooter, verifique se é a seção final e adicione seu código lá. Essa abordagem veio à mente depois de perceber que o willDisplayCell não conta com rodapés se você os tiver.

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

Acho que essa abordagem funciona melhor se você procura encontrar o carregamento final para o todo UITableView, e não simplesmente as células visíveis. Dependendo das suas necessidades, você pode querer apenas as células visíveis; nesse caso, a resposta do folex é uma boa rota.

Kyle Clegg
fonte
Voltando nilde tableView:viewForFooterInSection:mexer com meu layout no iOS 7 usando o Layout automático.
ma11hew28
Interessante. E se você configurá-lo para um quadro com altura e largura 0? return CGRectMake(0,0,0,0);
Kyle Clegg
Eu tentei isso e até me preparei self.tableView.sectionFooterHeight = 0. De qualquer maneira, parece inserir uma visualização de rodapé de seção com uma altura de cerca de 10. Aposto que eu poderia corrigir isso adicionando uma restrição de altura 0 à visualização que retorno. De qualquer forma, estou bem porque realmente queria descobrir como iniciar o UITableView na última célula, mas vi isso primeiro.
ma11hew28
1
@MattDiPasquale se você implementar viewForFooterInSection, também precisará implementar heightForFooterInSection. Ele deve retornar 0 para seções com rodapé nulo. Isso também está nos documentos oficiais agora.
Patric.schenke
viewForFooterInSection não obter chamado se você definir o heightForFooterInSection a 0
Oded Harth
10

Solução 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()
    }
}
fatihyildizhan
fonte
9

Usando API privada:

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

Usando API pública:

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

Um design melhor possível é adicionar as células visíveis a um conjunto; quando você precisar verificar se a tabela está carregada, poderá fazer um loop for em torno desse conjunto, por exemplo

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)
        }
    }
}
malhal
fonte
Solução muito melhor que as outras. Não requer uma recarga do tableView. É muito simples e o viewDidLoadRows: não é chamado toda vez que a última célula é carregada.
Matt Hudson
esta é a melhor solução até agora, já que as outras soluções não toma as várias seções em conta
justicepenny
Sinto muito pelo comentário sarcástico, mas como exatamente isso é 'mágica'? Chamando para outro método do delegado. Eu não entendo.
Lukas Petr
@LukasPetr dê uma olhada no cancelamento e no afterDelay. Eles permitem que a chamada do método seja cancelada para cada seção, exceto a última, que é quando a tabela termina o carregamento completamente.
malhal
@ malhal oh, tudo bem. É um pouco mais inteligente do que eu pensava à primeira vista.
Lukas Petr
8

Para a versão de resposta escolhida no 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
        }
    }
}

Eu precisava da variável isLoadingTableView porque queria ter certeza de que a tabela está sendo carregada antes de fazer uma seleção de célula padrão. Se você não incluir isso, toda vez que rolar a tabela, ele invocará seu código novamente.

Travis M.
fonte
6

A melhor abordagem que conheço é a resposta de Eric em: Seja notificado quando o UITableView terminar de solicitar dados?

Atualização: para que funcione, tenho que fazer essas chamadas -tableView:cellForRowAtIndexPath:

[tableView beginUpdates];
[tableView endUpdates];
René Retz
fonte
Por quanto vale, eu tenho usado esse método muito simples em todos os meus projetos há meses e funciona perfeitamente.
Eric MORAND 22/03
3

Para saber quando uma visualização de tabela termina de carregar seu conteúdo, primeiro precisamos ter um entendimento básico de como as visualizações são colocadas na tela.

No ciclo de vida de um aplicativo, existem quatro momentos principais:

  1. O aplicativo recebe um evento (toque, cronômetro, bloco despachado etc.)
  2. O aplicativo lida com o evento (modifica uma restrição, inicia uma animação, altera o plano de fundo etc.)
  3. O aplicativo calcula a nova hierarquia de visualizações
  4. O aplicativo processa a hierarquia de visualizações e exibe-a

Os tempos 2 e 3 são totalmente separados. Por quê ? Por razões de desempenho, não queremos realizar todos os cálculos do momento 3 cada vez que uma modificação é feita.

Então, acho que você está enfrentando um caso como este:

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

O que há de errado aqui?

Como qualquer visualização, uma visualização de tabela recarrega seu conteúdo lentamente. Na verdade, se você ligar reloadDatavárias vezes, isso não criará problemas de desempenho. A exibição da tabela apenas recalcula o tamanho do conteúdo com base na implementação delegada e aguarda o momento 3 para carregar suas células. Esse tempo é chamado de passe de layout.

Ok, como entrar no passe de layout?

Durante a aprovação do layout, o aplicativo calcula todos os quadros da hierarquia de visualizações. Para se envolver, você pode substituir os métodos dedicados layoutSubviews, updateLayoutConstraintsetc em a UIViewe os métodos equivalentes em uma subclasse do controlador de exibição.

É exatamente isso que uma exibição de tabela faz. Ele substitui layoutSubviewse, com base na implementação delegada, adiciona ou remove células. Ele chama cellForRowlogo antes de adicionar e colocar uma nova célula, willDisplaylogo depois. Se você chamou reloadDataou acabou de adicionar a exibição de tabela à hierarquia, a exibição de tabelas adiciona quantas células forem necessárias para preencher seu quadro nesse momento chave.

Tudo bem, mas agora, como saber quando uma exibição de tabelas terminou de recarregar seu conteúdo?

Agora podemos reformular esta pergunta: como saber quando uma exibição de tabela terminou de exibir suas subvisões?

A maneira mais fácil é entrar no layout da exibição da tabela:

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

Observe que esse método é chamado várias vezes no ciclo de vida da visualização da tabela. Devido ao comportamento de rolagem e desenfileiramento da exibição de tabela, as células são modificadas, removidas e adicionadas frequentemente. Mas funciona logo após o super.layoutSubviews()carregamento das células. Esta solução é equivalente a aguardar o willDisplayevento do último caminho do índice. Este evento é chamado durante a execução delayoutSubviews exibição da tabela para cada célula adicionada.

Outra maneira é ser chamado quando o aplicativo termina um passe de layout.

Conforme descrito na documentação , você pode usar uma opção do UIView.animate(withDuration:completion):

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

Essa solução funciona, mas a tela é atualizada uma vez entre o momento em que o layout é concluído e o horário em que o bloco é chamado. Isso é equivalente à DispatchMain.asyncsolução, mas especificado.

Como alternativa, eu preferiria forçar o layout da exibição da tabela

Existe um método dedicado para forçar qualquer visualização a calcular imediatamente seus quadros de subvisão layoutIfNeeded:

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

Tenha cuidado, no entanto, isso removerá o carregamento lento usado pelo sistema. Chamar esses métodos repetidamente pode criar problemas de desempenho. Verifique se eles não serão chamados antes que o quadro da visualização da tabela seja computado, caso contrário, a visualização da tabela será carregada novamente e você não será notificado.


Eu acho que não há solução perfeita. Classes de subclassificação podem levar a trubles. Um passe de layout começa de cima e vai para baixo, para que não seja fácil ser notificado quando todo o layout estiver pronto. E layoutIfNeeded()pode criar problemas de desempenho, etc. Mas conhecendo essas opções, você deve pensar em uma alternativa que atenda às suas necessidades.

GaétanZ
fonte
1

Aqui está como você faz isso no 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
    }

    // ....
}
Ondrej Kvasnovsky
fonte
1

aqui está como eu faço isso no 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()
    }
}
AbdulMomen عبدالمؤمن
fonte
1

Aqui está o que eu faria.

  1. Na sua classe base (pode ser rootVC BaseVc etc),

    A. Escreva um protocolo para enviar o retorno de chamada "DidFinishReloading".

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

    B. Escreva um método genérico para recarregar a exibição da tabela.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
  2. Na implementação do método da classe base, chame reloadData seguido por delegateMethod com atraso.

    -(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. Confirme o protocolo de conclusão de recarregamento em todos os controladores de exibição em que você precisa do retorno de chamada.

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

Referência: https://discussions.apple.com/thread/2598339?start=0&tstart=0

Suhas Aithal
fonte
0

A resposta @folex está certa.

Mas falhará se o tableView tiver mais de uma seção exibida por vez.

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

 }
}
umakanta
fonte
0

No Swift, você pode fazer algo assim. A condição a seguir será verdadeira sempre que você chegar ao final da tabela

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

Se você tiver várias seções, veja como obter a última linha na última seção (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

        }
    }
}
Daniel McLean
fonte
0

Acidentalmente, encontrei esta solução:

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

Você precisa definir o footerView antes de obter o contentSize, por exemplo, em viewDidLoad. Btw. definir o footeView permite excluir separadores "não utilizados"

Kowboj
fonte
-1

Você está procurando o número total de itens que serão exibidos na tabela ou o total de itens atualmente visíveis? De qualquer maneira .. Eu acredito que o método 'viewDidLoad' é executado depois que todos os métodos da fonte de dados são chamados. No entanto, isso funcionará apenas no primeiro carregamento dos dados (se você estiver usando um único ViewController de alocação).

DerekH
fonte
-1

Estou copiando o código de Andrew e expandindo-o para explicar o caso em que você tem apenas 1 linha na tabela. Está funcionando até agora para mim!

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

NOTA: Estou apenas usando uma seção do código de Andrew, tenha isso em mente.

jpage4500
fonte
Bem-vindo ao SO @ jpage4500. Editei sua resposta para remover as informações sobre pontos baixos de repetição e o aumento de perguntas. Expandir a resposta de outro usuário é uma resposta perfeitamente válida por si só, para que você possa reduzir a confusão deixando essas coisas de fora. É bom que você tenha dado o crédito a Andrew, para que ficasse.
Skrrgwasme
-3

No iOS7.0x, a solução é um pouco diferente. Aqui está o que eu criei.

    - (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 Raphael
fonte
-3

Objetivo C

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

Rápido

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

                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })
pkc456
fonte
1
Este método está disponível apenas UICollectionViewaté onde eu entendo.
Impressionante-o
Sim você está certo. O beginUpdates do UITableView é igual ao performBatchUpdates: conclusão do UICollectionView. stackoverflow.com/a/25128583/988169
pkc456
Portanto, dê uma resposta adequada e válida para a pergunta.
G.Abhisek