web-dev-qa-db-fra.com

Comment savoir quand UITableView a terminé ReloadData?

J'essaie de faire défiler le bas d'une UITableView après avoir terminé l'exécution de [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 puisque self.tableView, [self.tableView numberOfSections] et [self.tableView numberOfRowsinSection sont 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 retourne Sections = 1, Row = -1;

Quand je fais exactement le même NSLogs dans cellForRowAtIndexPath, j'obtiens Sections = 1 et Row = 8; (8 est juste)

146
Alan

Le rechargement a lieu lors de la prochaine passe de disposition, ce qui se produit normalement lorsque vous redonnez le contrôle à la boucle d’exécution (après, par exemple, votre action sur le bouton ou quoi que ce soit qui retourne).

Ainsi, une façon d'exécuter quelque chose après le rechargement de la vue tableau consiste simplement à 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 méthode consiste à planifier l’exécution ultérieure de votre code après mise en page à l’aide de 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 un examen plus approfondi, je constate que la vue tabulaire envoie tableView:numberOfSections: et tableView:numberOfRowsInSection: à sa source de données avant de revenir de reloadData. Si le délégué implémente tableView:heightForRowAtIndexPath:, la vue de table l'envoie également (pour chaque ligne) avant de revenir de reloadData.

Toutefois, la vue tableau n'envoie pas tableView:cellForRowAtIndexPath: ni tableView:headerViewForSection avant la phase de mise en page, qui se produit par défaut lorsque vous renvoyez le contrôle à la boucle d'exécution.

Je constate également que dans un programme de test minuscule, le code de votre question défile correctement jusqu'au bas de la vue tabulaire, sans me faisant quoi que ce soit de spécial (comme envoyer layoutIfNeeded ou utiliser dispatch_async).

253
rob mayoff

Rapide:

extension UITableView {
    func reloadData(completion: ()->()) {
        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...
}];
92
Aviel Gross

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

Vous pouvez facilement déterminer la fin de tableView.reloadData() 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 de reloadAllComponents () d'UIPickerView.

28
kolaworld

La méthode dispatch_async(dispatch_get_main_queue()) ci-dessus est garantie que n'est pas garantie. Je constate un comportement non déterministe, dans lequel le système a parfois terminé la mise en formeSubviews et le rendu de la cellule avant le bloc d'achèvement, et parfois après.

Voici une solution qui fonctionne à 100% pour moi, sous 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;
    [super 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

24
Tyler Sheaffer

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
    super.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
      super.reloadData()
  }
}

Exemple d'utilisation:

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

Il semble que les gens lisent encore cette question et les réponses. B/c de cela, je modifie ma réponse pour supprimer le Word Synchronous qui est vraiment sans rapport avec cela.

When [tableView reloadData] retourne, 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 en toute sécurité faire défiler 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 sur le plan terminologique, reconnaît la même chose dans sa dernière mise à jour.

Si votre tableView ne fait pas défiler l'écran vers le bas, vous rencontrez peut-être un problème avec un autre code que vous n'avez pas posté. Vous modifiez peut-être les données une fois le défilement terminé et vous ne rechargez pas et/ou ne faites pas défiler l'écran vers le bas?

Ajoutez une 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];
6
XJones

Et une version UICollectionView, 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 collectionVue 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()
    }
}

Utilisation:

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

J'utilise cette astuce, je suis sûr d'avoir déjà posté cette copie sur un duplicata 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;
}
5
malhal

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
}}
3
Asha Antony

Essayez de cette façon ça va marcher

[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

Je vais exécuter quand la table est complètement chargée

Une autre solution est que vous pouvez sous-classe UITableView

2
Shashi3456643

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 que vous définissez votre vue table sur CustomTableView dans le générateur d'interface:

 enter image description here

1
Sam

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

enter image description here

0
Vasily Bodnarchuk

Juste pour proposer une autre approche, basée sur l'idée que l'achèvement est la «dernière cellule visible» à être envoyée à 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() est terminé avant que la lastIndexPathToDisplay ne soit définie, la cellule "Dernière visible" sera affichée avant que lastIndexPathToDisplay ne soit définie et la finalisation ne sera pas appelée (et sera dans l'é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()
0
bauerMusic

Dans Swift 3.0 + nous pouvons créer une extension pour UITableView avec un escaped Closure comme 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é!

0
Anuradh S

Essaye ça:

tableView.backgroundColor = .black

tableView.reloadData ()

DispatchQueue.main.async (execute: {

tableView.backgroundColor = .green

})

// La couleur de la tableView passera du noir au vert uniquement après la fin de la fonction reloadData ().

0
Nirbhay Singh