Comment détecter la fin du chargement de UITableView


141

Je souhaite modifier le décalage de la table lorsque le chargement est terminé et que ce décalage dépend du nombre de cellules chargées sur la table.

Est-ce de toute façon sur le SDK de savoir quand un chargement uitableview est terminé? Je ne vois rien ni sur les délégués ni sur les protocoles de source de données.

Je ne peux pas utiliser le décompte des sources de données en raison du chargement des cellules visibles uniquement.


essayez une combinaison du nombre de sources de données et de 'indexPathsForVisibleRows'
Swapnil Luktuke

1
Rouvert sur la base d'informations supplémentaires dans le drapeau: "Ce n'est pas en double. Il pose des questions sur le chargement des cellules visibles, pas sur la fin de la demande de données. Voir la mise à jour de la réponse acceptée"
Kev

Cette solution fonctionne très bien pour moi. Vous le vérifiez stackoverflow.com/questions/1483581/…
Khaled Annajar

Réponses:


224

Améliorer la réponse @RichX: lastRowpeut être les deux [tableView numberOfRowsInSection: 0] - 1ou ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. Le code sera donc:

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

MISE À JOUR: Eh bien, le commentaire de @ htafoya est juste. Si vous voulez que ce code détecte la fin du chargement de toutes les données à partir de la source, ce ne sera pas le cas, mais ce n'est pas la question d'origine. Ce code permet de détecter le moment où toutes les cellules censées être visibles sont affichées. willDisplayCell:utilisé ici pour une interface utilisateur plus fluide (une seule cellule s'affiche généralement rapidement après un willDisplay:appel). Vous pouvez également l'essayer avec tableView:didEndDisplayingCell:.


Beaucoup mieux pour savoir quand toutes les cellules se chargent qui sont visibles.
Eric du

14
Cependant, cela sera appelé chaque fois que l'utilisateur fait défiler pour afficher plus de cellules.
htafoya

Le problème avec ceci est qu'il ne tient pas compte d'une vue de pied de page. Ce n'est peut-être pas un problème pour la plupart, mais si c'est le cas, vous pouvez appliquer la même idée mais dans la viewForFooterInSectionméthode.
Kyle Clegg du

1
@KyleClegg patric.schenke a mentionné l'une des raisons en commentaire de votre réponse. Et si le pied de page n'est pas visible à la fin du chargement de la cellule?
folex

27
tableView: didEndDisplayingCell: est en fait appelé lors de la suppression d'une cellule de la vue, pas lorsque son rendu dans la vue est terminé, donc cela ne fonctionnera pas. Pas un bon nom de méthode. Je dois lire les documents.
Andrew Raphael

34

Version 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
cette méthode est appelée lorsque nous chargeons des données pour la première fois. Dans mon cas, il n'y a que 4 cellules visibles. J'ai chargé 8 lignes mais cette fonction était toujours appelée. Ce que je veux, c'est charger plus de données lorsque l'utilisateur fait défiler jusqu'à la dernière ligne
Muhammad Nayab

Salut Muhammad Nayab, vous pourriez peut-être remplacer "if indexPath == lastVisibleIndexPath" par "if indexPath.row == yourDataSource.count"
Daniele Ceglia

1
@DanieleCeglia Merci de l'avoir signalé. si indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 cela va fonctionner pour moi. À votre santé!!!
Yogesh Patel le

28

J'utilise toujours cette solution très simple:

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

comment détecter la dernière ligne? thats le problème pour moi .. pouvez-vous expliquer comment vous obtenez ce lastrow dans cette condition si.
Sameera Chathuranga

6
La dernière ligne est le [tableView numberOfRowsInSection: 0] - 1. Vous devez remplacer 0par la valeur requise. Mais ce n'est pas le problème. Le problème est que UITableView charge uniquement visible. Cependant ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowrésout le problème.
folex du

1
Si nous recherchons une complétion absolue, ne serait-il pas préférable d'utiliser - (void) tableView: (UITableView *) tableView didEndDisplayingCell: (UITableViewCell *) cell forRowAtIndexPath: (NSIndexPath *) indexPath?
morcutt

10

Voici une autre option qui semble fonctionner pour moi. Dans la méthode déléguée viewForFooter, vérifiez s'il s'agit de la dernière section et ajoutez votre code à cet endroit. Cette approche est venue à l'esprit après avoir réalisé que willDisplayCell ne tient pas compte des pieds de page si vous en avez.

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

Je trouve que cette approche fonctionne mieux si vous cherchez à trouver le chargement final pour l'ensemble UITableView, et pas simplement les cellules visibles. En fonction de vos besoins, vous voudrez peut-être uniquement les cellules visibles, auquel cas la réponse de folex est un bon itinéraire.


Revenir nilde tableView:viewForFooterInSection:ma mise en page dans iOS 7 en utilisant la mise en page automatique.
ma11hew28

Intéressant. Et si vous le définissez sur un cadre avec une hauteur et une largeur de 0? return CGRectMake(0,0,0,0);
Kyle Clegg

J'ai essayé et même réglé self.tableView.sectionFooterHeight = 0. Quoi qu'il en soit, il semble insérer une vue de pied de page d'une hauteur d'environ 10. Je parie que je pourrais résoudre ce problème en ajoutant une contrainte de hauteur 0 à la vue que je retourne. Quoi qu'il en soit, je suis bon parce que je voulais en fait savoir comment démarrer UITableView sur la dernière cellule, mais j'ai vu cela en premier.
ma11hew28

1
@MattDiPasquale si vous implémentez viewForFooterInSection, vous devez également implémenter heightForFooterInSection. Il doit retourner 0 pour les sections avec un pied de page nul. C'est également dans les documents officiels maintenant.
patric.schenke

viewForFooterInSection n'est pas appelé si vous définissez le paramètre heightForFooterInSection sur 0
Oded Harth

10

Solution 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

Utilisation de l'API privée:

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

Utilisation de l'API publique:

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

Une meilleure conception possible est d'ajouter les cellules visibles à un ensemble, puis lorsque vous avez besoin de vérifier si la table est chargée, vous pouvez à la place faire une boucle for autour de cet ensemble, par exemple

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

Bien meilleure solution que les autres. Il ne nécessite pas de rechargement de tableView. C'est très simple et le viewDidLoadRows: n'est pas appelé à chaque fois que la dernière cellule est chargée.
Matt Hudson

c'est la meilleure solution à ce jour, car les autres solutions ne prennent pas en compte les multiples sections
justicepenny

Je suis désolé pour le commentaire sarcastique, mais comment est-ce exactement «magique»? Appel à une autre méthode du délégué. Je ne comprends pas.
Lukas Petr

@LukasPetr jetez un œil à l'annulation et à l'afterDelay. Celles-ci permettent d'annuler l'appel de méthode pour chaque section sauf la dernière, c'est-à-dire lorsque le chargement de la table est complètement terminé.
malhal

@malhal oh, d'accord. C'est un peu plus intelligent que je ne le pensais à première vue.
Lukas Petr

8

Pour la version de réponse choisie dans 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
        }
    }
}

J'avais besoin de la variable isLoadingTableView parce que je voulais m'assurer que le chargement de la table est terminé avant de faire une sélection de cellule par défaut. Si vous ne l'incluez pas, chaque fois que vous faites défiler le tableau, votre code sera de nouveau appelé.



3

Pour savoir quand une vue de tableau termine le chargement de son contenu, nous devons d'abord avoir une compréhension de base de la façon dont les vues sont affichées à l'écran.

Dans le cycle de vie d'une application, il y a 4 moments clés:

  1. L'application reçoit un événement (tactile, minuterie, bloc distribué, etc.)
  2. L'application gère l'événement (elle modifie une contrainte, démarre une animation, change l'arrière-plan, etc.)
  3. L'application calcule la nouvelle hiérarchie de vues
  4. L'application rend la hiérarchie des vues et l'affiche

Les temps 2 et 3 sont totalement séparés. Pourquoi ? Pour des raisons de performances, nous ne souhaitons pas effectuer tous les calculs du moment 3 à chaque fois qu'une modification est effectuée.

Donc, je pense que vous êtes confronté à un cas comme celui-ci:

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

Quel est le problème ici?

Comme toute vue, une vue de table recharge son contenu paresseusement. En fait, si vous appelez reloadDataplusieurs fois, cela ne créera pas de problèmes de performances. La vue tableau ne recalcule que la taille de son contenu en fonction de son implémentation déléguée et attend le moment 3 pour charger ses cellules. Cette fois, on appelle une passe de mise en page.

Ok, comment entrer dans le pass de mise en page?

Pendant la passe de mise en page, l'application calcule tous les cadres de la hiérarchie de vues. Pour vous impliquer, vous pouvez remplacer les méthodes dédiées layoutSubviews, updateLayoutConstraintsetc. dans a UIViewet les méthodes équivalentes dans une sous-classe de contrôleur de vue.

C'est exactement ce que fait une vue de table. Il remplace layoutSubviewset en fonction de votre implémentation de délégué, ajoute ou supprime des cellules. Il appelle cellForRowjuste avant d'ajouter et de disposer une nouvelle cellule, willDisplayjuste après. Si vous avez appelé reloadDataou simplement ajouté la vue tableau à la hiérarchie, la vue tableaux ajoute autant de cellules que nécessaire pour remplir son cadre à ce moment clé.

D'accord, mais maintenant, comment savoir quand une vue de tableaux a fini de recharger son contenu?

Nous pouvons maintenant reformuler cette question: comment savoir quand une vue de tableau a fini de disposer ses sous-vues?

Le moyen le plus simple est d'entrer dans la disposition de la vue tableau:

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

Notez que cette méthode est appelée plusieurs fois dans le cycle de vie de la vue table. En raison du défilement et du comportement de sortie de la file d'attente de la vue tableau, les cellules sont modifiées, supprimées et ajoutées souvent. Mais cela fonctionne, juste après le super.layoutSubviews()chargement des cellules. Cette solution équivaut à attendre l' willDisplayévénement du dernier chemin d'index. Cet événement est appelé lors de l'exécution layoutSubviewsde la vue tableau pour chaque cellule ajoutée.

Un autre moyen consiste à être appelé lorsque l'application termine une passe de mise en page.

Comme décrit dans la documentation , vous pouvez utiliser une option du UIView.animate(withDuration:completion):

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

Cette solution fonctionne mais l'écran se rafraîchira une fois entre le moment où la mise en page est terminée et le moment où le bloc est appelé. Ceci est équivalent à la DispatchMain.asyncsolution mais spécifié.

Sinon, je préférerais forcer la mise en page de la vue tableau

Il existe une méthode dédiée pour forcer toute vue à calculer immédiatement ses cadres de sous-vue layoutIfNeeded:

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

Attention cependant, cela supprimera le chargement différé utilisé par le système. L'appel répété de ces méthodes peut créer des problèmes de performances. Assurez-vous qu'ils ne seront pas appelés avant que le cadre de la vue de table ne soit calculé, sinon la vue de table sera chargée à nouveau et vous ne serez pas averti.


Je pense qu'il n'y a pas de solution parfaite. Le sous-classement des classes peut conduire à des trubles. Une passe de mise en page commence par le haut et va vers le bas, il n'est donc pas facile d'être averti lorsque toute la mise en page est terminée. Et layoutIfNeeded()pourrait créer des problèmes de performances, etc. Mais connaissant ces options, vous devriez être en mesure de penser à une alternative qui répondra à vos besoins.


1

Voici comment procéder dans 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

voici comment je le fais dans 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

Voici ce que je ferais.

  1. Dans votre classe de base (peut être rootVC BaseVc etc.),

    A. Écrivez un protocole pour envoyer le rappel "DidFinishReloading".

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

    B. Ecrivez une méthode générique pour recharger la vue de table.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
  2. Dans l'implémentation de la méthode de classe de base, appelez reloadData suivi de delegateMethod avec delay.

    -(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. Confirmez le protocole d'achèvement de rechargement dans tous les contrôleurs de vue où vous avez besoin du rappel.

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

Référence: https://discussions.apple.com/thread/2598339?start=0&tstart=0


0

La réponse @folex est juste.

Mais cela échouera si la tableView a plus d'une section affichée à la fois.

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

 }
}

0

Dans Swift, vous pouvez faire quelque chose comme ça. La condition suivante sera vraie chaque fois que vous atteindrez la fin de la table

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


0

Si vous avez plusieurs sections, voici comment obtenir la dernière ligne de la dernière section (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

Tout à fait accidentellement, je suis tombé sur cette solution:

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

Vous devez définir le footerView avant d'obtenir le contentSize, par exemple dans viewDidLoad. Btw. la configuration du footeView vous permet de supprimer les séparateurs "inutilisés"


-1

Recherchez-vous le nombre total d'éléments qui seront affichés dans le tableau ou le total des éléments actuellement visibles? De toute façon .. Je crois que la méthode «viewDidLoad» s'exécute après que toutes les méthodes de source de données sont appelées. Cependant, cela ne fonctionnera que sur le premier chargement des données (si vous utilisez un seul alloc ViewController).


-1

Je copie le code d'Andrew et je l'élargis pour tenir compte du cas où vous n'avez qu'une ligne dans le tableau. Cela fonctionne si loin pour moi!

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

REMARQUE: J'utilise juste une section du code d'Andrew, alors gardez cela à l'esprit.


Bienvenue sur SO @ jpage4500. J'ai modifié votre réponse pour supprimer les informations sur les points de répétition faibles et la réponse à la question. Étendre la réponse d'un autre utilisateur est une réponse parfaitement valide en soi, vous pouvez donc réduire l'encombrement en laissant ces éléments de côté. C'est bien que vous ayez accordé du crédit à Andrew, alors cela est resté.
skrrgwasme

-3

Dans iOS7.0x, la solution est un peu différente. Voici ce que j'ai trouvé.

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

Objectif c

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

Rapide

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

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

1
Cette méthode n'est disponible UICollectionViewque pour autant que je sache.
Génial-o

Oui, tu as raison. BeginUpdates de UITableView est égal à performBatchUpdates: completion de UICollectionView. stackoverflow.com/a/25128583/988169
pkc456

Donnez donc une réponse appropriée et valide à la question.
G.Abhisek
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.