211 votes

Comment savoir quand UITableView a terminé le ReloadData ?

J'essaie de faire défiler le bas d'un UITableView après qu'il ait terminé de l'exécuter. [self.tableView reloadData]

A l'origine, j'avais

 [self.tableView reloadData]
 NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];

[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];

Mais ensuite, j'ai lu que le reloadData est asynchrone, donc le défilement ne se produit pas puisque le self.tableView , [self.tableView numberOfSections] y [self.tableView numberOfRowsinSection sont toutes égales à 0.

Merci !

Ce qui est bizarre c'est que j'utilise :

[self.tableView reloadData];
NSLog(@"Number of Sections %d", [self.tableView numberOfSections]);
NSLog(@"Number of Rows %d", [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1);

Dans la console, il retourne Sections = 1, Row = -1 ;

Quand je fais exactement les mêmes NSLogs en cellForRowAtIndexPath J'obtiens Sections = 1 et Row = 8 ; (8 est juste)

307voto

rob mayoff Points 124153

Le rechargement a lieu lors du passage suivant de la mise en page, ce qui se produit normalement lorsque vous rendez le contrôle à la boucle d'exécution (après, disons, le retour de l'action du bouton ou autre).

Ainsi, une façon d'exécuter quelque chose après le rechargement de la vue tableau est simplement de forcer la vue tableau à effectuer la mise en page immédiatement :

[self.tableView reloadData];
[self.tableView layoutIfNeeded];
 NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection: ([self.tableView numberOfSections]-1)];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];

Une autre solution consiste à planifier l'exécution ultérieure de votre code après la mise en page en utilisant dispatch_async :

[self.tableView reloadData];

dispatch_async(dispatch_get_main_queue(), ^{
     NSIndexPath* indexPath = [NSIndexPath indexPathForRow: ([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1) inSection:([self.tableView numberOfSections]-1)];

    [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
});

UPDATE

Après une enquête plus approfondie, je découvre que la vue de la table envoie tableView:numberOfSections: y tableView:numberOfRowsInSection: à sa source de données avant de revenir de reloadData . Si le délégué implémente tableView:heightForRowAtIndexPath: la vue tableau l'envoie également (pour chaque ligne) avant de revenir de l'écran d'accueil. reloadData .

Cependant, la vue de la table n'envoie pas tableView:cellForRowAtIndexPath: o tableView:headerViewForSection jusqu'à la phase de mise en page, qui se produit par défaut lorsque vous rendez le contrôle à la boucle d'exécution.

J'ai également constaté que dans un petit programme de test, le code de votre question fait défiler correctement le bas de la vue tableau, sans Je ne fais rien de spécial (comme envoyer layoutIfNeeded ou en utilisant dispatch_async ).

113voto

Aviel Gross Points 1110

Swift :

extension UITableView {
    func reloadData(completion:@escaping ()->()) {
        UIView.animate(withDuration: 0, animations: reloadData)
            { _ in completion() }
    } 
}

// ...somewhere later...

tableView.reloadData {
    print("done")
}

Objective-C :

[UIView animateWithDuration:0 animations:^{
    [myTableView reloadData];
} completion:^(BOOL finished) {
    //Do something after that...
}];

39voto

Tyler Sheaffer Points 952

El dispatch_async(dispatch_get_main_queue()) La méthode ci-dessus est pas de garantie de fonctionnement . Je constate un comportement non déterministe, dans lequel le système a parfois terminé les sous-vues de mise en page et le rendu des cellules avant le bloc d'achèvement, et parfois après.

Voici une solution qui fonctionne à 100% pour moi, sur iOS 10. Elle nécessite la possibilité d'instancier le UITableView ou le UICollectionView comme une sous-classe personnalisée. Voici la solution pour UICollectionView, mais c'est exactement la même chose pour UITableView :

CustomCollectionView.h :

#import <UIKit/UIKit.h>

@interface CustomCollectionView: UICollectionView

- (void)reloadDataWithCompletion:(void (^)(void))completionBlock;

@end

CustomCollectionView.m :

#import "CustomCollectionView.h"

@interface CustomCollectionView ()

@property (nonatomic, copy) void (^reloadDataCompletionBlock)(void);

@end

@implementation CustomCollectionView

- (void)reloadDataWithCompletion:(void (^)(void))completionBlock
{
    self.reloadDataCompletionBlock = completionBlock;
    [self reloadData];
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if (self.reloadDataCompletionBlock) {
        self.reloadDataCompletionBlock();
        self.reloadDataCompletionBlock = nil;
    }
}

@end

Exemple d'utilisation :

[self.collectionView reloadDataWithCompletion:^{
    // reloadData is guaranteed to have completed
}];

Voir aquí pour une version Swift de cette réponse

32voto

Shawn Aukstak Points 756

J'ai eu les mêmes problèmes que Tyler Sheaffer.

J'ai mis en place sa solution en Swift et cela a résolu mes problèmes.

Swift 3.0 :

final class UITableViewWithReloadCompletion: UITableView {
  private var reloadDataCompletionBlock: (() -> Void)?

  override func layoutSubviews() {
    super.layoutSubviews()

    reloadDataCompletionBlock?()
    reloadDataCompletionBlock = nil
  }

  func reloadDataWithCompletion(completion: @escaping () -> Void) {
    reloadDataCompletionBlock = completion
    self.reloadData()
  }
}

Swift 2 :

class UITableViewWithReloadCompletion: UITableView {

  var reloadDataCompletionBlock: (() -> Void)?

  override func layoutSubviews() {
    super.layoutSubviews()

    self.reloadDataCompletionBlock?()
    self.reloadDataCompletionBlock = nil
  }

  func reloadDataWithCompletion(completion:() -> Void) {
      reloadDataCompletionBlock = completion
      self.reloadData()
  }
}

Exemple d'utilisation :

tableView.reloadDataWithCompletion() {
 // reloadData is guaranteed to have completed
}

12voto

Womble Points 2299

Et un UICollectionView version, basée sur la réponse de kolaworld :

https://stackoverflow.com/a/43162226/1452758

Doit être testé. Fonctionne jusqu'à présent sur iOS 9.2, Xcode 9.2 beta 2, avec le défilement d'une collectionView vers un index, comme une fermeture.

extension UICollectionView
{
    /// Calls reloadsData() on self, and ensures that the given closure is
    /// called after reloadData() has been completed.
    ///
    /// Discussion: reloadData() appears to be asynchronous. i.e. the
    /// reloading actually happens during the next layout pass. So, doing
    /// things like scrolling the collectionView immediately after a
    /// call to reloadData() can cause trouble.
    ///
    /// This method uses CATransaction to schedule the closure.

    func reloadDataThenPerform(_ closure: @escaping (() -> Void))
    {       
        CATransaction.begin()
            CATransaction.setCompletionBlock(closure)
            self.reloadData()
        CATransaction.commit()
    }
}

Utilisation :

myCollectionView.reloadDataThenPerform {
    myCollectionView.scrollToItem(at: indexPath,
            at: .centeredVertically,
            animated: true)
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X