Come rilevare la fine del caricamento di UITableView


141

Voglio cambiare l'offset della tabella al termine del caricamento e tale offset dipende dal numero di celle caricate sulla tabella.

È comunque sull'SDK sapere quando è terminato il caricamento di uitableview? Non vedo nulla né sui delegati né sui protocolli di origine dati.

Non riesco a utilizzare il conteggio delle origini dati a causa del caricamento delle sole celle visibili.


provare una combinazione di conteggio dell'origine dati e "indexPathsForVisibleRows"
Swapnil Luktuke

1
Riaperto sulla base di ulteriori informazioni in flag: "Non è un duplicato. Chiede informazioni sul caricamento di celle visibili, non sul completamento della richiesta di dati. Vedi aggiornamento alla risposta accettata"
Kev

Questa soluzione funziona bene per me. Si controlla che stackoverflow.com/questions/1483581/...~~V~~singular~~3rd
Khaled Annajar

Risposte:


224

Migliora la risposta @RichX: lastRowpuò essere entrambi [tableView numberOfRowsInSection: 0] - 1o ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. Quindi il codice sarà:

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

AGGIORNAMENTO: Bene, il commento di @ htafoya è giusto. Se si desidera che questo codice rilevi la fine del caricamento di tutti i dati dall'origine, non lo farebbe, ma non è la domanda originale. Questo codice serve per rilevare quando vengono visualizzate tutte le celle che devono essere visibili. willDisplayCell:utilizzato qui per un'interfaccia utente più fluida (di solito una singola cella viene visualizzata rapidamente dopo la willDisplay:chiamata). Puoi anche provarlo con tableView:didEndDisplayingCell:.


Molto meglio per sapere quando tutte le celle si caricano che sono visibili.
Eric,

14
Tuttavia, questo verrà chiamato ogni volta che l'utente scorre per visualizzare più celle.
htafoya,

Il problema è che non tiene conto di una vista a piè di pagina. Potrebbe non essere un problema per la maggior parte, ma se lo è puoi applicare la stessa idea ma nel viewForFooterInSectionmetodo.
Kyle Clegg,

1
@KyleClegg patric.schenke ha menzionato uno dei motivi in ​​commento alla tua risposta. Inoltre, cosa succede se il piè di pagina non è visibile alla fine del caricamento della cella?
folex,

27
tableView: didEndDisplayingCell: in realtà viene chiamato quando si rimuove una cella dalla vista, non quando il suo rendering nella vista è completo in modo che non funzioni. Non è un buon nome di metodo. Devo leggere i documenti.
Andrew Raphael,

34

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

1
questo metodo viene chiamato quando cariciamo i dati per la prima volta. Nel mio caso ci sono solo 4 celle visibili. Ho caricato 8 righe ma questa funzione veniva ancora chiamata. Quello che voglio è caricare più dati quando l'utente scorre fino all'ultima riga
Muhammad Nayab

Ciao Muhammad Nayab, forse potresti sostituire "if indexPath == lastVisibleIndexPath" con "if indexPath.row == yourDataSource.count"
Daniele Ceglia il

1
@DanieleCeglia Grazie per averlo segnalato. se indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 funzionerà per me. Saluti!!!
Yogesh Patel,

28

Uso sempre questa soluzione molto semplice:

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

come rilevare l'ultima riga? questo è il problema per me ... puoi spiegare come ottenere quel lastrow in quella condizione se.
Sameera Chathuranga,

6
L'ultima riga è la [tableView numberOfRowsInSection: 0] - 1. È necessario sostituire il 0valore necessario. Ma non è questo il problema. Il problema è che UITableView carica solo visibile. Tuttavia ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowrisolve il problema.
folex,

1
Se stiamo cercando il completamento assoluto non sarebbe meglio usare - (void) tableView: (UITableView *) tableView didEndDisplayingCell: (UITableViewCell *) cell forRowAtIndexPath: (NSIndexPath *) indexPath?
Morcutt,

10

Ecco un'altra opzione che sembra funzionare per me. Nel metodo delegato viewForFooter controlla se è la sezione finale e aggiungi il tuo codice lì. Questo approccio mi è venuto in mente dopo aver realizzato che willDisplayCell non tiene conto dei piè di pagina se li hai.

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

Trovo che questo approccio funzioni meglio se stai cercando di trovare il caricamento finale per l'intero UITableView, e non semplicemente le celle visibili. A seconda delle tue esigenze potresti voler solo le celle visibili, nel qual caso la risposta di folex è una buona strada.


Tornando nilda tableView:viewForFooterInSection:pasticciato con il mio layout in iOS 7 usando Auto Layout.
ma11hew28,

Interessante. Cosa succede se lo si imposta su una cornice con altezza e larghezza 0? return CGRectMake(0,0,0,0);
Kyle Clegg,

L'ho provato e ho anche impostato self.tableView.sectionFooterHeight = 0. Ad ogni modo, sembra inserire una vista piè di pagina della sezione con un'altezza di circa 10. Scommetto che potrei risolvere questo problema aggiungendo un vincolo di altezza 0 alla vista che ritorno. Comunque, sto bene perché in realtà volevo capire come avviare UITableView sull'ultima cella, ma ho visto prima questo.
ma11hew28,

1
@MattDiPasquale se si implementa viewForFooterInSection, è necessario implementare heightForFooterInSection. Deve restituire 0 per sezioni con un piè di pagina nullo. Questo è anche nei documenti ufficiali ormai.
patric.schenke,

viewForFooterInSection non viene chiamato se si imposta heightForFooterInSection su 0
Oded Harth

10

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

9

Utilizzo dell'API privata:

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

Utilizzo dell'API pubblica:

- (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 possibile progetto migliore è quello di aggiungere le celle visibili a un set, quindi quando è necessario verificare se la tabella è caricata è possibile invece fare un ciclo for attorno a questo set, ad es.

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

Soluzione molto migliore rispetto alle altre. Non richiede un ricaricamento tableView. È molto semplice e viewDidLoadRows: non viene chiamato ogni volta che viene caricata l'ultima cella.
Matt Hudson,

questa è la soluzione migliore finora, dal momento che le altre soluzioni non tengono conto delle sezioni multiple
Justicepenny,

Mi dispiace per il commento subdolo, ma com'è esattamente questa 'magia'? Chiamare un altro metodo da delegato. Non capisco
Lukas Petr,

@LukasPetr dai un'occhiata all'annullamento e all'afterDelay. Questi consentono di annullare la chiamata del metodo per ogni sezione tranne l'ultima, ovvero quando la tabella ha completato il caricamento.
Malhal,

@malhal oh, ok. È un po 'più intelligente di quanto pensassi a prima vista.
Lukas Petr,

8

Per la versione di risposta scelta in 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
        }
    }
}

Avevo bisogno della variabile isLoadingTableView perché volevo assicurarmi che il caricamento della tabella fosse completato prima di effettuare una selezione di celle predefinita. Se non lo includi, ogni volta che scorri la tabella invocherà di nuovo il tuo codice.



3

Per sapere quando una vista tabella termina il caricamento del suo contenuto, dobbiamo prima avere una conoscenza di base di come le viste vengono visualizzate sullo schermo.

Nel ciclo di vita di un'app, ci sono 4 momenti chiave:

  1. L'app riceve un evento (touch, timer, blocco spedito ecc.)
  2. L'app gestisce l'evento (modifica un vincolo, avvia un'animazione, cambia sfondo ecc.)
  3. L'app calcola la nuova gerarchia di viste
  4. L'app esegue il rendering della gerarchia di visualizzazione e la visualizza

I tempi 2 e 3 sono totalmente separati. Perché ? Per motivi di prestazioni, non vogliamo eseguire tutti i calcoli del momento 3 ogni volta che viene apportata una modifica.

Quindi, penso che stai affrontando un caso come questo:

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

Cosa c'è che non va qui?

Come ogni vista, una vista tabella ricarica pigramente il suo contenuto. In realtà, se chiami reloadDatapiù volte non creerà problemi di prestazioni. La vista tabella ricalcola solo le dimensioni del contenuto in base all'implementazione del delegato e attende il momento 3 per caricare le celle. Questa volta si chiama passaggio layout.

Ok, come accedere al layout layout?

Durante il passaggio del layout, l'app calcola tutti i frame della gerarchia della vista. Per partecipare, è possibile ignorare i metodi dedicati layoutSubviews, updateLayoutConstraintsecc. In UIViewae i metodi equivalenti in una sottoclasse del controller di visualizzazione.

Questo è esattamente ciò che fa una vista tabella. Sostituisce layoutSubviewse in base all'implementazione del delegato aggiunge o rimuove le celle. Chiama cellForRowsubito prima di aggiungere e creare una nuova cella, willDisplaysubito dopo. Se hai chiamato reloadDatao hai appena aggiunto la vista tabella alla gerarchia, la vista tabelle aggiunge tutte le celle necessarie per riempire il suo frame in questo momento chiave.

Bene, ma ora, come sapere quando una vista delle tabelle ha finito di ricaricare il suo contenuto?

Ora possiamo riformulare questa domanda: come sapere quando una vista tabella ha finito di disporre le sue sottoview?

Il modo più semplice è entrare nel layout della vista tabella:

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

Si noti che questo metodo viene chiamato più volte nel ciclo di vita della vista tabella. A causa del comportamento di scorrimento e dequeue della vista tabella, le celle vengono modificate, rimosse e aggiunte spesso. Ma funziona, subito dopo il super.layoutSubviews()caricamento delle celle. Questa soluzione equivale ad attendere l' willDisplayevento dell'ultimo percorso dell'indice. Questo evento viene chiamato durante l'esecuzione dilayoutSubviews vista tabella per ogni cella aggiunta.

Un altro modo è quello di essere chiamato quando l'app termina un passaggio di layout.

Come descritto nella documentazione , è possibile utilizzare un'opzione di UIView.animate(withDuration:completion):

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

Questa soluzione funziona ma lo schermo si aggiornerà una volta tra il momento in cui il layout viene eseguito e il momento in cui viene chiamato il blocco. Questo è equivalente alla DispatchMain.asyncsoluzione ma specificato.

In alternativa, preferirei forzare il layout della vista tabella

Esiste un metodo dedicato per forzare qualsiasi vista a calcolare immediatamente i suoi frame di subview layoutIfNeeded:

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

Fare attenzione, tuttavia, rimuovendo il caricamento lento utilizzato dal sistema. La chiamata ripetuta di questi metodi potrebbe creare problemi di prestazioni. Assicurati che non vengano chiamati prima di calcolare il frame della vista tabella, altrimenti la vista tabella verrà caricata di nuovo e non riceverai alcuna notifica.


Penso che non ci sia una soluzione perfetta. Le classi di sottoclasse potrebbero portare a problemi. Un passaggio di layout inizia dall'alto e va verso il basso, quindi non è facile ricevere notifiche quando tutto il layout è terminato. E layoutIfNeeded()potrebbe creare problemi di prestazioni, ecc. Ma conoscendo queste opzioni, dovresti essere in grado di pensare a un'alternativa adatta alle tue esigenze.


1

Ecco come lo fai in 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

ecco come lo faccio in 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()
    }
}

1

Ecco cosa farei.

  1. Nella tua classe di base (può essere rootVC BaseVc ecc.),

    A. Scrivere un protocollo per inviare il callback "DidFinishReloading".

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

    B. Scrivere un metodo generico per ricaricare la vista tabella.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
  2. Nell'implementazione del metodo della classe base, chiamare reloadData seguito da delegateMethod con ritardo.

    -(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. Confermare il protocollo di completamento del caricamento in tutti i controller di visualizzazione in cui è necessario il callback.

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

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


0

La risposta di @folex è corretta.

Ma fallirà se tableView ha più di una sezione visualizzata alla volta.

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

 }
}

0

In Swift puoi fare qualcosa del genere. Le seguenti condizioni saranno vere ogni volta che si raggiunge la fine di tableView

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


0

Se hai più sezioni, ecco come ottenere l'ultima riga nell'ultima sezione (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

        }
    }
}

0

Abbastanza per caso mi sono imbattuto in questa soluzione:

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

È necessario impostare footerView prima di ottenere contentSize, ad esempio in viewDidLoad. Btw. l'impostazione di footeView consente di eliminare i separatori "non utilizzati"


-1

Stai cercando il numero totale di elementi che verranno visualizzati nella tabella o il totale degli elementi attualmente visibili? In entrambi i casi .. Credo che il metodo 'viewDidLoad' venga eseguito dopo che sono stati chiamati tutti i metodi dell'origine dati. Tuttavia, questo funzionerà solo sul primo caricamento dei dati (se si utilizza un unico ViewController allocato).


-1

Sto copiando il codice di Andrew e lo sto espandendo per tenere conto del caso in cui hai solo 1 riga nella tabella. Funziona così lontano per me!

- (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: sto solo usando 1 sezione del codice di Andrew, quindi tienilo a mente ..


Benvenuti in SO @ jpage4500. Ho modificato la tua risposta per rimuovere gli elementi relativi ai punti di reputazione bassi e al bump della domanda. Espandere la risposta di un altro utente è una risposta perfettamente valida da sola, quindi puoi ridurre il disordine lasciando quella roba fuori. È bello che tu abbia dato credito ad Andrew, quindi è rimasto.
skrrgwasme,

-3

In iOS7.0x la soluzione è leggermente diversa. Ecco cosa mi è venuto in mente.

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

-3

Obiettivo C

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

veloce

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

                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })

1
Questo metodo è disponibile solo UICollectionViewper quanto ho capito.
Fantastico-o

Si hai ragione. BeginUpdates di UITableView è uguale a performBatchUpdates di UICollectionView: completamento. stackoverflow.com/a/25128583/988169
pkc456

Quindi, dai una risposta corretta e valida alla domanda.
G. Abhisek,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.