Como posso saber se o UICollectionView foi carregado completamente?

98

Eu tenho que fazer alguma operação sempre que UICollectionView foi carregado completamente, ou seja, naquele momento, todos os métodos de fonte de dados / layout de UICollectionView devem ser chamados. Como eu sei disso ?? Existe algum método delegado para saber o status carregado de UICollectionView?

Jirune
fonte

Respostas:

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];
}
Cullen SUN
fonte
1
isso trava para mim quando navego de volta, caso contrário, funciona bem.
Ericosg
4
@ericosg adicionar [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];ao -viewWillDisappear:animated:bem.
pxpgraphics de
Brilhante! Muito obrigado!
Abdo de
41
O tamanho do conteúdo também muda ao rolar, por isso não é confiável.
Ryan Heitner
Eu tentei para uma exibição de coleção que obtém seus dados usando um método de descanso assíncrono. Isso significa que o Visible Cells está sempre vazio quando esse método é acionado, o que significa que rolar para um item na coleção no início.
Chucky
155

Isso funcionou para mim:

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

Sintaxe do Swift 4:

self.collectionView.reloadData()
self.collectionView.performBatchUpdates(nil, completion: {
    (result) in
    // ready
})
myeyesareblind
fonte
1
Trabalhou para mim no iOS 9
Magoo
5
@ G.Abhisek Errrr, claro .... o que você precisa explicar? A primeira linha reloadDataatualiza o collectionViewpara que ele se baseie nos métodos da fonte de dados novamente ... o performBatchUpdatestem um bloco de conclusão anexado, então se ambos forem executados no encadeamento principal você conhece qualquer código que colocar no lugar de /// collection-view finished reloadserá executado com um novo e organizadocollectionView
Magoo
8
Não funcionou para mim quando o collectionView tinha células de tamanho dinâmico
ingaham
2
Este método é a solução perfeita sem qualquer dor de cabeça.
arjavlad
1
@ingaham Isso funciona perfeitamente para mim com células de tamanho dinâmico, Swift 4.2 IOS 12
Romulo BM
27

Na verdade, é muito simples.

Quando você, por exemplo, chama o método reloadData do UICollectionView ou o método invalidateLayout do layout, você faz o seguinte:

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
});

Por que isso funciona:

O thread principal (que é onde devemos fazer todas as atualizações da IU) hospeda a fila principal, que é serial por natureza, ou seja, funciona no estilo FIFO. Portanto, no exemplo acima, o primeiro bloco é chamado, o qual tem nosso reloadDatamétodo sendo chamado, seguido por qualquer outra coisa no segundo bloco.

Agora, o thread principal também está bloqueando. Então se você éreloadData leva 3s para executar, o processamento do segundo bloco será adiado por esses 3s.

dezinezync
fonte
2
Na verdade, acabei de testar isso. O bloco está sendo chamado antes de todos os meus outros métodos de delegado. Então não funciona?
taylorcressy
@taylorcressy ei, eu atualizei o código, algo que estou usando agora em 2 aplicativos de produção. Também inclui a explicação.
dezinezync
Não funciona para mim. Quando chamo reloadData, o collectionView solicita células após a execução do segundo bloco. Estou tendo o mesmo problema que @taylorcressy parece ter.
Raphael
1
Você pode tentar colocar a segunda chamada em bloco dentro da primeira? Não verificado, mas pode funcionar.
dezinezync
6
reloadDataagora enfileira o carregamento das células no encadeamento principal (mesmo se chamado do encadeamento principal). Portanto, as células não são realmente carregadas ( cellForRowAtIndexPath:não são chamadas) após os reloadDataretornos. Encapsular o código que você deseja executar depois que suas células forem carregadas dispatch_async(dispatch_get_main...e chamá-lo após sua chamada para reloadDataobterá o resultado desejado.
Tylerc230
12

Apenas para adicionar uma ótima resposta @dezinezync:

Swift 3+

collectionView.collectionViewLayout.invalidateLayout() // or reloadData()
DispatchQueue.main.async {
    // your stuff here executing after collectionView has been layouted
}
nikans
fonte
@nikas isso nós temos que adicionar em vista apareceu !!
SARATH SASI
1
Não necessariamente, você pode chamá-lo de onde quiser fazer algo quando o layout for finalmente carregado.
nikans
Eu estava recebendo cintilação de rolagem tentando fazer minha coleçãoView rolar no final após inserir itens, isso corrigiu, obrigado.
Skoua
6

Faça isso deste modo:

       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)
        })
Darko
fonte
4

Tente forçar uma passagem de layout síncrona por layoutIfNeeded () logo após a chamada reloadData (). Parece funcionar para UICollectionView e UITableView no iOS 12.

collectionView.reloadData()
collectionView.layoutIfNeeded() 

// cellForItem/sizeForItem calls should be complete
completion?()
Tyler
fonte
Obrigado cara! Há muito tempo que procuro a solução para esse problema.
Trzy Gracje
3

Como dezinezync respondeu, o que você precisa é enviar para a fila principal um bloco de código depois reloadDatade um UITableViewouUICollectionView , e então este bloco será executado após o desenfileiramento das células

Para deixar isso mais direto ao usar, eu usaria uma extensão como esta:

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

Ele pode também ser implementada a um UITableViewbem

Matheus Rocco
fonte
3

Uma abordagem diferente usando RxSwift / RxCocoa:

        collectionView.rx.observe(CGSize.self, "contentSize")
            .subscribe(onNext: { size in
                print(size as Any)
            })
            .disposed(by: disposeBag)
Chinh Nguyen
fonte
1
Estou gostando disso performBatchUpdatesnão estava funcionando no meu caso, este funciona. Felicidades!
Zoltán
@ MichałZiobro, você pode atualizar seu código em algum lugar? O método deve ser chamado apenas quando contentSize foi alterado? Portanto, a menos que você altere em cada pergaminho, isso deve funcionar!
Chinh Nguyen
1

Isso funciona para mim:

__weak typeof(self) wself= self;
[self.contentCollectionView performBatchUpdates:^{
    [wself.contentCollectionView reloadData];
} completion:^(BOOL finished) {
    [wself pageViewCurrentIndexDidChanged:self.contentCollectionView];
}];
jAckOdE
fonte
2
absurdo ... isso não terminou de forma alguma.
Magoo
1

Eu precisava que alguma ação fosse realizada em todas as células visíveis quando a visualização da coleção fosse carregada antes de ser visível para o usuário, usei:

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

Preste atenção que isso será chamado ao rolar pela visualização da coleção, então, para evitar essa sobrecarga, adicionei:

private var souldPerformAction: Bool = true

e na própria ação:

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

A ação ainda será realizada várias vezes, conforme o número de células visíveis no estado inicial. mas em todas essas chamadas, você terá o mesmo número de células visíveis (todas elas). E o sinalizador booleano impedirá que ele seja executado novamente depois que o usuário começar a interagir com a visualização da coleção.

gutte
fonte
1

Eu apenas fiz o seguinte para realizar qualquer coisa após o recarregamento da visualização da coleção. Você pode usar esse código até mesmo na resposta da API.

self.collectionView.reloadData()

DispatchQueue.main.async {
   // Do Task after collection view is reloaded                
}
Asad Jamil
fonte
1

A melhor solução que encontrei até agora é usar CATransactionpara lidar com a conclusão.

Swift 5 :

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

collectionView.reloadData()

CATransaction.commit()
MaxMedvedev
fonte
0

Def fazer isso:

//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
    }

}

Então chame assim dentro do seu VC

let collection = MyCollectionView()

self.collection.reloadData(completion: {

})

Certifique-se de usar a subclasse !!

Jon Vogel
fonte
0

Este trabalho para mim:


- (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];
                                  }];

}

Chrisz
fonte
0

Simplesmente recarregue o collectionView dentro das atualizações em lote e então verifique no bloco de conclusão se ele foi concluído ou não com a ajuda do booleano "terminar".

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

Abaixo está a única abordagem que funcionou para mim.

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

Foi assim que resolvi o problema com o Swift 3.0:

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

    if !self.collectionView.visibleCells.isEmpty {
        // stuff
    }
}
Landonandrey
fonte
Isso não vai funcionar. VisibleCells terá conteúdo assim que a primeira célula for carregada ... o problema é que queremos saber quando a última célula foi carregada.
Travis M.
-9

Experimente isto:

- (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;
}
Rocker
fonte
2
Não está correto, cellFor será chamado apenas se essa célula for ficar visível, nesse caso o último item visível não será igual ao número total de itens ...
Jirune
-10

Você pode fazer assim ...

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

    }
Swapnil
fonte
Embora este código possa responder à pergunta, fornecer contexto adicional sobre por que e / ou como ele responde à pergunta aumentaria significativamente seu valor a longo prazo. Por favor edite sua resposta para adicionar alguma explicação.
Toby Speight