Existe-t-il un moyen de savoir quand un UITableView
a fini de demander des données à sa source de données?
Aucune des méthodes viewDidLoad
/viewWillAppear
/viewDidAppear
du contrôleur de vue associé (UITableViewController
) n'est utile ici, car elles se déclenchent toutes trop tôt. Aucun d'entre eux (tout à fait compréhensible) ne garantit que les requêtes à la source de données sont terminées pour le moment (par exemple, jusqu'à ce que la vue défile).
Une solution de contournement que j'ai trouvée consiste à appeler reloadData
dans viewDidAppear
, car, lorsque reloadData
revient, la vue de la table is garantit d'avoir fini d'interroger le source de données autant que nécessaire pour le moment.
Cependant, cela semble plutôt désagréable, car je suppose que cela provoque la double demande des mêmes informations à la source de données (une fois automatiquement et une fois en raison de l'appel reloadData
) lors de son premier chargement.
La raison pour laquelle je veux faire cela est que je veux conserver la position de défilement du UITableView
- mais jusqu'au niveau du pixel, pas seulement jusqu'à la ligne la plus proche.
Lors de la restauration de la position de défilement (à l'aide de scrollRectToVisible:animated:
), J'ai besoin que la vue de table contienne déjà suffisamment de données, sinon le scrollRectToVisible:animated:
L'appel de méthode ne fait rien (ce qui se produit si vous placez l'appel seul dans l'un des viewDidLoad
, viewWillAppear
ou viewDidAppear
).
Cette réponse ne semble plus fonctionner, en raison de certaines modifications apportées à l'implémentation UITableView depuis la réponse. Voir ce commentaire: Recevoir une notification lorsque UITableView a fini de demander des données?
Je joue avec ce problème depuis quelques jours et je pense que le sous-classement de UITableView
reloadData
est la meilleure approche:
- (void)reloadData {
NSLog(@"BEGIN reloadData");
[super reloadData];
NSLog(@"END reloadData");
}
reloadData
ne se termine pas avant que la table ait fini de recharger ses données. Ainsi, lorsque le second NSLog
est déclenché, la vue de table a en fait fini de demander des données.
J'ai sous-classé UITableView
pour envoyer des méthodes au délégué avant et après reloadData
. Il fonctionne comme un charme.
J'ai eu un même scénario dans mon application et j'ai pensé publier ma réponse à vous les gars car les autres réponses mentionnées ici ne fonctionnent pas pour moi pour iOS7 et versions ultérieures
Enfin, c'est la seule chose qui a fonctionné pour moi.
[yourTableview reloadData];
dispatch_async(dispatch_get_main_queue(),^{
NSIndexPath *path = [NSIndexPath indexPathForRow:yourRow inSection:yourSection];
//Basically maintain your logic to get the indexpath
[yourTableview scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
Mise à jour rapide:
yourTableview.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let path : NSIndexPath = NSIndexPath(forRow: myRowValue, inSection: mySectionValue)
//Basically maintain your logic to get the indexpath
yourTableview.scrollToRowAtIndexPath(path, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
})
Alors, comment ça marche.
Fondamentalement, lorsque vous effectuez un rechargement, le thread principal devient occupé, alors au moment où nous effectuons un thread asynchrone de répartition, le bloc attend que le thread principal soit terminé. Donc, une fois que la vue de table a été complètement chargée, le thread principal sera terminé et il enverra donc notre bloc de méthode
Testé sur iOS7 et iOS8 et ça marche génial;)
Mise à jour pour iOS9: Cela fonctionne très bien est iOS9 également. J'ai créé un exemple de projet dans github en tant que POC. https: //github.com/ipraba/TableReloadingNotifier
Je joins ici la capture d'écran de mon test.
Environnement testé: simulateur iOS9 iPhone6 de Xcode7
EDIT: Cette réponse n'est en fait pas une solution. Elle semble probablement fonctionner au début parce que le rechargement peut se produire assez rapidement, mais en fait le bloc d'achèvement n'est pas nécessairement appelé une fois que les données ont complètement terminé le rechargement - parce que reloadData ne bloque pas. Vous devriez probablement chercher une meilleure solution.
Pour développer la réponse de @Eric MORAND, mettons un bloc d'achèvement. Qui n'aime pas un bloc?
@interface DUTableView : UITableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
@end
et...
#import "DUTableView.h"
@implementation DUTableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[super reloadData];
if(completionBlock) {
completionBlock();
}
}
@end
Usage:
[self.tableView reloadDataWithCompletion:^{
//do your stuff here
}];
reloadData demande simplement des données pour les cellules visibles. Dit, pour être averti lorsque spécifier une partie de votre table est chargée, veuillez accrocher le tableView: willDisplayCell:
méthode.
- (void) reloadDisplayData
{
isLoading = YES;
NSLog(@"Reload display with last index %d", lastIndex);
[_tableView reloadData];
if(lastIndex <= 0){
isLoading = YES;
//Notify completed
}
- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row >= lastIndex){
isLoading = NO;
//Notify completed
}
Voilà ma solution. 100% fonctionne et utilisé dans de nombreux projets. Il s'agit d'une simple sous-classe UITableView.
@protocol MyTableViewDelegate<NSObject, UITableViewDelegate>
@optional
- (void)tableViewWillReloadData:(UITableView *)tableView;
- (void)tableViewDidReloadData:(UITableView *)tableView;
@end
@interface MyTableView : UITableView {
struct {
unsigned int delegateWillReloadData:1;
unsigned int delegateDidReloadData:1;
unsigned int reloading:1;
} _flags;
}
@end
@implementation MyTableView
- (id<MyTableViewDelegate>)delegate {
return (id<MyTableViewDelegate>)[super delegate];
}
- (void)setDelegate:(id<MyTableViewDelegate>)delegate {
[super setDelegate:delegate];
_flags.delegateWillReloadData = [delegate respondsToSelector:@selector(tableViewWillReloadData:)];
_flags.delegateDidReloadData = [delegate respondsToSelector:@selector(tableViewDidReloadData:)];
}
- (void)reloadData {
[super reloadData];
if (_flags.reloading == NO) {
_flags.reloading = YES;
if (_flags.delegateWillReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewWillReloadData:self];
}
[self performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f];
}
}
- (void)finishReload {
_flags.reloading = NO;
if (_flags.delegateDidReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewDidReloadData:self];
}
}
@end
C'est similaire à la solution de Josh Brown à une exception près. Aucun délai n'est nécessaire dans la méthode performSelector. Peu importe le temps que prend reloadData
. tableViewDidLoadData:
Se déclenche toujours lorsque tableView
finit de demander dataSource
cellForRowAtIndexPath
.
Même si vous ne voulez pas sous-classer UITableView
, vous pouvez simplement appeler [performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f]
et votre sélecteur sera appelé juste après la fin du rechargement de la table. Mais vous devez vous assurer que le sélecteur n'est appelé qu'une seule fois par appel à reloadData
:
[self.tableView reloadData];
[self performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f];
Prendre plaisir. :)
Ceci est une réponse à une question légèrement différente: j'avais besoin de savoir quand UITableView
avait également fini d'appeler cellForRowAtIndexPath()
. J'ai sous-classé layoutSubviews()
(merci @Eric MORAND) et ajouté un rappel de délégué:
SDTableView.h:
@protocol SDTableViewDelegate <NSObject, UITableViewDelegate>
@required
- (void)willReloadData;
- (void)didReloadData;
- (void)willLayoutSubviews;
- (void)didLayoutSubviews;
@end
@interface SDTableView : UITableView
@property(nonatomic,assign) id <SDTableViewDelegate> delegate;
@end;
SDTableView.m:
#import "SDTableView.h"
@implementation SDTableView
@dynamic delegate;
- (void) reloadData {
[self.delegate willReloadData];
[super reloadData];
[self.delegate didReloadData];
}
- (void) layoutSubviews {
[self.delegate willLayoutSubviews];
[super layoutSubviews];
[self.delegate didLayoutSubviews];
}
@end
Utilisation:
MyTableViewController.h:
#import "SDTableView.h"
@interface MyTableViewController : UITableViewController <SDTableViewDelegate>
@property (nonatomic) BOOL reloadInProgress;
MyTableViewController.m:
#import "MyTableViewController.h"
@implementation MyTableViewController
@synthesize reloadInProgress;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if ( ! reloadInProgress) {
NSLog(@"---- numberOfSectionsInTableView(): reloadInProgress=TRUE");
reloadInProgress = TRUE;
}
return 1;
}
- (void)willReloadData {}
- (void)didReloadData {}
- (void)willLayoutSubviews {}
- (void)didLayoutSubviews {
if (reloadInProgress) {
NSLog(@"---- layoutSubviewsEnd(): reloadInProgress=FALSE");
reloadInProgress = FALSE;
}
}
REMARQUES: Comme il s'agit d'une sous-classe de UITableView
qui a déjà une propriété déléguée pointant vers MyTableViewController
il n'est pas nécessaire de ajoutez-en un autre. Le "délégué @dynamic" indique au compilateur d'utiliser cette propriété. (Voici un lien décrivant ceci: http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/ )
La propriété UITableView
dans MyTableViewController
doit être modifiée pour utiliser la nouvelle classe SDTableView
. Cela se fait dans l'Inspecteur d'identité d'Interface Builder. Sélectionnez le UITableView
à l'intérieur du UITableViewController
et définissez sa "Classe personnalisée" sur SDTableView
.
J'avais trouvé quelque chose de similaire pour recevoir une notification de changement dans contentSize
de TableView
. Je pense que cela devrait fonctionner ici aussi, car contentSize change également avec le chargement des données.
Essaye ça:
Dans viewDidLoad
écrivez,
[self.tableView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:NULL];
et ajoutez cette méthode à votre viewController:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"contentSize"]) {
DLog(@"change = %@", change.description)
NSValue *new = [change valueForKey:@"new"];
NSValue *old = [change valueForKey:@"old"];
if (new && old) {
if (![old isEqualToValue:new]) {
// do your stuff
}
}
}
}
Vous pourriez avoir besoin de légères modifications dans la vérification des modifications. Cela avait fonctionné pour moi cependant.
À votre santé! :)
Voici une solution possible, bien que ce soit un hack:
[self.tableView reloadData];
[self performSelector:@selector(scrollTableView) withObject:nil afterDelay:0.3];
Où votre -scrollTableView
la méthode fait défiler la vue du tableau avec -scrollRectToVisible:animated:
. Et, bien sûr, vous pouvez configurer le retard dans le code ci-dessus de 0,3 à tout ce qui semble fonctionner pour vous. Oui, c'est ridiculement hacky, mais ça marche pour moi sur mon iPhone 5 et 4S ...
Il semble que vous souhaitiez mettre à jour le contenu des cellules, mais sans les sauts soudains qui peuvent accompagner les insertions et les suppressions de cellules.
Il y a plusieurs articles sur ce sujet. En voici un.
Je suggère d'utiliser setContentOffset: animated: au lieu de scrollRectToVisible: animated: pour des réglages parfaits en pixels d'une vue de défilement.
J'avais quelque chose de similaire, je crois. J'ai ajouté un BOOL comme variable d'instance qui me dit si l'offset a été restauré et vérifie que dans -viewWillAppear:
. Lorsqu'il n'a pas été restauré, je le restaure dans cette méthode et définit le BOOL pour indiquer que j'ai récupéré le décalage.
C'est une sorte de hack et ça peut probablement être mieux fait, mais cela fonctionne pour moi en ce moment.
Vous pouvez essayer la logique suivante:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MyIdentifier"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
if ( [self chkIfLastCellIndexToCreate:tableView :indexPath]){
NSLog(@"Created Last Cell. IndexPath = %@", indexPath);
//[self.activityIndicator hide];
//Do the task for TableView Loading Finished
}
prevIndexPath = indexPath;
return cell;
}
-(BOOL) chkIfLastCellIndexToCreate:(UITableView*)tableView : (NSIndexPath *)indexPath{
BOOL bRetVal = NO;
NSArray *visibleIndices = [tableView indexPathsForVisibleRows];
if (!visibleIndices || ![visibleIndices count])
bRetVal = YES;
NSIndexPath *firstVisibleIP = [visibleIndices objectAtIndex:0];
NSIndexPath *lastVisibleIP = [visibleIndices objectAtIndex:[visibleIndices count]-1];
if ((indexPath.row > prevIndexPath.row) && (indexPath.section >= prevIndexPath.section)){
//Ascending - scrolling up
if ([indexPath isEqual:lastVisibleIP]) {
bRetVal = YES;
//NSLog(@"Last Loading Cell :: %@", indexPath);
}
} else if ((indexPath.row < prevIndexPath.row) && (indexPath.section <= prevIndexPath.section)) {
//Descending - scrolling down
if ([indexPath isEqual:firstVisibleIP]) {
bRetVal = YES;
//NSLog(@"Last Loading Cell :: %@", indexPath);
}
}
return bRetVal;
}
Et avant d'appeler reloadData, définissez prevIndexPath sur nil. Comme:
prevIndexPath = nil;
[mainTableView reloadData];
J'ai testé avec NSLogs, et cette logique semble correcte. Vous pouvez personnaliser/améliorer au besoin.
enfin j'ai fait fonctionner mon code avec ça -
[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];
il y avait peu de choses qui devaient être prises en charge -
- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
"Je lance simplement la répétition du minuteur programmé et je l'invalide uniquement lorsque contentSize de la table est plus grande lorsque la hauteur de tableHeaderView (signifie qu'il y a du contenu de lignes dans la table). Le code en C # (monotouch), mais j'espère que l'idée est claire:
public override void ReloadTableData()
{
base.ReloadTableData();
// don't do anything if there is no data
if (ItemsSource != null && ItemsSource.Length > 0)
{
_timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.MinValue,
new NSAction(() =>
{
// make sure that table has header view and content size is big enought
if (TableView.TableHeaderView != null &&
TableView.ContentSize.Height >
TableView.TableHeaderView.Frame.Height)
{
TableView.SetContentOffset(
new PointF(0, TableView.TableHeaderView.Frame.Height), false);
_timer.Invalidate();
_timer = null;
}
}));
}
}
UITableView
layoutSubviews
n'est-il pas appelé juste avant que la vue tableau affiche son contenu? J'ai remarqué qu'il est appelé une fois que la vue de table a fini de charger ses données, vous devriez peut-être enquêter dans cette direction.
Vous pouvez redimensionner votre table ou définir sa taille de contenu dans cette méthode lorsque toutes les données sont chargées:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
tableView.frame =CGRectMake(tableView.frame.Origin.x, tableView.frame.Origin.y, tableView.frame.size.width, tableView.contentSize.height);
}
La meilleure solution que j'ai trouvée dans Swift
extension UITableView {
func reloadData(completion: ()->()) {
self.reloadData()
dispatch_async(dispatch_get_main_queue()) {
completion()
}
}
}
Depuis iOS 6, la méthode déléguée UITableview
a appelé:
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
s'exécutera une fois que votre table se rechargera avec succès. Vous pouvez faire la personnalisation comme requis dans cette méthode.