Comment savoir quand UITableView a terminé ReloadData?

183

J'essaie de faire défiler vers le bas d'un UITableView après avoir terminé [self.tableView reloadData]

J'avais à l'origine

 [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 reloadData est asynchrone, donc le défilement ne se produit pas depuis le self.tableView, [self.tableView numberOfSections]et [self.tableView numberOfRowsinSectionsont tous à 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 renvoie Sections = 1, Row = -1;

Lorsque je fais exactement les mêmes NSLogs, cellForRowAtIndexPathj'obtiens Sections = 1 et Row = 8; (8 a raison)

Alan
la source
Reproduction
2
meilleure solution que j'ai vue. stackoverflow.com/questions/1483581/…
Khaled Annajar
Ma réponse pour ce qui suit pourrait vous aider, stackoverflow.com/questions/4163579/…
Suhas Aithal
Essayez ma réponse ici - stackoverflow.com/questions/4163579/…
Suhas Aithal

Réponses:

288

Le rechargement se produit lors de la prochaine passe de mise en page, ce qui se produit normalement lorsque vous retournez le contrôle à la boucle d'exécution (après, par exemple, l'action de votre bouton ou tout ce qui retourne).

Donc, une façon d'exécuter quelque chose après le rechargement de la vue de table est simplement de forcer la vue de table à 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 façon consiste à planifier votre code de mise en page pour qu'il s'exécute plus tard 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];
});

METTRE À JOUR

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

Cependant, la vue de table n'envoie pas tableView:cellForRowAtIndexPath:ou tableView:headerViewForSectionjusqu'à la phase de disposition, ce qui se produit par défaut lorsque vous retournez le contrôle à la boucle d'exécution.

Je trouve également que dans un petit programme de test, le code de votre question défile correctement vers le bas de la vue du tableau, sans que je fasse rien de spécial (comme envoyer layoutIfNeededou utiliser dispatch_async).

Rob Mayoff
la source
3
@rob, en fonction de la taille de votre source de données de table, vous pouvez animer en allant au bas de la vue de table dans la même boucle d'exécution. Si vous essayez votre code de test avec une table énorme, votre astuce d'utilisation de GCD pour retarder le défilement jusqu'à la prochaine boucle d'exécution fonctionnera, alors que le défilement immédiat échouera. Mais de toute façon, merci pour cette astuce !!
M. T
7
La méthode 2 n'a pas fonctionné pour moi pour une raison inconnue, mais a choisi la première méthode à la place.
Raj Pawan Gumdal
4
la dispatch_async(dispatch_get_main_queue())méthode n'est pas garantie de fonctionner. Je vois un comportement non déterministe avec lui, dans lequel parfois le système a terminé les layoutSubviews et le rendu de la cellule avant le bloc d'achèvement, et parfois après. Je publierai une réponse qui a fonctionné pour moi ci-dessous.
Tyler Sheaffer
3
Acceptez de dispatch_async(dispatch_get_main_queue())ne pas toujours travailler. Voir des résultats aléatoires ici.
Vojto
1
Le thread principal exécute un fichier NSRunLoop. Une boucle d'exécution comporte différentes phases et vous pouvez planifier un rappel pour une phase spécifique (en utilisant a CFRunLoopObserver). UIKit planifie la mise en page lors d'une phase ultérieure, après le retour de votre gestionnaire d'événements.
rob mayoff
106

Rapide:

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

...somewhere later...

tableView.reloadData {
    println("done")
}

Objectif c:

[UIView animateWithDuration:0 animations:^{
    [myTableView reloadData];
} completion:^(BOOL finished) {
    //Do something after that...
}];
Aviel Gross
la source
16
Cela équivaut à distribuer quelque chose sur le thread principal dans un «futur proche». Il est probable que vous voyiez simplement la vue de table rendre les objets avant que le thread principal ne déque le bloc d'achèvement. Il n'est pas conseillé de faire ce genre de piratage en premier lieu, mais dans tous les cas, vous devriez utiliser dispatch_after si vous comptez tenter cela.
seo
1
La solution de Rob est bonne mais ne fonctionne pas s'il n'y a pas de lignes dans le tableau. La solution d'Aviel a l'avantage de fonctionner même lorsque le tableau ne contient pas de lignes mais uniquement des sections.
Chrstph SLN
@Christophe À partir de maintenant, j'ai pu utiliser la mise à jour de Rob dans une vue de table sans aucune ligne en remplaçant dans mon contrôleur de vue Mock la tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Intméthode et en insérant dans mon override ce que je voulais pour informer que le rechargement était terminé.
Gobe
49

À partir de Xcode 8.2.1, iOS 10 et swift 3,

Vous pouvez déterminer la fin de tableView.reloadData()facilement en utilisant un bloc CATransaction:

CATransaction.begin()
CATransaction.setCompletionBlock({
    print("reload completed")
    //Your completion code here
})
print("reloading")
tableView.reloadData()
CATransaction.commit()

Ce qui précède fonctionne également pour déterminer la fin de reloadData () d'UICollectionView et reloadAllComponents () d'UIPickerView.

kolaworld
la source
👍 Je travaille également si vous effectuez un rechargement personnalisé, comme insérer, supprimer ou déplacer manuellement des lignes dans la vue tableau, dans beginUpdateset dans les endUpdatesappels.
Darrarski
Je pense que c'est en fait la solution moderne. en effet, c'est le modèle commun dans iOS, exemple ... stackoverflow.com/a/47536770/294884
Fattie
J'ai essayé ça. J'ai un comportement très étrange. Ma tableview affiche correctement deux headerViews. À l'intérieur de setCompletionBlockmes numberOfSectionsspectacles 2 ... jusqu'ici tout va bien. Pourtant si à l'intérieur setCompletionBlockje le fais tableView.headerView(forSection: 1)ça revient nil!!! par conséquent, je pense que ce bloc se produit soit avant le rechargement, soit capture quelque chose avant, soit je fais quelque chose de mal. Pour info, j'ai essayé la réponse de Tyler et cela a fonctionné! @Fattie
Honey
32

La dispatch_async(dispatch_get_main_queue())méthode ci-dessus n'est pas garantie de fonctionner . Je vois un comportement non déterministe avec lui, dans lequel parfois le système a terminé les layoutSubviews et le rendu de la cellule 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 UITableView ou UICollectionView en tant que sous-classe personnalisée. Voici la solution 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 ici pour une version Swift de cette réponse

Tyler Sheaffer
la source
C'est la seule manière correcte. Je l'ai ajouté à mon projet car j'avais besoin des images finales de certaines cellules à des fins d'animation. J'ai également ajouté et édité pour Swift. J'espère que ça ne vous dérange pas 😉
Jon Vogel
2
Une fois que vous avez appelé le bloc, layoutSubviewsil doit être défini sur nilcar les appels ultérieurs à layoutSubviews, pas nécessairement dus à l' reloadDataappel, entraîneront l'exécution du bloc car une référence forte est conservée, ce qui n'est pas le comportement souhaité.
Mark Bourke
pourquoi ne puis-je pas l'utiliser pour UITableView? il ne montre aucune interface visible. J'ai importé le fichier d'en-tête aussi mais toujours le même
Julfikar
2
Un ajout à cette réponse est qu'il est possible de supprimer le rappel existant s'il n'y en a qu'un, ce qui signifie que plusieurs appelants auront une condition de concurrence. La solution consiste à créer reloadDataCompletionBlockun tableau de blocs et à les parcourir lors de l'exécution et à vider le tableau après cela.
Tyler Sheaffer
1) n'est-ce pas équivalent à la première réponse de Rob, c'est-à-dire utiliser layoutIfNeeded? 2) Pourquoi avez-vous mentionné iOS 10, cela ne fonctionne-t-il pas sur iOS 9?!
Honey
30

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

J'ai implémenté sa solution dans 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
}
Shawn Aukstak
la source
1
agréable! petite pioche, vous pouvez supprimer le if leten disant reloadDataCompletionBlock?()qui appellera si ce n'est pas nul 💥
Tyler Sheaffer
Pas de chance avec celui-ci dans ma situation sur ios9
Matjan
self.reloadDataCompletionBlock? { completion() }aurait dû êtreself.reloadDataCompletionBlock?()
emem
Comment gérer le redimensionnement de la hauteur de vue du tableau? J'appelais précédemment tableView.beginUpdates () tableView.layoutIfNeeded () tableView.endUpdates ()
Parth Tamane
10

Et une UICollectionViewversion, basée sur la réponse de kolaworld:

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

Besoin de tests. Fonctionne jusqu'à présent sur iOS 9.2, Xcode 9.2 beta 2, avec le défilement d'une collectionView vers un index, en guise de 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()
    }
}

Usage:

myCollectionView.reloadDataThenPerform {
    myCollectionView.scrollToItem(at: indexPath,
            at: .centeredVertically,
            animated: true)
}
Womble
la source
6

Il semble que les gens lisent toujours cette question et les réponses. B / c de cela, je modifie ma réponse pour supprimer le mot synchrone qui est vraiment hors de propos.

When [tableView reloadData]renvoie, les structures de données internes derrière la tableView ont été mises à jour. Par conséquent, lorsque la méthode est terminée, vous pouvez faire défiler en toute sécurité vers le bas. J'ai vérifié cela dans ma propre application. La réponse largement acceptée de @ rob-mayoff, tout en prêtant à confusion dans la terminologie, reconnaît la même chose dans sa dernière mise à jour.

Si votre tableViewne défile pas vers le bas, vous pouvez avoir un problème avec un autre code que vous n'avez pas publié. Peut-être que vous modifiez les données une fois le défilement terminé et que vous ne rechargez pas et / ou ne faites pas défiler vers le bas alors?

Ajoutez de la journalisation comme suit pour vérifier que les données de la table sont correctes après reloadData. J'ai le code suivant dans un exemple d'application et cela fonctionne parfaitement.

// change the data source

NSLog(@"Before reload / sections = %d, last row = %d",
      [self.tableView numberOfSections],
      [self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);

[self.tableView reloadData];

NSLog(@"After reload / sections = %d, last row = %d",
      [self.tableView numberOfSections],
      [self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]);

[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:[self.tableView numberOfSections]-1]-1
                                                          inSection:[self.tableView numberOfSections] - 1]
                      atScrollPosition:UITableViewScrollPositionBottom
                              animated:YES];
XJones
la source
J'ai mis à jour mes questions. Savez-vous pourquoi mes NSLogs sortiraient comme ça?
Alan
8
reloadDatan'est pas synchrone. Il était - voir cette réponse: stackoverflow.com/a/16071589/193896
bendytree
1
C'est synchrone. Il est très facile de tester et de voir cela avec un exemple d'application. Vous avez lié à la réponse de @ rob dans cette question. Si vous lisez sa mise à jour en bas, il l'a également vérifié. Vous parlez peut-être des changements de disposition visuelle. Il est vrai que la tableView n'est pas visiblement mise à jour de manière synchrone mais les données le sont. C'est pourquoi les valeurs dont l'OP a besoin sont correctes immédiatement après les reloadDataretours.
XJones
1
Vous pouvez être confus sur ce qui devrait se passer reloadData. Utilisez mon cas de test dans viewWillAppearaccept pour la scrollToRowAtIndexPath:ligne b / c qui n'a pas de sens si le tableViewn'est pas affiché. Vous verrez que reloadDatacela a mis à jour les données mises en cache dans l' tableViewinstance et qui reloadDataest synchrone. Si vous faites référence à d'autres tableViewméthodes de délégué appelées lors de la tableViewmise en page, celles-ci ne seront pas appelées si le tableViewn'est pas affiché. Si je ne comprends pas votre scénario, veuillez expliquer.
XJones
3
Quels moments amusants. Nous sommes en 2014 et il y a des arguments pour savoir si une méthode est synchrone et asynchrone ou non. On a l'impression de deviner. Tous les détails d'implémentation sont complètement opaques derrière ce nom de méthode. La programmation n'est-elle pas géniale?
fatuhoku
5

J'utilise cette astuce, je suis sûr que je l'ai déjà postée sur un double de cette question:

-(void)tableViewDidLoadRows:(UITableView *)tableView{
    // do something after loading, e.g. select a cell.
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // trick to detect when table view has finished loading.
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];
    [self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];

    // specific to your controller
    return self.objects.count;
}
malhal
la source
@Fattie on ne sait pas si vous l'entendez comme un commentaire positif ou un commentaire négatif. Mais j'ai vu que vous aviez commenté une autre réponse comme "cela semble être la meilleure solution!" , donc je suppose que relativement parlant, vous ne considérez pas cette solution comme la meilleure.
Cœur
1
Vous vous appuyez sur un effet secondaire d'une fausse animation? Ce n'est pas une bonne idée. Apprenez à effectuer le sélecteur ou GCD et faites-le correctement. Btw, il existe maintenant une méthode chargée par table que vous pouvez simplement utiliser si cela ne vous dérange pas d'utiliser un protocole privé, ce qui est probablement très bien car c'est le framework qui appelle votre code plutôt que l'inverse.
malhal
3

En fait, celui-ci a résolu mon problème:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {

NSSet *visibleSections = [NSSet setWithArray:[[tableView indexPathsForVisibleRows] valueForKey:@"section"]];
if (visibleSections) {
    // hide the activityIndicator/Loader
}}
Asha Antony
la source
2

Essayez de cette façon, cela fonctionnera

[tblViewTerms performSelectorOnMainThread:@selector(dataLoadDoneWithLastTermIndex:) withObject:lastTermIndex waitUntilDone:YES];waitUntilDone:YES];

@interface UITableView (TableViewCompletion)

-(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex;

@end

@implementation UITableView(TableViewCompletion)

-(void)dataLoadDoneWithLastTermIndex:(NSNumber*)lastTermIndex
{
    NSLog(@"dataLoadDone");


NSIndexPath* indexPath = [NSIndexPath indexPathForRow: [lastTermIndex integerValue] inSection: 0];

[self selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];

}
@end

J'exécuterai quand la table sera complètement chargée

L'autre solution est que vous pouvez sous-classer UITableView

Shashi3456643
la source
1

J'ai fini par utiliser une variante de la solution de Shawn:

Créez une classe UITableView personnalisée avec un délégué:

protocol CustomTableViewDelegate {
    func CustomTableViewDidLayoutSubviews()
}

class CustomTableView: UITableView {

    var customDelegate: CustomTableViewDelegate?

    override func layoutSubviews() {
        super.layoutSubviews()
        self.customDelegate?.CustomTableViewDidLayoutSubviews()
    }
}

Puis dans mon code, j'utilise

class SomeClass: UIViewController, CustomTableViewDelegate {

    @IBOutlet weak var myTableView: CustomTableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.myTableView.customDelegate = self
    }

    func CustomTableViewDidLayoutSubviews() {
        print("didlayoutsubviews")
        // DO other cool things here!!
    }
}

Assurez-vous également de définir votre vue de table sur CustomTableView dans le générateur d'interface:

entrez la description de l'image ici

Sam
la source
cela fonctionne mais le problème est que la méthode est touchée à chaque fois qu'elle charge une seule cellule, PAS TOUTE LA VUE DE TABLE RECHARGE, donc clairement cette réponse n'est pas en rapport avec la question posée.
Yash Bedi
Certes, il est appelé plus d'une fois, mais pas sur toutes les cellules. Vous pouvez donc écouter le premier délégué et ignorer le reste jusqu'à ce que vous appeliez à nouveau reloadData.
Sam
1

Dans Swift 3.0 +, nous pouvons créer une extension pour UITableViewavec un escaped Closurecomme ci-dessous:

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

Et utilisez-le comme ci-dessous où vous voulez:

Your_Table_View.reloadData {
   print("reload done")
 }

espérons que cela aidera quelqu'un. à votre santé!

Caldeira de Chanaka
la source
Idée géniale ... seule chose, c'est juste pour éviter toute confusion, j'ai changé le nom de la fonction pour recharger, plutôt que reloadData (). Merci
Vijay Kumar AB
1

Détails

  • Xcode version 10.2.1 (10E1001), Swift 5

Solution

import UIKit

// MARK: - UITableView reloading functions

protocol ReloadCompletable: class { func reloadData() }

extension ReloadCompletable {
    func run(transaction closure: (() -> Void)?, completion: (() -> Void)?) {
        guard let closure = closure else { return }
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        closure()
        CATransaction.commit()
    }

    func run(transaction closure: (() -> Void)?, completion: ((Self) -> Void)?) {
        run(transaction: closure) { [weak self] in
            guard let self = self else { return }
            completion?(self)
        }
    }

    func reloadData(completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadData() }, completion: closure)
    }
}

// MARK: - UITableView reloading functions

extension ReloadCompletable where Self: UITableView {
    func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadRows(at: indexPaths, with: animation) }, completion: closure)
    }

    func reloadSections(_ sections: IndexSet, with animation: UITableView.RowAnimation, completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadSections(sections, with: animation) }, completion: closure)
    }
}

// MARK: - UICollectionView reloading functions

extension ReloadCompletable where Self: UICollectionView {

    func reloadSections(_ sections: IndexSet, completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadSections(sections) }, completion: closure)
    }

    func reloadItems(at indexPaths: [IndexPath], completion closure: ((Self) -> Void)?) {
        run(transaction: { [weak self] in self?.reloadItems(at: indexPaths) }, completion: closure)
    }
}

Usage

UITableView

// Activate
extension UITableView: ReloadCompletable { }

// ......
let tableView = UICollectionView()

// reload data
tableView.reloadData { tableView in print(collectionView) }

// or
tableView.reloadRows(at: indexPathsToReload, with: rowAnimation) { tableView in print(tableView) }

// or
tableView.reloadSections(IndexSet(integer: 0), with: rowAnimation) { _tableView in print(tableView) }

UICollectionView

// Activate
extension UICollectionView: ReloadCompletable { }

// ......
let collectionView = UICollectionView()

// reload data
collectionView.reloadData { collectionView in print(collectionView) }

// or
collectionView.reloadItems(at: indexPathsToReload) { collectionView in print(collectionView) }

// or
collectionView.reloadSections(IndexSet(integer: 0)) { collectionView in print(collectionView) }

Échantillon complet

N'oubliez pas d' ajouter le code de la solution ici

import UIKit

class ViewController: UIViewController {

    private weak var navigationBar: UINavigationBar?
    private weak var tableView: UITableView?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupNavigationItem()
        setupTableView()
    }
}
// MARK: - Activate UITableView reloadData with completion functions

extension UITableView: ReloadCompletable { }

// MARK: - Setup(init) subviews

extension ViewController {

    private func setupTableView() {
        guard let navigationBar = navigationBar else { return }
        let tableView = UITableView()
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.dataSource = self
        self.tableView = tableView
    }

    private func setupNavigationItem() {
        let navigationBar = UINavigationBar()
        view.addSubview(navigationBar)
        self.navigationBar = navigationBar
        navigationBar.translatesAutoresizingMaskIntoConstraints = false
        navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        let navigationItem = UINavigationItem()
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "all", style: .plain, target: self, action: #selector(reloadAllCellsButtonTouchedUpInside(source:)))
        let buttons: [UIBarButtonItem] = [
                                            .init(title: "row", style: .plain, target: self,
                                                  action: #selector(reloadRowButtonTouchedUpInside(source:))),
                                            .init(title: "section", style: .plain, target: self,
                                                  action: #selector(reloadSectionButtonTouchedUpInside(source:)))
                                            ]
        navigationItem.leftBarButtonItems = buttons
        navigationBar.items = [navigationItem]
    }
}

// MARK: - Buttons actions

extension ViewController {

    @objc func reloadAllCellsButtonTouchedUpInside(source: UIBarButtonItem) {
        let elementsName = "Data"
        print("-- Reloading \(elementsName) started")
        tableView?.reloadData { taleView in
            print("-- Reloading \(elementsName) stopped \(taleView)")
        }
    }

    private var randomRowAnimation: UITableView.RowAnimation {
        return UITableView.RowAnimation(rawValue: (0...6).randomElement() ?? 0) ?? UITableView.RowAnimation.automatic
    }

    @objc func reloadRowButtonTouchedUpInside(source: UIBarButtonItem) {
        guard let tableView = tableView else { return }
        let elementsName = "Rows"
        print("-- Reloading \(elementsName) started")
        let indexPathToReload = tableView.indexPathsForVisibleRows?.randomElement() ?? IndexPath(row: 0, section: 0)
        tableView.reloadRows(at: [indexPathToReload], with: randomRowAnimation) { _tableView in
            //print("-- \(taleView)")
            print("-- Reloading \(elementsName) stopped in \(_tableView)")
        }
    }

    @objc func reloadSectionButtonTouchedUpInside(source: UIBarButtonItem) {
        guard let tableView = tableView else { return }
        let elementsName = "Sections"
        print("-- Reloading \(elementsName) started")
        tableView.reloadSections(IndexSet(integer: 0), with: randomRowAnimation) { _tableView in
            //print("-- \(taleView)")
            print("-- Reloading \(elementsName) stopped in \(_tableView)")
        }
    }
}

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int { return 1 }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 20 }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = "\(Date())"
        return cell
    }
}

Résultats

entrez la description de l'image ici

Vasily Bodnarchuk
la source
0

Juste pour proposer une autre approche, basée sur l'idée que l'achèvement est la cellule «dernière visible» à envoyer cellForRow.

// Will be set when reload is called
var lastIndexPathToDisplay: IndexPath?

typealias ReloadCompletion = ()->Void

var reloadCompletion: ReloadCompletion?

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    // Setup cell

    if indexPath == self.lastIndexPathToDisplay {

        self.lastIndexPathToDisplay = nil

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

    // Return cell
...

func reloadData(completion: @escaping ReloadCompletion) {

    self.reloadCompletion = completion

    self.mainTable.reloadData()

    self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last
}

Un problème possible est le suivant: si reloadData()a terminé avant que le ne lastIndexPathToDisplaysoit défini, la cellule `` dernière visible '' sera affichée avant d' lastIndexPathToDisplayêtre définie et la complétion ne sera pas appelée (et sera en état `` en attente ''):

self.mainTable.reloadData()

// cellForRowAt could be finished here, before setting `lastIndexPathToDisplay`

self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last

Si nous inversons, nous pourrions finir par déclencher l'achèvement en faisant défiler avant reloadData().

self.lastIndexPathToDisplay = self.mainTable.indexPathsForVisibleRows?.last

// cellForRowAt could trigger the completion by scrolling here since we arm 'lastIndexPathToDisplay' before 'reloadData()'

self.mainTable.reloadData()
bauerMusique
la source
0

Essaye ça:

tableView.backgroundColor = .black

tableView.reloadData()

DispatchQueue.main.async(execute: {

    tableView.backgroundColor = .green

})

La couleur de tableView passera du noir au vert uniquement une fois la reloadData()fonction terminée.

Nirbhay Singh
la source
0

Vous pouvez utiliser la fonction performBatchUpdates de uitableview

Voici comment vous pouvez réaliser

self.tableView.performBatchUpdates({

      //Perform reload
        self.tableView.reloadData()
    }) { (completed) in

        //Reload Completed Use your code here
    }
Noman Haroon
la source
0

Création d'une extension réutilisable de CATransaction:

public extension CATransaction {
    static func perform(method: () -> Void, completion: @escaping () -> Void) {
        begin()
        setCompletionBlock {
            completion()
        }
        method()
        commit()
    }
}

Créez maintenant une extension de UITableView qui utiliserait la méthode d'extension de CATransaction:

public extension UITableView {
    func reloadData(completion: @escaping (() -> Void)) {
       CATransaction.perform(method: {
           reloadData()
       }, completion: completion)
    }
}

Usage:

tableView.reloadData(completion: {
    //Do the stuff
})
Umair
la source
-2

Vous pouvez l'utiliser pour faire quelque chose après le rechargement des données:

[UIView animateWithDuration:0 animations:^{
    [self.contentTableView reloadData];
} completion:^(BOOL finished) {
    _isUnderwritingUpdate = NO;
}];
Vit
la source
-20

Essayez de définir des délais:

[_tableView performSelector:@selector(reloadData) withObject:nil afterDelay:0.2];
[_activityIndicator performSelector:@selector(stopAnimating) withObject:nil afterDelay:0.2];
Jake
la source
14
C'est dangereux. Et si le rechargement prend plus de temps que votre retard?
rob