¿Cómo sé que UICollectionView se ha cargado por completo?

98

Tengo que hacer alguna operación cada vez que UICollectionView se ha cargado por completo, es decir, en ese momento se deben llamar todos los métodos de diseño / fuente de datos de UICollectionView. ¿¿Cómo sé eso?? ¿Existe algún método delegado para conocer el estado de carga de UICollectionView?

Jirune
fuente

Respuestas:

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 SOL
fuente
1
esto me falla cuando navego hacia atrás, de lo contrario funciona bien.
ericosg
4
@ericosg añadir [self.collectionView removeObserver:self forKeyPath:@"contentSize" context:NULL];a -viewWillDisappear:animated:también.
pxpgraphics
¡Brillante! ¡Muchas gracias!
Abdo
41
El tamaño del contenido también cambia cuando se desplaza, por lo que no es confiable.
Ryan Heitner
He intentado obtener una vista de colección que obtiene sus datos utilizando un método de descanso asíncrono. Esto significa que las celdas visibles siempre están vacías cuando se activa este método, lo que significa que me desplazo a un elemento de la colección al principio.
Chucky
155

Esto funcionó para mí:

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

Sintaxis de Swift 4:

self.collectionView.reloadData()
self.collectionView.performBatchUpdates(nil, completion: {
    (result) in
    // ready
})
Mis ojos son ciegos
fuente
1
Funcionó para mí en iOS 9
Magoo
5
@ G.Abhisek Errrr, claro ... ¿qué necesitas explicar? La primera línea reloadDataactualiza el, collectionViewpor lo que se basa en sus métodos de origen de datos nuevamente ... performBatchUpdatestiene un bloque de finalización adjunto, por lo que si ambos se realizan en el hilo principal, sabrá que cualquier código que se coloque en lugar de /// collection-view finished reloadse ejecutará con un nuevo y diseñadocollectionView
Magoo
8
No funcionó para mí cuando el collectionView tenía celdas de tamaño dinámico
ingaham
2
Este método es la solución perfecta sin ningún dolor de cabeza.
arjavlad
1
@ingaham Esto funciona perfectamente para mí con celdas de tamaño dinámico, Swift 4.2 IOS 12
Romulo BM
27

De hecho, es bastante simple.

Cuando, por ejemplo, llama al método reloadData de UICollectionView o al método invalidateLayout de su diseño, hace lo siguiente:

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 qué esto funciona:

El hilo principal (que es donde deberíamos hacer todas las actualizaciones de la interfaz de usuario) alberga la cola principal, que es de naturaleza serial, es decir, funciona en formato FIFO. Entonces, en el ejemplo anterior, se llama al primer bloque, que tiene nuestro reloadDatamétodo invocado, seguido de cualquier otra cosa en el segundo bloque.

Ahora el hilo principal también se bloquea. Entonces, si reloadDatatarda 3 segundos en ejecutarse, el procesamiento del segundo bloque será aplazado por esos 3 segundos.

dezinezync
fuente
2
De hecho, acabo de probar esto, el bloque está siendo llamado antes que todos mis otros métodos delegados. ¿Entonces no funciona?
taylorcressy
@taylorcressy hey, he actualizado el código, algo que estoy usando ahora en 2 aplicaciones de producción. También se incluye la explicación.
dezinezync
No funciona para mi. Cuando llamo a reloadData, collectionView solicita celdas después de la ejecución del segundo bloque. Así que estoy teniendo el mismo problema que parece tener @taylorcressy.
Raphael
1
¿Puedes intentar poner la segunda llamada de bloque dentro de la primera? Sin verificar, pero podría funcionar.
dezinezync
6
reloadDataahora pone en cola la carga de las celdas en el hilo principal (incluso si se llama desde el hilo principal). Entonces, las celdas no se cargan ( cellForRowAtIndexPath:no se llaman) después de las reloadDatadevoluciones. Envolver el código que desea ejecutar después de que se carguen sus celdas dispatch_async(dispatch_get_main...y llamarlo después de su llamada a reloadDatalogrará el resultado deseado.
Tylerc230
12

Solo para agregar a una excelente respuesta de @dezinezync:

Swift 3+

collectionView.collectionViewLayout.invalidateLayout() // or reloadData()
DispatchQueue.main.async {
    // your stuff here executing after collectionView has been layouted
}
nikans
fuente
@nikas esto que tenemos que agregar a la vista apareció !!
SARATH SASI
1
No necesariamente, puede llamarlo desde donde quiera hacer algo cuando finalmente se cargue el diseño.
Nikans
Estaba recibiendo parpadeos de desplazamiento tratando de hacer que mi collectionView se desplazara al final después de insertar elementos, esto lo solucionó, gracias.
Skoua
6

Hazlo asi:

       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
fuente
4

Intente forzar un pase de diseño síncrono a través de layoutIfNeeded () justo después de la llamada reloadData (). Parece funcionar tanto para UICollectionView como para UITableView en iOS 12.

collectionView.reloadData()
collectionView.layoutIfNeeded() 

// cellForItem/sizeForItem calls should be complete
completion?()
Tyler
fuente
¡Gracias hombre! He estado buscando la solución a ese problema durante bastante tiempo.
Trzy Gracje
3

Como respondió dezinezync , lo que necesita es enviar a la cola principal un bloque de código después reloadDatade un UITableViewo UICollectionView, y luego este bloque se ejecutará después de que las celdas se retiren de la cola

Para hacer esto más directo al usarlo, usaría una extensión como esta:

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

Puede también ser implementado a una UITableView, así

Matheus Rocco
fuente
3

Un enfoque diferente usando RxSwift / RxCocoa:

        collectionView.rx.observe(CGSize.self, "contentSize")
            .subscribe(onNext: { size in
                print(size as Any)
            })
            .disposed(by: disposeBag)
Chinh Nguyen
fuente
1
Me gusta esto. performBatchUpdatesno estaba funcionando en mi caso, este lo hace. ¡Salud!
Zoltán
@ MichałZiobro, ¿puedes actualizar tu código en alguna parte? ¿Se debe llamar al método solo cuando se modificó contentSize? Entonces, a menos que lo cambie en cada pergamino, ¡esto debería funcionar!
Chinh Nguyen
1

Esto funciona para mi:

__weak typeof(self) wself= self;
[self.contentCollectionView performBatchUpdates:^{
    [wself.contentCollectionView reloadData];
} completion:^(BOOL finished) {
    [wself pageViewCurrentIndexDidChanged:self.contentCollectionView];
}];
jAckOdE
fuente
2
tonterías ... esto no ha terminado de presentarse de ninguna manera.
Magoo
1

Necesitaba realizar alguna acción en todas las celdas visibles cuando la vista de colección se carga antes de que sea visible para el usuario, usé:

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

Preste atención a que esto se llamará al desplazarse por la vista de colección, por lo que para evitar esta sobrecarga, agregué:

private var souldPerformAction: Bool = true

y en la acción misma:

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

La acción aún se realizará varias veces, como el número de celdas visibles en el estado inicial. pero en todas esas llamadas, tendrá la misma cantidad de celdas visibles (todas). Y la bandera booleana evitará que se ejecute nuevamente después de que el usuario comience a interactuar con la vista de colección.

destripar
fuente
1

Acabo de hacer lo siguiente para realizar cualquier cosa después de recargar la vista de colección. Puede utilizar este código incluso en la respuesta de la API.

self.collectionView.reloadData()

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

La mejor solución que he encontrado hasta ahora es usar CATransaction para manejar la finalización.

Rápido 5 :

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

collectionView.reloadData()

CATransaction.commit()
MaxMedvedev
fuente
0

Def haz esto:

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

}

Entonces llama así dentro de tu VC

let collection = MyCollectionView()

self.collection.reloadData(completion: {

})

¡Asegúrate de estar usando la subclase!

Jon Vogel
fuente
0

Este trabajo para mi:


- (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
fuente
0

Simplemente vuelva a cargar collectionView dentro de las actualizaciones por lotes y luego verifique en el bloque de finalización si está terminado o no con la ayuda del booleano "finish".

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

A continuación se muestra el único enfoque que funcionó para mí.

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

Así es como resolví el problema con Swift 3.0:

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

    if !self.collectionView.visibleCells.isEmpty {
        // stuff
    }
}
Landonandrey
fuente
Esto no funcionará. VisibleCells tendrá contenido tan pronto como se cargue la primera celda ... el problema es que queremos saber cuándo se cargó la última celda.
Travis M.
-9

Prueba esto:

- (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;
}
Balancín
fuente
2
No es correcto, solo se llamará a cellFor si esa celda va a ser visible, en ese caso, el último elemento visible no será igual al número total de elementos ...
Jirune
-10

Puedes hacer así ...

  - (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
fuente
Aunque este código puede responder a la pregunta, proporcionar un contexto adicional sobre por qué y / o cómo responde la pregunta mejoraría significativamente su valor a largo plazo. Por favor, editar su respuesta a añadir un poco de explicación.
Toby Speight