@@ -200,11 +200,12 @@ extension ChatMessageCollectionAdapter: ChatDataSourceDelegateProtocol {
200
200
let performInBackground = updateType != . firstLoad
201
201
202
202
self . isLoadingContents = true
203
- let performBatchUpdates : ( CollectionChanges , @escaping ( ) -> Void ) -> Void = { [ weak self] changes, updateModelClosure in
203
+ let performBatchUpdates : ( CollectionChanges , @escaping ( ) -> Void , Bool ) -> Void = { [ weak self] changes, updateModelClosure, areCollectionChangesConsistent in
204
204
self ? . performBatchUpdates (
205
205
updateModelClosure: updateModelClosure,
206
206
changes: changes,
207
- updateType: updateType
207
+ updateType: updateType,
208
+ areCollectionChangesConsistent: areCollectionChangesConsistent
208
209
) {
209
210
self ? . isLoadingContents = false
210
211
completion ( )
@@ -224,13 +225,13 @@ extension ChatMessageCollectionAdapter: ChatDataSourceDelegateProtocol {
224
225
guard let modelUpdate = createModelUpdate ( ) else { return }
225
226
226
227
DispatchQueue . main. async {
227
- performBatchUpdates ( modelUpdate. changes, modelUpdate. updateModelClosure)
228
+ performBatchUpdates ( modelUpdate. changes, modelUpdate. updateModelClosure, modelUpdate . areChangesConsistent )
228
229
}
229
230
}
230
231
} else {
231
232
guard let modelUpdate = createModelUpdate ( ) else { return }
232
233
233
- performBatchUpdates ( modelUpdate. changes, modelUpdate. updateModelClosure)
234
+ performBatchUpdates ( modelUpdate. changes, modelUpdate. updateModelClosure, modelUpdate . areChangesConsistent )
234
235
}
235
236
}
236
237
@@ -263,7 +264,7 @@ extension ChatMessageCollectionAdapter: ChatDataSourceDelegateProtocol {
263
264
}
264
265
}
265
266
266
- private func createModelUpdates( newItems: [ ChatItemProtocol ] , oldItems: ChatItemCompanionCollection , collectionViewWidth: CGFloat ) -> ( changes: CollectionChanges , updateModelClosure: ( ) -> Void ) {
267
+ private func createModelUpdates( newItems: [ ChatItemProtocol ] , oldItems: ChatItemCompanionCollection , collectionViewWidth: CGFloat ) -> ( changes: CollectionChanges , updateModelClosure: ( ) -> Void , areChangesConsistent : Bool ) {
267
268
let newDecoratedItems = self . chatItemsDecorator. decorateItems ( newItems)
268
269
let changes = generateChanges (
269
270
oldCollection: oldItems. map ( HashableItem . init) ,
@@ -275,7 +276,26 @@ extension ChatMessageCollectionAdapter: ChatDataSourceDelegateProtocol {
275
276
self ? . layoutModel = layoutModel
276
277
self ? . chatItemCompanionCollection = itemCompanionCollection
277
278
}
278
- return ( changes, updateModelClosure)
279
+ let areCollectionChangesConsistent = Self . validateCollectionChangeModel (
280
+ changes,
281
+ oldItems: oldItems,
282
+ newItems: newDecoratedItems
283
+ )
284
+ return ( changes, updateModelClosure, areCollectionChangesConsistent)
285
+ }
286
+
287
+ private static func validateCollectionChangeModel(
288
+ _ collection: CollectionChanges ,
289
+ oldItems: ChatItemCompanionCollection ,
290
+ newItems: [ DecoratedChatItem ]
291
+ ) -> Bool {
292
+ let deletionChangesCount = collection. deletedIndexPaths. count
293
+ let insertionChangesCount = collection. insertedIndexPaths. count
294
+
295
+ let oldItemsCount = oldItems. count
296
+ let newItemsCount = newItems. count
297
+
298
+ return ( newItemsCount - oldItemsCount) == ( insertionChangesCount - deletionChangesCount)
279
299
}
280
300
281
301
// Returns scrolling position in interval [0, 1], 0 top, 1 bottom
@@ -370,6 +390,7 @@ extension ChatMessageCollectionAdapter: ChatDataSourceDelegateProtocol {
370
390
private func performBatchUpdates( updateModelClosure: @escaping ( ) -> Void , // swiftlint:disable:this cyclomatic_complexity
371
391
changes: CollectionChanges ,
372
392
updateType: UpdateType ,
393
+ areCollectionChangesConsistent: Bool ,
373
394
completion: @escaping ( ) -> Void ) {
374
395
guard let collectionView = self . collectionView else {
375
396
completion ( )
@@ -385,7 +406,7 @@ extension ChatMessageCollectionAdapter: ChatDataSourceDelegateProtocol {
385
406
// a) It's unsafe to perform reloadData while there's a performBatchUpdates animating: https://github.com/diegosanchezr/UICollectionViewStressing/tree/master/GhostCells
386
407
// Note: using reloadSections instead reloadData is safe and might not need a delay. However, using always reloadSections causes flickering on pagination and a crash on the first layout that needs a workaround. Let's stick to reloaData for now
387
408
// b) If it's a performBatchUpdates but visible cells are invalid let's wait until all finish (otherwise we would give wrong cells to presenters in updateVisibleCells)
388
- let mustDelayUpdate = hasUnfinishedBatchUpdates && ( wantsReloadData || !visibleCellsAreValid)
409
+ let mustDelayUpdate = hasUnfinishedBatchUpdates && ( !areCollectionChangesConsistent || wantsReloadData || !visibleCellsAreValid)
389
410
guard !mustDelayUpdate else {
390
411
// For reference, it is possible to force the current performBatchUpdates to finish in the next run loop, by cancelling animations:
391
412
// self.collectionView.subviews.forEach { $0.layer.removeAllAnimations() }
@@ -395,14 +416,19 @@ extension ChatMessageCollectionAdapter: ChatDataSourceDelegateProtocol {
395
416
updateModelClosure: updateModelClosure,
396
417
changes: changes,
397
418
updateType: updateType,
419
+ areCollectionChangesConsistent: areCollectionChangesConsistent,
398
420
completion: completion
399
421
)
400
422
}
401
423
return
402
424
}
403
425
// ... if they are still invalid the only thing we can do is a reloadData
404
426
let mustDoReloadData = !visibleCellsAreValid // Only way to recover from this inconsistent state
405
- usesBatchUpdates = !wantsReloadData && !mustDoReloadData
427
+ usesBatchUpdates = !wantsReloadData && !mustDoReloadData && areCollectionChangesConsistent
428
+
429
+ if !wantsReloadData && !mustDoReloadData && !areCollectionChangesConsistent {
430
+ assertionFailure ( " Collection changes are invalid. " )
431
+ }
406
432
}
407
433
408
434
let scrollAction : ScrollAction
0 commit comments