Cómo detectar el final de la carga de UITableView

141

Quiero cambiar el desplazamiento de la tabla cuando finaliza la carga y ese desplazamiento depende del número de celdas cargadas en la tabla.

¿De todos modos está en el SDK saber cuándo ha finalizado la carga de uitableview? No veo nada en el delegado ni en los protocolos de origen de datos.

No puedo usar el recuento de las fuentes de datos solo por la carga de las celdas visibles.

emenegro
fuente
pruebe una combinación de recuento de fuente de datos y 'indexPathsForVisibleRows'
Swapnil Luktuke el
1
Reabrió en función de más información en el marcador: "No está duplicado. Pregunta sobre la carga de celdas visibles, no sobre el final de la consulta de datos. Consulte la actualización de la respuesta aceptada"
Kev
Esta solución me funciona bien. Lo verificas stackoverflow.com/questions/1483581/…
Khaled Annajar

Respuestas:

224

Mejora a la respuesta @RichX: lastRowpuede ser ambos [tableView numberOfRowsInSection: 0] - 1o ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. Entonces el 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];
    }
}

ACTUALIZACIÓN: Bueno, el comentario de @ htafoya es correcto. Si desea que este código detecte el final de la carga de todos los datos desde el origen, no lo haría, pero esa no es la pregunta original. Este código es para detectar cuándo se muestran todas las celdas que deben ser visibles. willDisplayCell:se usa aquí para una interfaz de usuario más fluida (una sola celda generalmente se muestra rápidamente después de la willDisplay:llamada). También puedes probarlo con tableView:didEndDisplayingCell:.

folex
fuente
Mucho mejor para saber cuándo se cargan todas las celdas que son visibles.
Eric
14
Sin embargo, esto se llamará siempre que el usuario se desplace para ver más celdas.
htafoya
El problema con esto es que no tiene en cuenta una vista de pie de página. Puede que no sea un problema para la mayoría, pero si lo es, puede aplicar la misma idea pero en el viewForFooterInSectionmétodo.
Kyle Clegg
1
@KyleClegg patric.schenke mencionó una de las razones en el comentario a su respuesta. Además, ¿qué pasa si el pie de página no es visible al final de la carga de la celda?
folex
27
tableView: didEndDisplayingCell: en realidad se llama al eliminar una celda de la vista, no cuando se completa su representación en la vista para que eso no funcione. No es un buen nombre de método. Tengo que leer los documentos.
Andrew Raphael
34

Versión Swift 3 y 4 y 5:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}
Daniele Ceglia
fuente
1
Este método se llama la primera vez que cargamos datos. En mi caso solo hay 4 celdas visibles. Cargué 8 filas pero aún se llamaba a esta función. Lo que quiero es cargar más datos cuando el usuario se desplaza hacia abajo hasta la última fila
Muhammad Nayab
Hola Muhammad Nayab, tal vez podrías reemplazar "if indexPath == lastVisibleIndexPath" con "if indexPath.row == yourDataSource.count"
Daniele Ceglia
1
@DanieleCeglia Gracias por señalar esto. if indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 esto va a funcionar para mí. ¡¡¡Salud!!!
Yogesh Patel
28

Siempre uso esta solución muy simple:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == lastRow){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}
RichX
fuente
¿Cómo detectar la última fila? ese es el problema para mí ... ¿puede explicar cómo consigue ese efecto lastrow en esa condición?
Sameera Chathuranga
66
La última fila es la [tableView numberOfRowsInSection: 0] - 1. Debe reemplazar 0por el valor necesario. Pero ese no es el problema. El problema es que UITableView solo carga las cargas visibles. Sin embargo ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowresuelve el problema.
folex
1
Si estamos buscando una terminación absoluta, ¿no sería mejor usar - (void) tableView: (UITableView *) tableView didEndDisplayingCell: (UITableViewCell *) cell forRowAtIndexPath: (NSIndexPath *) indexPath?
morcutt
10

Aquí hay otra opción que parece funcionar para mí. En el método de delegado viewForFooter, verifique si es la sección final y agregue su código allí. Este enfoque vino a mi mente después de darse cuenta de que willDisplayCell no tiene en cuenta los pies de página si los tiene.

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

Creo que este enfoque funciona mejor si está buscando encontrar la carga final para todo UITableView, y no simplemente las celdas visibles. Dependiendo de sus necesidades, es posible que solo desee las celdas visibles, en cuyo caso la respuesta de folex es una buena ruta.

Kyle Clegg
fuente
Volviendo nilde tableView:viewForFooterInSection:enredado con mi diseño en iOS 7 usando Auto Layout.
ma11hew28
Interesante. ¿Qué pasa si lo configura en un marco con altura y ancho 0? return CGRectMake(0,0,0,0);
Kyle Clegg
Lo intenté e incluso lo puse self.tableView.sectionFooterHeight = 0. De cualquier manera, parece insertar una vista de pie de página de sección con una altura de aproximadamente 10. Apuesto a que podría solucionar esto agregando una restricción de altura 0 a la vista que regreso. De todos modos, soy bueno porque realmente quería descubrir cómo iniciar UITableView en la última celda, pero vi esto primero.
ma11hew28
1
@MattDiPasquale si implementa viewForFooterInSection, también debe implementar heightForFooterInSection. Tiene que devolver 0 para secciones con un pie de página nulo. Esto también está en los documentos oficiales por ahora.
patric.schenke
viewForFooterInSection no se llama si establece la heightForFooterInSection en 0
Oded Harth
10

Solución 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
fuente
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;
}

Un posible mejor diseño es agregar las celdas visibles a un conjunto, luego, cuando necesite verificar si la tabla está cargada, puede hacer un bucle for alrededor de este conjunto, por ejemplo

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
fuente
Una solución mucho mejor que las demás. No requiere una recarga de tableView. Es muy simple y viewDidLoadRows: no se llama cada vez que se carga la última celda.
Matt Hudson el
Esta es la mejor solución hasta ahora, ya que las otras soluciones no tienen en cuenta las múltiples secciones
Justicepenny
Lamento el comentario sarcástico, pero ¿cómo es exactamente esta 'magia'? Llamando a otro método del delegado. No lo entiendo
Lukas Petr
@LukasPetr eche un vistazo a cancelar y afterDelay. Esos permiten que la llamada al método se cancele para cada sección, excepto la última, que es cuando la tabla ha terminado de cargarse por completo.
malhal
@malhal oh, está bien. Es un poco más inteligente de lo que pensé a primera vista.
Lukas Petr
8

Para la versión de respuesta elegida en 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
        }
    }
}

Necesitaba la variable isLoadingTableView porque quería asegurarme de que la tabla se haya cargado antes de hacer una selección de celda predeterminada. Si no incluye esto, cada vez que desplace la tabla invocará su código nuevamente.

Travis M.
fuente
6

El mejor enfoque que conozco es la respuesta de Eric en: ¿ Recibir una notificación cuando UITableView haya terminado de solicitar datos?

Actualización: para que funcione tengo que poner estas llamadas -tableView:cellForRowAtIndexPath:

[tableView beginUpdates];
[tableView endUpdates];
René Retz
fuente
Por lo que vale, he estado usando este método muy simple en todos mis proyectos durante meses y funciona perfectamente.
Eric MORAND
3

Para saber cuándo una vista de tabla termina de cargar su contenido, primero debemos tener una comprensión básica de cómo se muestran las vistas en la pantalla.

En el ciclo de vida de una aplicación, hay 4 momentos clave:

  1. La aplicación recibe un evento (toque, temporizador, bloque enviado, etc.)
  2. La aplicación maneja el evento (modifica una restricción, inicia una animación, cambia el fondo, etc.)
  3. La aplicación calcula la nueva jerarquía de vistas.
  4. La aplicación representa la jerarquía de vistas y la muestra

Las 2 y 3 veces están totalmente separadas. Por qué ? Por razones de rendimiento, no queremos realizar todos los cálculos del momento 3 cada vez que se realiza una modificación.

Entonces, creo que te enfrentas a un caso como este:

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

¿Qué pasa aquí?

Como cualquier vista, una vista de tabla recarga su contenido de forma perezosa. En realidad, si llama reloadDatavarias veces, no creará problemas de rendimiento. La vista de tabla solo vuelve a calcular su tamaño de contenido en función de su implementación delegada y espera el momento 3 para cargar sus celdas. Esta vez se llama pase de diseño.

Bien, ¿cómo entrar en el pase de diseño?

Durante el pase de diseño, la aplicación calcula todos los fotogramas de la jerarquía de vistas. Para involucrarse, puede anular los métodos dedicados layoutSubviews, updateLayoutConstraintsetc. en UIVieway los métodos equivalentes en una subclase de controlador de vista.

Eso es exactamente lo que hace una vista de tabla. Anula layoutSubviewsy, según la implementación de su delegado, agrega o elimina celdas. Llama cellForRowjusto antes de agregar y diseñar una nueva celda, willDisplayjusto después. Si llamó reloadDatao agregó la vista de tabla a la jerarquía, la vista de tablas agrega tantas celdas como sea necesario para llenar su marco en este momento clave.

Bien, pero ahora, ¿cómo saber cuándo una vista de tablas ha terminado de recargar su contenido?

Ahora podemos reformular esta pregunta: ¿cómo saber cuándo una vista de tabla ha terminado de presentar sus subvistas?

La forma más fácil es acceder al diseño de la vista de tabla:

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

Tenga en cuenta que este método se llama muchas veces en el ciclo de vida de la vista de tabla. Debido al desplazamiento y al comportamiento de la vista de tabla, las celdas se modifican, eliminan y agregan con frecuencia. Pero funciona, justo después de que las super.layoutSubviews()células se cargan. Esta solución es equivalente a esperar el willDisplayevento de la última ruta de índice. Este evento se llama durante la ejecución layoutSubviewsde la vista de tabla para cada celda agregada.

Otra forma es recibir una llamada cuando la aplicación finalice un pase de diseño.

Como se describe en la documentación , puede usar una opción de UIView.animate(withDuration:completion):

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

Esta solución funciona pero la pantalla se actualizará una vez entre el momento en que se realiza el diseño y el momento en que se llama al bloque. Esto es equivalente a la DispatchMain.asyncsolución pero especificado.

Alternativamente, preferiría forzar el diseño de la vista de tabla

Hay un método dedicado para forzar a cualquier vista a calcular inmediatamente sus marcos de subvista layoutIfNeeded:

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

Sin embargo, tenga cuidado, al hacerlo se eliminará la carga diferida utilizada por el sistema. Llamar a esos métodos repetidamente podría crear problemas de rendimiento. Asegúrese de que no se les llame antes de que se calcule el marco de la vista de la tabla, de lo contrario, la vista de la tabla se cargará nuevamente y no se le notificará.


Creo que no hay una solución perfecta. Subclasificar las clases podría conducir a problemas. Un pase de diseño comienza desde la parte superior y va hacia la parte inferior, por lo que no es fácil recibir una notificación cuando se realiza todo el diseño. Y layoutIfNeeded()podría crear problemas de rendimiento, etc. Pero conociendo esas opciones, debería poder pensar en una alternativa que se ajuste a sus necesidades.

GaétanZ
fuente
1

Así es como lo haces en 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
fuente
1

así es como lo hago en 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 عبدالمؤمن
fuente
1

Esto es lo que haría.

  1. En su clase base (puede ser rootVC BaseVc, etc.),

    A. Escriba un protocolo para enviar la devolución de llamada "DidFinishReloading".

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

    B. Escriba un método genérico para recargar la vista de tabla.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
  2. En la implementación del método de la clase base, llame a reloadData seguido de delegateMethod con retraso.

    -(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 con el protocolo de finalización de recarga en todos los controladores de vista donde necesita la devolución de llamada.

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

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

Suhas Aithal
fuente
0

La respuesta de @folex es correcta.

Pero fallará si tableView tiene más de una sección mostrada a la vez.

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

 }
}
umakanta
fuente
0

En Swift puedes hacer algo como esto. La siguiente condición será verdadera cada vez que llegue al final de la tabla

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

Si tiene varias secciones, aquí le mostramos cómo obtener la última fila en la última sección (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
fuente
0

Accidentalmente me topé con esta solución:

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

Debe configurar footerView antes de obtener contentSize, por ejemplo, en viewDidLoad. Por cierto. configurar footeView le permite eliminar separadores "no utilizados"

Kowboj
fuente
-1

¿Está buscando el número total de elementos que se mostrarán en la tabla o el total de elementos actualmente visibles? De cualquier manera ... Creo que el método 'viewDidLoad' se ejecuta después de que se invocan todos los métodos de origen de datos. Sin embargo, esto solo funcionará en la primera carga de datos (si está utilizando un único ViewController de asignación).

DerekH
fuente
-1

Estoy copiando el código de Andrew y expandiéndolo para tener en cuenta el caso en el que solo tiene 1 fila en la tabla. ¡Me está funcionando hasta ahora!

- (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: Solo estoy usando 1 sección del código de Andrew, así que tenlo en cuenta.

jpage4500
fuente
Bienvenido a SO @ jpage4500. He editado tu respuesta para eliminar las cosas sobre los puntos de baja repetición y el bache de preguntas. Ampliar la respuesta de otro usuario es una respuesta perfectamente válida por sí misma, por lo que puede reducir el desorden dejando esas cosas fuera. Sin embargo, es bueno que le hayas dado crédito a Andrew, así que eso se quedó.
skrrgwasme
-3

En iOS7.0x la solución es un poco diferente. Esto es lo que se me ocurrió.

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

C objetivo

[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
fuente
1
Este método solo está disponible UICollectionViewhasta donde yo entiendo.
Impresionante-o
Sí, tiene usted razón. BeginUpdates de UITableView es igual a performBatchUpdates de UICollectionView: finalización. stackoverflow.com/a/25128583/988169
pkc456
Por lo tanto, dé una respuesta adecuada y válida a la pregunta.
G.Abhisek