web-dev-qa-db-fra.com

Recevoir une notification lorsque UITableView a fini de demander des données?

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).

108
kennethmac2000

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 UITableViewreloadData 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.

60
Eric MORAND

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

enter image description here

51
iPrabu

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
                                        }];
25
bandejapaisa

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
}
11
Black Coder

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 dataSourcecellForRowAtIndexPath.

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. :)

10
Mark Kryzhanouski

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.

8
Symmetric

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é! :)

6
Abdullah Umer

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 ...

2
Josh Brown

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.

1
Walt Sellers

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.

1
Joost

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.

1
Joe M

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 -

  1. appeler dans "- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath"
  2. assurez-vous simplement que le message "scrollToRowAtIndexPath" est envoyé à l'instance appropriée de UITableView, qui est certainement MyTableview dans ce cas.
  3. Dans mon cas, UIView est la vue qui contient une instance de UITableView
  4. En outre, cela sera appelé pour chaque charge de cellule. Par conséquent, placez une logique dans "cellForRowAtIndexPath" pour éviter d'appeler "scrollToRowAtIndexPath" plus d'une fois.
0
smile.al.d.way

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;
                    }
                }));
        }
    }
0
Eugene Tiutiunnyk

UITableViewlayoutSubviews 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.

0
Eric MORAND

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);
}
0
Banker Mittal

La meilleure solution que j'ai trouvée dans Swift

extension UITableView {
    func reloadData(completion: ()->()) {
        self.reloadData()
        dispatch_async(dispatch_get_main_queue()) {
            completion()
        }
    }
}
0
YannSteph

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.

0