web-dev-qa-db-fra.com

Comment détecter la fin du chargement de UITableView

Je souhaite modifier le décalage de la table lorsque le chargement est terminé. Ce décalage dépend du nombre de cellules chargées dans la table. 

Est-il possible, dans le SDK, de savoir quand un chargement d'uitableview est terminé? Je ne vois rien ni sur les protocoles de délégués ni sur les sources de données.

Je ne peux pas utiliser le nombre de sources de données car le chargement des cellules visibles uniquement.

123
emenegro

Améliorer à @RichX réponse: lastRow peut être à la fois [tableView numberOfRowsInSection: 0] - 1 ou ((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];
    }
}

UPDATE: .__ 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 depuis la source, ce ne sera pas le cas, mais ce n'est pas la question de départ. Ce code sert à détecter quand toutes les cellules censées être visibles sont affichées. willDisplayCell: utilisé ici pour une interface utilisateur plus fluide (une cellule unique affiche généralement rapidement après l'appel willDisplay:). Vous pouvez aussi l'essayer avec tableView:didEndDisplayingCell:.

208
folex

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];
    }
}
26
RichX

Version Swift 3 & 4:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}
16
Daniele Ceglia

Swift 2 solution:

// 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()
    }
}
10
fatihyildizhan

Voici une autre option qui semble fonctionner pour moi. Dans la méthode de délégation viewForFooter, vérifiez s'il s'agit de la dernière section et ajoutez votre code à cet endroit. Cette approche m'est venue à l'esprit après avoir réalisé que willDisplayCell ne prend pas en compte les pieds de page si vous les 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 toute la variable UITableView, et pas simplement pour 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.

9
Kyle Clegg

Essayez cette magie:

- (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.objects.count;
}

// called after the rows in the last section is loaded
-(void)tableViewDidLoadRows:(UITableView*)tableView{
    // make the cell selected after all rows loaded
    if(self.selectedObject){
        NSInteger index = [self.objects indexOfObject:self.selectedObject];
        [tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0] animated:NO scrollPosition:UITableViewScrollPositionMiddle];
    }
}

Le comportement du chargement de la table signifie que vous ne pouvez pas appeler une ligne sélectionnée tant que la table ne sait pas le nombre de lignes et que je voulais une ligne sélectionnée par défaut. J'avais un délégué de vue de table qui n'était pas un contrôleur de vue; je ne pouvais donc pas simplement placer la méthode de sélection de cellule de tableau dans la vue ou charger les méthodes de délégué; aucune autre réponse ne me convenait.

9
malhal

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 était terminé avant de sélectionner des cellules par défaut. Si vous n'incluez pas cela, chaque fois que vous faites défiler le tableau, votre code est à nouveau appelé.

8
Travis M.

La meilleure approche que je connaisse est la réponse d'Eric sur: Recevoir une notification lorsque UITableView a fini de demander des données?

Mise à jour: Pour que cela fonctionne, je dois mettre ces appels dans -tableView:cellForRowAtIndexPath:

[tableView beginUpdates];
[tableView endUpdates];
6
René Retz

Voici comment vous le faites 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()
    }
}

Voici ce que je ferais.

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

    A. Ecrivez un protocole pour envoyer le rappel "DidFinishReloading".

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

    B. Écrivez une méthode générique pour recharger la vue tableau.

    -(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 délai.

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

1
Suhas Aithal

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
Satnam Sync

Très 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. régler le footeView vous permet de supprimer les séparateurs "inutilisés"

0
Kowboj

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
Daniel McLean

Je sais que c'est répondu, je ne fais qu'ajouter une recommandation.

Selon la documentation suivante

https://www.objc.io/issues/2-concurrency/thread-safe-class-design/

Résoudre les problèmes de synchronisation avec dispatch_async est une mauvaise idée. Je suggère que nous devrions gérer cela en ajoutant FLAG ou quelque chose.

0
arango_86

@folex a raison.

Mais il échouera si la table affiche plusieurs sections affichées à la fois.

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

 }
}
0
umakanta

Pour savoir quand une vue sous forme de tableau finit de charger 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 (toucher, minuterie, blocage d'un bloc, etc.)
  2. L'application gère l'événement (elle modifie une contrainte, lance une animation, modifie l'arrière-plan, etc.)
  3. L'application calcule la nouvelle hiérarchie de vues
  4. L'application restitue la hiérarchie des vues et l'affiche

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

Donc, je pense que vous faites face à un cas comme celui-ci:

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

Qu'est-ce qui ne va pas ici?

Comme toute vue, une vue table recharge son contenu paresseusement. En fait, si vous appelez reloadData plusieurs fois, vous ne rencontrerez aucun problème de performances. La vue tabulaire recalcule uniquement la taille de son contenu en fonction de son implémentation déléguée et attend le moment 3 pour charger ses cellules. Ce temps est appelé passe de mise en page.

OK, comment entrer dans la carte de présentation?

Au cours de la passe de mise en page, l'application calcule toutes les images de la hiérarchie des vues. Pour participer, vous pouvez remplacer les méthodes dédiées layoutSubviews, updateLayoutConstraints etc. dans une variable UIView et les méthodes équivalentes dans une sous-classe du contrôleur de vue. 

C’est exactement ce que fait une vue tableau. Il remplace layoutSubviews et, en fonction de votre implémentation de délégué, ajoute ou supprime des cellules. Il appelle cellForRow juste avant d'ajouter et d'agencer une nouvelle cellule, willDisplay juste après. Si vous avez appelé reloadData ou si vous avez simplement ajouté la vue tabulaire à la hiérarchie, cette dernière ajoute autant de cellules que nécessaire pour remplir son cadre à ce moment clé.

Très bien, mais maintenant, comment savoir quand une vue de tables a fini de recharger son contenu?

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

Le moyen le plus simple est d'entrer dans la présentation 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 comportement de défilement et de suppression de file d'attente de la vue tabulaire, les cellules sont souvent modifiées, supprimées et ajoutées. Mais cela fonctionne, juste après la super.layoutSubviews(), les cellules sont chargées. Cette solution équivaut à attendre l'événement willDisplay du dernier chemin d'index. Cet événement est appelé lors de l'exécution de layoutSubviews de la vue tabulaire pour chaque cellule ajoutée.

Une autre méthode consiste à se faire appeler lorsque l'application termine une passe de présentation. 

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

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

Cette solution fonctionne mais l'écran sera actualisé 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 solution DispatchMain.async mais spécifié.

Alternativement, je préférerais forcer la présentation de la vue tableau

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

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

Attention, cela enlèvera le chargement paresseux 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 tableau ne soit calculé, sinon la vue tableau sera chargée à nouveau et vous ne serez pas averti.


Je pense qu'il n'y a pas de solution parfaite. Les classes de sous-classes pourraient conduire à des trubles. Une passe de mise en page commence du haut vers le bas, il est donc difficile 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 solution qui répondra à vos besoins.

0
GaétanZ