Comment savoir que UICollectionView a été complètement chargé?


98

Je dois faire une opération chaque fois que UICollectionView a été complètement chargé, c'est-à-dire qu'à ce moment-là, toutes les méthodes de source de données / disposition de UICollectionView doivent être appelées. Comment sais-je ça ?? Existe-t-il une méthode déléguée pour connaître l'état chargé de UICollectionView?

Réponses:


62
// In viewDidLoad
[self.collectionView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionOld context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary  *)change context:(void *)context
{
    // You will get here when the reloadData finished 
}

- (void)dealloc
{
    [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];
}

1
cela se bloque pour moi lorsque je reviens, sinon fonctionne bien.
ericosg

4
@ericosg ajouter [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];à -viewWillDisappear:animated:aussi bien.
pxpgraphics

Brillant! Merci beaucoup!
Abdo

41
La taille du contenu change également lors du défilement et n'est donc pas fiable.
Ryan Heitner

J'ai essayé une vue de collection qui récupère ses données en utilisant une méthode de repos asynchrone. Cela signifie que Visible Cells est toujours vide lorsque cette méthode se déclenche, ce qui signifie que je fais défiler jusqu'à un élément de la collection au début.
Chucky le

155

Cela a fonctionné pour moi:

[self.collectionView reloadData];
[self.collectionView performBatchUpdates:^{}
                              completion:^(BOOL finished) {
                                  /// collection-view finished reload
                              }];

Syntaxe Swift 4:

self.collectionView.reloadData()
self.collectionView.performBatchUpdates(nil, completion: {
    (result) in
    // ready
})

1
A travaillé pour moi sur iOS 9
Magoo

5
@ G.Abhisek Errrr, bien sûr .... qu'est-ce que vous avez besoin d'expliquer? La première ligne reloadDataactualise le collectionViewpour qu'il s'appuie à nouveau sur ses méthodes de source de données ... le performBatchUpdatesa un bloc de complétion attaché donc si les deux sont exécutés sur le thread principal, vous savez que tout code mis à la place de /// collection-view finished reloads'exécutera avec un nouveau et présentécollectionView
Magoo

8
Cela ne fonctionnait pas pour moi lorsque la collectionView avait des cellules de taille dynamique
ingaham

2
Cette méthode est la solution parfaite sans aucun mal de tête.
arjavlad

1
@ingaham Cela fonctionne parfaitement pour moi avec des cellules de taille dynamique, Swift 4.2 IOS 12
Romulo BM

27

C'est en fait plutôt très simple.

Lorsque vous appelez par exemple la méthode reloadData de UICollectionView ou la méthode invalidateLayout de la disposition, vous procédez comme suit:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
});

dispatch_async(dispatch_get_main_queue(), ^{
    //your stuff happens here
    //after the reloadData/invalidateLayout finishes executing
});

Pourquoi cela fonctionne:

Le thread principal (où nous devons faire toutes les mises à jour de l'interface utilisateur) héberge la file d'attente principale, qui est de nature série, c'est-à-dire qu'elle fonctionne à la manière FIFO. Ainsi, dans l'exemple ci-dessus, le premier bloc est appelé, ce qui fait que notre reloadDataméthode est invoquée, suivi de tout autre élément dans le deuxième bloc.

Maintenant, le thread principal se bloque également. Donc, si vous prenez reloadData3s pour s'exécuter, le traitement du deuxième bloc sera différé de ces 3s.


2
En fait, je viens de tester cela, le bloc est appelé avant toutes mes autres méthodes de délégué. Alors ça ne marche pas?
taylorcressy

@taylorcressy hé, j'ai mis à jour le code, quelque chose que j'utilise maintenant dans 2 applications de production. Compris également l'explication.
dezinezync

Ça ne marche pas pour moi. Lorsque j'appelle reloadData, la collectionView demande des cellules après l'exécution du deuxième bloc. J'ai donc le même problème que @taylorcressy semble avoir.
Raphael le

1
Pouvez-vous essayer de placer le deuxième appel de blocage dans le premier? Non vérifié, mais pourrait fonctionner.
dezinezync

6
reloadDatamet maintenant en file d'attente le chargement des cellules sur le thread principal (même si elles sont appelées depuis le thread principal). Ainsi, les cellules ne sont pas réellement chargées ( cellForRowAtIndexPath:ne sont pas appelées) après les reloadDataretours. Le code d'habillage que vous voulez exécuter après le chargement de vos cellules dispatch_async(dispatch_get_main...et l'appel après votre appel à reloadDataobtiendra le résultat souhaité.
Tylerc230 le

12

Juste pour ajouter à une excellente réponse @dezinezync:

Swift 3+

collectionView.collectionViewLayout.invalidateLayout() // or reloadData()
DispatchQueue.main.async {
    // your stuff here executing after collectionView has been layouted
}

@nik car cela que nous devons ajouter dans la vue est apparu !!
SARATH SASI

1
Pas nécessairement, vous pouvez l'appeler d'où vous voulez faire quelque chose lorsque la mise en page est enfin chargée.
nikans

J'avais des scintillements de défilement en essayant de faire défiler ma collectionView à la fin après avoir inséré des éléments, cela l'a corrigé, merci.
Skoua

6

Fais-le comme ça:

       UIView.animateWithDuration(0.0, animations: { [weak self] in
                guard let strongSelf = self else { return }

                strongSelf.collectionView.reloadData()

            }, completion: { [weak self] (finished) in
                guard let strongSelf = self else { return }

                // Do whatever is needed, reload is finished here
                // e.g. scrollToItemAtIndexPath
                let newIndexPath = NSIndexPath(forItem: 1, inSection: 0)
                strongSelf.collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
        })

4

Essayez de forcer une mise en page synchrone via layoutIfNeeded () juste après l'appel reloadData (). Semble fonctionner à la fois pour UICollectionView et UITableView sur iOS 12.

collectionView.reloadData()
collectionView.layoutIfNeeded() 

// cellForItem/sizeForItem calls should be complete
completion?()

Merci mec! Je cherche la solution à ce problème depuis assez longtemps.
Trzy Gracje

3

Comme dezinezync a répondu, ce dont vous avez besoin est d'envoyer à la file d'attente principale un bloc de code après reloadDataun UITableViewou UICollectionView, puis ce bloc sera exécuté après le retrait des cellules.

Afin de rendre cela plus clair lors de l'utilisation, j'utiliserais une extension comme celle-ci:

extension UICollectionView {
    func reloadData(_ completion: @escaping () -> Void) {
        reloadData()
        DispatchQueue.main.async { completion() }
    }
}

Il peut également être mis en œuvre à un UITableViewaussi bien


3

Une approche différente en utilisant RxSwift / RxCocoa:

        collectionView.rx.observe(CGSize.self, "contentSize")
            .subscribe(onNext: { size in
                print(size as Any)
            })
            .disposed(by: disposeBag)

1
J'aime ça. performBatchUpdatesne fonctionnait pas dans mon cas, celui-ci le fait. À votre santé!
Zoltán

@ MichałZiobro, pouvez-vous mettre à jour votre code quelque part? La méthode ne doit être appelée que lorsque contentSize a été modifié? Donc, à moins que vous ne le changiez à chaque parchemin, cela devrait fonctionner!
Chinh Nguyen

1

Cela fonctionne pour moi:

__weak typeof(self) wself= self;
[self.contentCollectionView performBatchUpdates:^{
    [wself.contentCollectionView reloadData];
} completion:^(BOOL finished) {
    [wself pageViewCurrentIndexDidChanged:self.contentCollectionView];
}];

2
absurdité ... cela n'a en aucun cas fini de se présenter.
Magoo

1

J'avais besoin d'une action à faire sur toutes les cellules visibles lorsque la vue de la collection est chargée avant qu'elle ne soit visible pour l'utilisateur, j'ai utilisé:

public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if shouldPerformBatch {
        self.collectionView.performBatchUpdates(nil) { completed in
            self.modifyVisibleCells()
        }
    }
}

Faites attention que cela sera appelé lors du défilement dans la vue de la collection, donc pour éviter cette surcharge, j'ai ajouté:

private var souldPerformAction: Bool = true

et dans l'action elle-même:

private func modifyVisibleCells() {
    if self.shouldPerformAction {
        // perform action
        ...
        ...
    }
    self.shouldPerformAction = false
}

L'action sera toujours exécutée plusieurs fois, comme le nombre de cellules visibles à l'état initial. mais sur tous ces appels, vous aurez le même nombre de cellules visibles (toutes). Et l'indicateur booléen l'empêchera de s'exécuter à nouveau une fois que l'utilisateur a commencé à interagir avec la vue de collection.


1

J'ai juste fait ce qui suit pour effectuer quoi que ce soit après le rechargement de la vue de collection. Vous pouvez utiliser ce code même dans la réponse API.

self.collectionView.reloadData()

DispatchQueue.main.async {
   // Do Task after collection view is reloaded                
}

1

La meilleure solution que j'ai trouvée jusqu'à présent est de l'utiliser CATransactionpour gérer l'achèvement.

Swift 5 :

CATransaction.begin()
CATransaction.setCompletionBlock {
    // UICollectionView is ready
}

collectionView.reloadData()

CATransaction.commit()

0

Def faites ceci:

//Subclass UICollectionView
class MyCollectionView: UICollectionView {

    //Store a completion block as a property
    var completion: (() -> Void)?

    //Make a custom funciton to reload data with a completion handle
    func reloadData(completion: @escaping() -> Void) {
        //Set the completion handle to the stored property
        self.completion = completion
        //Call super
        super.reloadData()
    }

    //Override layoutSubviews
    override func layoutSubviews() {
        //Call super
        super.layoutSubviews()
        //Call the completion
        self.completion?()
        //Set the completion to nil so it is reset and doesn't keep gettign called
        self.completion = nil
    }

}

Puis appelez comme ça dans votre VC

let collection = MyCollectionView()

self.collection.reloadData(completion: {

})

Assurez-vous que vous utilisez la sous-classe !!


0

Ce travail pour moi:


- (void)viewDidLoad {
    [super viewDidLoad];

    int ScrollToIndex = 4;

    [self.UICollectionView performBatchUpdates:^{}
                                    completion:^(BOOL finished) {
                                             NSIndexPath *indexPath = [NSIndexPath indexPathForItem:ScrollToIndex inSection:0];
                                             [self.UICollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
                                  }];

}


0

Rechargez simplement collectionView à l'intérieur des mises à jour par lots, puis vérifiez dans le bloc de complétion s'il est terminé ou non à l'aide du booléen "finish".

self.collectionView.performBatchUpdates({
        self.collectionView.reloadData()
    }) { (finish) in
        if finish{
            // Do your stuff here!
        }
    }

0

Voici la seule approche qui a fonctionné pour moi.

extension UICollectionView {
    func reloadData(_ completion: (() -> Void)? = nil) {
        reloadData()
        guard let completion = completion else { return }
        layoutIfNeeded()
        completion()
    }
}

-1

Voici comment j'ai résolu le problème avec Swift 3.0:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    if !self.collectionView.visibleCells.isEmpty {
        // stuff
    }
}

Cela ne fonctionnera pas. VisibleCells aura du contenu dès que la première cellule est chargée ... le problème est que nous voulons savoir quand la dernière cellule a été chargée.
Travis M.

-9

Essaye ça:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return _Items.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell;
    //Some cell stuff here...

    if(indexPath.row == _Items.count-1){
       //THIS IS THE LAST CELL, SO TABLE IS LOADED! DO STUFF!
    }

    return cell;
}

2
Ce n'est pas correct, cellFor ne sera appelé que si cette cellule va être visible, dans ce cas, le dernier élément visible ne sera pas aussi égal au nombre total d'éléments ...
Jirune

-10

Vous pouvez faire comme ça ...

  - (void)reloadMyCollectionView{

       [myCollectionView reload];
       [self performSelector:@selector(myStuff) withObject:nil afterDelay:0.0];

   }

  - (void)myStuff{
     // Do your stuff here. This will method will get called once your collection view get loaded.

    }

Bien que ce code puisse répondre à la question, fournir un contexte supplémentaire concernant la raison et / ou la manière dont il répond à la question améliorerait considérablement sa valeur à long terme. Veuillez modifier votre réponse pour ajouter des explications.
Toby Speight le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.