Nightmare con crash de performBatchUpdates

Estoy frente a una pesadilla de un locking durante performBatchUpdates en una vista de colección.

El problema es básicamente esto: tengo un montón de imágenes en un directory en un server. Quiero mostrar las miniaturas de esos files en una vista de colección. Pero la miniatura debe downloadse del server de forma asíncrona. A medida que llegan, se insertán en la vista de la colección usando algo como esto:

dispatch_async(dispatch_get_main_queue(), ^{ [self.collectionView performBatchUpdates:^{ if (removedIndexes && [removedIndexes count] > 0) { [self.collectionView deleteItemsAtIndexPaths:removedIndexes]; } if (changedIndexes && [changedIndexes count] > 0) { [self.collectionView reloadItemsAtIndexPaths:changedIndexes]; } if (insertedIndexes && [insertedIndexes count] > 0) { [self.collectionView insertItemsAtIndexPaths:insertedIndexes]; } } completion:nil]; }); 

El problema es este (creo). Supongamos que en el momento = 0, la vista de colección tiene 10 ítems. Luego agrego 100 files más al server. La aplicación ve los nuevos files y comienza a download las miniaturas. A medida que se descargan las miniaturas, se insertán en la vista de la colección. Pero debido a que las descargas pueden tomar diferentes momentos y esta operación de descarga es asíncrona, en un momento iOS perderá la count de cuántos elementos tiene la colección y todo se bloqueará con este post infame catastrófico.

*** Finalización de la aplicación debido a la exception no detectada 'NSInternalInconsistencyException', motivo: 'Actualización no válida: número no válido de elementos en la sección 0. El número de elementos contenidos en una sección existente después de la actualización (213) debe ser igual al número de elementos contenida en esa sección antes de la actualización (154), más o less el número de elementos insertados o eliminados de esa sección (40 insertados, 0 eliminados) y más o less la cantidad de elementos movidos dentro o fuera de esa sección (0 movidos en , 0 se mudó). '

La testing de que tengo algo sospechoso es que si imprimo el recuento de elementos en el set de datos, veo exactamente 213. Entonces, el set de datos coincide con el número correcto y el post no tiene sentido.

He tenido este problema antes, aquí , pero ese fue un proyecto iOS 7. De alguna manera, el problema volvió ahora en iOS 8 y las soluciones allí no funcionan y ahora el set de datos ES EN SYNC .

Creo que el problema es causado por los índices.

Llave:

  • Para los elementos actualizados y eliminados, los índices tienen que ser los índices de los artículos originales.
  • Para los elementos insertados, los índices deben ser los índices de los elementos finales.

Aquí hay un código de demostración con comentarios:

 class CollectionViewController: UICollectionViewController { var items: [String]! let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"] let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"] override func viewDidLoad() { super.viewDidLoad() self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:))) items = before } func onRefresh(_: AnyObject) { items = after collectionView?.performBatchUpdates({ self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ]) // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path // self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ]) // NOTE: Have to be the indexes of original list self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ]) // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update' // self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ]) // NOTE: Have to be index of final list self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ]) }, completion: nil) } override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { return 1 } override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return items.count } override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) let label = cell.viewWithTag(100) as! UILabel label.text = items[indexPath.row] return cell } } 

Parece que necesitas hacer un poco de trabajo adicional con el procesamiento por lotes, que imágenes han aparecido para cada 'grupo' de animation. Desde el event handling lockings como este antes, la forma en que funciona 'performBatchUpdates' es

  1. Antes de invocar su bloque, comtesting por duplicado todos los recuentos de artículos y los guarda al llamar a 'numberOfItemsInSection' (este es el 154 en su post de error).
  2. Ejecuta el bloque, rastrea las inserciones / eliminaciones y calcula la cantidad final de elementos que debe basarse en las inserciones y eliminaciones.
  3. Después de ejecutar el bloque, verifica doblemente los recuentos que calculó a los recuentos reales cuando le pregunta a su dataSource 'numberOfItemsInSection' (este es el número 213). Si no coincide, se bloqueará.

Según las variables 'insertedIndexes' y 'changedIndexes', está calculando previamente qué cosas deben aparecer en function de la respuesta de descarga del server y luego ejecutando el lote. Sin embargo, supongo que su método 'numberOfItemsInSection' siempre está devolviendo el conteo 'verdadero' de elementos.

Entonces, si una descarga se completa durante el paso 2, cuando realiza la comprobación de sanidad en '3', sus numbers ya no se alinearán.

Solución más fácil: Espere hasta que todos los files se hayan descargado, luego haga un único 'batchUpdates'. Probablemente no sea la mejor experiencia de usuario, pero evita este problema.

Solución más difícil: realice los lotes según sea necesario y haga un seguimiento de los elementos que ya se han mostrado / están actualmente animando por separado del número total de elementos. Algo como:

 BOOL _performingAnimation; NSInteger _finalItemCount; - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return _finalItemCount; } - (void)somethingDidFinishDownloading { if (_performingAnimation) { return; } // Calculate changes. dispatch_async(dispatch_get_main_queue(), ^{ _performingAnimation = YES; [self.collectionView performBatchUpdates:^{ if (removedIndexes && [removedIndexes count] > 0) { [self.collectionView deleteItemsAtIndexPaths:removedIndexes]; } if (changedIndexes && [changedIndexes count] > 0) { [self.collectionView reloadItemsAtIndexPaths:changedIndexes]; } if (insertedIndexes && [insertedIndexes count] > 0) { [self.collectionView insertItemsAtIndexPaths:insertedIndexes]; } _finalItemCount += (insertedIndexes.count - removedIndexes.count); } completion:^{ _performingAnimation = NO; }]; }); } 

Lo único que hay que resolver después de eso sería asegurarse de ejecutar una comprobación final de los elementos sobrantes si el último elemento para download finalizó durante una animation (tal vez tenga un método 'performFinalAnimationIfNeeded' que ejecuta en el bloque de finalización)