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)
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];
});
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
).
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...
}];
À 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.
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
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
}
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];
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)
}
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;
}
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
}}
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
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:
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)
}
}
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) }
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
}
}
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()
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é!
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 ().