Las celdas UICollectionView solo se muestran después de la segunda llamada para reloadData

Probablemente estoy haciendo algo increíblemente estúpido, pero no puedo entender por mi vida lo que está sucediendo …

Lo que trato de hacer es bastante simple. Tengo un UICollectionView dependiente de los datos que estoy cargando desde el server. Después de get y configurar los datos del server, llamo a reloadData() en la vista de colección. Cuando lo hago, se llama a numberOfItemsInSection con el recuento correcto, pero cellForItemAt nunca recibe llamadas a less que llame a reloadData() por segunda vez (ni siquiera necesita un retraso).

Aquí está la idea general (aunque me doy count de que probablemente esté relacionada con alguna raza condicional en otro lado, pero tal vez esto sea suficiente para comenzar la conversación):

 class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { @IBOutlet weak var collectionView: UICollectionView! var data = [DataModel]() override func viewDidLoad() { collectionView.delegate = self collectionView.dataSource = self collectionView.register(UINib(nibName: "MyCell", bundle: nil), forCellWithReuseIdentifier: "MyCell") getData() } func getData() { someAsyncMethodToGetData() { (data) in // Using CloudKit so I think this is necessary, but tried it with and without DispatchQueue.main.async { self.data = data self.collectionView.reloadData() // This is the only way to get cells to render and for cellForItemAt to be called after data is loaded self.collectionView.reloadData() } } // Gets called initially AND after reloadData is called in the data completion closure. First time is 0, next time is number of DataModel objects func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return data.count } // Only gets called initially (unless I call reloadData() twice) func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = feedCollectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as? MyCell else { return UICollectionViewCell() } // Configure the cell with data... return cell } } 

Entonces, aquí hay algunas cosas que ya he intentado:

  • Hecho doble que reloadData() se llama al hilo principal (se agregó un punto de corte simbólico y se llamó Thread.isMainThread para verificar)
  • Movió reloadData() en una acción de button para eliminar la posibilidad de cualquier problema de subprocess: aún tenía que hacer clic dos veces para que las células aparecieran
  • Ponga las actualizaciones en performBatchUpdates en function de otras soluciones de SO que encontré
  • Se agregó setNeedsLayout y setNeedsDisplay (no estoy seguro de por qué esto sería necesario en este caso, pero lo intenté por setNeedsDisplay desesperación.

¿Alguna sugerencia? 🙏

Actualización 1

Ok, creo que encontré el problema, pero me gustaría entender la mejor manera de resolverlo. Aparece llamando a reloadData , la vista de la colección solo volverá a cargar las celdas ya visibles. getData() un poco el código anterior para devolver 1 celda antes de getData() . Cuando getData() luego llama a reloadData() esa 1 celda se actualiza (pero si hay 10 elementos en la matriz de datos, todavía solo mostrará [re] la 1 celda). Supongo que esto tiene sentido, ya que reloadData() solo recarga las celdas visibles, pero esto tiene que ser un problema común cuando la longitud de la matriz cambia y aún hay espacio en la pantalla para mostrar. Además, ¿por qué la llamada reloadData() resuelve dos veces el problema y muestra celdas que nunca fueron visibles? 🤔

Aquí hay una versión aún más simple del código que tengo arriba, eliminando toda la idea de "get datos de un server" y reemplazándolos con un simple retraso.

La cosa es UICollectionViewFlowLayout no se invalida correctamente después del número de artículos cambiado. Entonces, collectionView.contentSize todavía es antiguo. Necesitas hacer algo como

 self.collectionView.reloadData() self.collectionView.collectionViewLayout.invalidateLayout() 

Esto le dirá al layout que vuelva a calcular para la nueva fuente de datos.