diff --git a/Source/IGListKit/IGListAdapterUpdater.m b/Source/IGListKit/IGListAdapterUpdater.m index 04aa3579..057d73ea 100644 --- a/Source/IGListKit/IGListAdapterUpdater.m +++ b/Source/IGListKit/IGListAdapterUpdater.m @@ -15,6 +15,7 @@ #import "IGListMoveIndexPathInternal.h" #import "IGListReloadIndexPath.h" #import "UICollectionView+IGListBatchUpdateData.h" +#import "IGListAdapterUpdaterHelpers.h" @implementation IGListAdapterUpdater @@ -197,11 +198,14 @@ // block executed in the first param block of -[UICollectionView performBatchUpdates:completion:] void (^batchUpdatesBlock)(IGListIndexSetResult *result) = ^(IGListIndexSetResult *result){ executeUpdateBlocks(); - - self.applyingUpdateData = [self _flushCollectionView:collectionView - withDiffResult:result - batchUpdates:self.batchUpdates - fromObjects:fromObjects]; + + self.applyingUpdateData = IGListApplyUpdatesToCollectionView(collectionView, + result, + self.batchUpdates, + fromObjects, + experiments, + self.movesAsDeletesInserts, + self.preferItemReloadsForSectionReloads); [self _cleanStateAfterUpdates]; [self _performBatchUpdatesItemBlockApplied]; @@ -273,114 +277,6 @@ willPerformBatchUpdatesWithCollectionView:collectionView } } -void convertReloadToDeleteInsert(NSMutableIndexSet *reloads, - NSMutableIndexSet *deletes, - NSMutableIndexSet *inserts, - IGListIndexSetResult *result, - NSArray> *fromObjects) { - // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts - const BOOL hasObjects = [fromObjects count] > 0; - [[reloads copy] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - // if a diff was not performed, there are no changes. instead use the same index that was originally queued - id diffIdentifier = hasObjects ? [fromObjects[idx] diffIdentifier] : nil; - const NSInteger from = hasObjects ? [result oldIndexForIdentifier:diffIdentifier] : idx; - const NSInteger to = hasObjects ? [result newIndexForIdentifier:diffIdentifier] : idx; - [reloads removeIndex:from]; - - // if a reload is queued outside the diff and the object was inserted or deleted it cannot be - if (from != NSNotFound && to != NSNotFound) { - [deletes addIndex:from]; - [inserts addIndex:to]; - } else { - IGAssert([result.deletes containsIndex:idx], - @"Reloaded section %lu was not found in deletes with from: %li, to: %li, deletes: %@, fromClass: %@", - (unsigned long)idx, (long)from, (long)to, deletes, [(id)fromObjects[idx] class]); - } - }]; -} - -static NSArray *convertSectionReloadToItemUpdates(NSIndexSet *sectionReloads, UICollectionView *collectionView) { - NSMutableArray *updates = [NSMutableArray new]; - [sectionReloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { - NSUInteger numberOfItems = [collectionView numberOfItemsInSection:sectionIndex]; - for (NSUInteger itemIndex = 0; itemIndex < numberOfItems; itemIndex++) { - [updates addObject:[NSIndexPath indexPathForItem:itemIndex inSection:sectionIndex]]; - } - }]; - return [updates copy]; -} - -- (IGListBatchUpdateData *)_flushCollectionView:(UICollectionView *)collectionView - withDiffResult:(IGListIndexSetResult *)diffResult - batchUpdates:(IGListBatchUpdates *)batchUpdates - fromObjects:(NSArray > *)fromObjects { - NSSet *moves = [[NSSet alloc] initWithArray:diffResult.moves]; - - // combine section reloads from the diff and manual reloads via reloadItems: - NSMutableIndexSet *reloads = [diffResult.updates mutableCopy]; - [reloads addIndexes:batchUpdates.sectionReloads]; - - NSMutableIndexSet *inserts = [diffResult.inserts mutableCopy]; - NSMutableIndexSet *deletes = [diffResult.deletes mutableCopy]; - NSMutableArray *itemUpdates = [NSMutableArray new]; - if (self.movesAsDeletesInserts) { - for (IGListMoveIndex *move in moves) { - [deletes addIndex:move.from]; - [inserts addIndex:move.to]; - } - // clear out all moves - moves = [NSSet new]; - } - - // Item reloads are not safe, if any section moves happened or there are inserts/deletes. - if (self.preferItemReloadsForSectionReloads - && moves.count == 0 && inserts.count == 0 && deletes.count == 0 && reloads.count > 0) { - [reloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { - NSMutableIndexSet *localIndexSet = [NSMutableIndexSet indexSetWithIndex:sectionIndex]; - if (sectionIndex < [collectionView numberOfSections] - && sectionIndex < [collectionView.dataSource numberOfSectionsInCollectionView:collectionView] - && [collectionView numberOfItemsInSection:sectionIndex] == [collectionView.dataSource collectionView:collectionView numberOfItemsInSection:sectionIndex]) { - // Perfer to do item reloads instead, if the number of items in section is unchanged. - [itemUpdates addObjectsFromArray:convertSectionReloadToItemUpdates(localIndexSet, collectionView)]; - } else { - // Otherwise, fallback to convert into delete+insert section operation. - convertReloadToDeleteInsert(localIndexSet, deletes, inserts, diffResult, fromObjects); - } - }]; - } else { - // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts - convertReloadToDeleteInsert(reloads, deletes, inserts, diffResult, fromObjects); - } - - NSMutableArray *itemInserts = batchUpdates.itemInserts; - NSMutableArray *itemDeletes = batchUpdates.itemDeletes; - NSMutableArray *itemMoves = batchUpdates.itemMoves; - - NSSet *uniqueDeletes = [NSSet setWithArray:itemDeletes]; - NSMutableSet *reloadDeletePaths = [NSMutableSet new]; - NSMutableSet *reloadInsertPaths = [NSMutableSet new]; - for (IGListReloadIndexPath *reload in batchUpdates.itemReloads) { - if (![uniqueDeletes containsObject:reload.fromIndexPath]) { - [reloadDeletePaths addObject:reload.fromIndexPath]; - [reloadInsertPaths addObject:reload.toIndexPath]; - } - } - [itemDeletes addObjectsFromArray:[reloadDeletePaths allObjects]]; - [itemInserts addObjectsFromArray:[reloadInsertPaths allObjects]]; - - const BOOL fixIndexPathImbalance = IGListExperimentEnabled(self.experiments, IGListExperimentFixIndexPathImbalance); - IGListBatchUpdateData *updateData = [[IGListBatchUpdateData alloc] initWithInsertSections:inserts - deleteSections:deletes - moveSections:moves - insertIndexPaths:itemInserts - deleteIndexPaths:itemDeletes - updateIndexPaths:itemUpdates - moveIndexPaths:itemMoves - fixIndexPathImbalance:fixIndexPathImbalance]; - [collectionView ig_applyBatchUpdateData:updateData]; - return updateData; -} - - (void)_beginPerformBatchUpdatesToObjects:(NSArray *)toObjects { self.pendingTransitionToObjects = toObjects; self.state = IGListBatchUpdateStateQueuedBatchUpdate; diff --git a/Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.h b/Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.h new file mode 100644 index 00000000..7ef56bb7 --- /dev/null +++ b/Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListBatchUpdateData; +@class IGListIndexSetResult; +@class IGListBatchUpdates; +@protocol IGListDiffable; + +NS_ASSUME_NONNULL_BEGIN + +extern void IGListConvertReloadToDeleteInsert(NSMutableIndexSet *reloads, + NSMutableIndexSet *deletes, + NSMutableIndexSet *inserts, + IGListIndexSetResult *result, + NSArray> *fromObjects); + +extern IGListBatchUpdateData *IGListApplyUpdatesToCollectionView(UICollectionView *collectionView, + IGListIndexSetResult *diffResult, + IGListBatchUpdates *batchUpdates, + NSArray> *fromObjects, + IGListExperiment experiments, + BOOL movesAsDeletesInserts, + BOOL preferItemReloadsForSectionReloads); + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.m b/Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.m new file mode 100644 index 00000000..5d0123ae --- /dev/null +++ b/Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.m @@ -0,0 +1,132 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapterUpdaterHelpers.h" + +#import + +#import +#import +#import +#import + +#import "IGListBatchUpdates.h" +#import "IGListReloadIndexPath.h" +#import "UICollectionView+IGListBatchUpdateData.h" + +void IGListConvertReloadToDeleteInsert(NSMutableIndexSet *reloads, + NSMutableIndexSet *deletes, + NSMutableIndexSet *inserts, + IGListIndexSetResult *result, + NSArray> *fromObjects) { + // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts + const BOOL hasObjects = [fromObjects count] > 0; + [[reloads copy] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + // if a diff was not performed, there are no changes. instead use the same index that was originally queued + id diffIdentifier = hasObjects ? [fromObjects[idx] diffIdentifier] : nil; + const NSInteger from = hasObjects ? [result oldIndexForIdentifier:diffIdentifier] : idx; + const NSInteger to = hasObjects ? [result newIndexForIdentifier:diffIdentifier] : idx; + [reloads removeIndex:from]; + + // if a reload is queued outside the diff and the object was inserted or deleted it cannot be + if (from != NSNotFound && to != NSNotFound) { + [deletes addIndex:from]; + [inserts addIndex:to]; + } else { + IGAssert([result.deletes containsIndex:idx], + @"Reloaded section %lu was not found in deletes with from: %li, to: %li, deletes: %@, fromClass: %@", + (unsigned long)idx, (long)from, (long)to, deletes, [(id)fromObjects[idx] class]); + } + }]; +} + +static NSArray *convertSectionReloadToItemUpdates(NSIndexSet *sectionReloads, UICollectionView *collectionView) { + NSMutableArray *updates = [NSMutableArray new]; + [sectionReloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { + NSUInteger numberOfItems = [collectionView numberOfItemsInSection:sectionIndex]; + for (NSUInteger itemIndex = 0; itemIndex < numberOfItems; itemIndex++) { + [updates addObject:[NSIndexPath indexPathForItem:itemIndex inSection:sectionIndex]]; + } + }]; + return [updates copy]; +} + +IGListBatchUpdateData *IGListApplyUpdatesToCollectionView(UICollectionView *collectionView, + IGListIndexSetResult *diffResult, + IGListBatchUpdates *batchUpdates, + NSArray> *fromObjects, + IGListExperiment experiments, + BOOL movesAsDeletesInserts, + BOOL preferItemReloadsForSectionReloads) { + NSSet *moves = [[NSSet alloc] initWithArray:diffResult.moves]; + + // combine section reloads from the diff and manual reloads via reloadItems: + NSMutableIndexSet *reloads = [diffResult.updates mutableCopy]; + [reloads addIndexes:batchUpdates.sectionReloads]; + + NSMutableIndexSet *inserts = [diffResult.inserts mutableCopy]; + NSMutableIndexSet *deletes = [diffResult.deletes mutableCopy]; + NSMutableArray *itemUpdates = [NSMutableArray new]; + if (movesAsDeletesInserts) { + for (IGListMoveIndex *move in moves) { + [deletes addIndex:move.from]; + [inserts addIndex:move.to]; + } + // clear out all moves + moves = [NSSet new]; + } + + // Item reloads are not safe, if any section moves happened or there are inserts/deletes. + if (preferItemReloadsForSectionReloads + && moves.count == 0 && inserts.count == 0 && deletes.count == 0 && reloads.count > 0) { + [reloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { + NSMutableIndexSet *localIndexSet = [NSMutableIndexSet indexSetWithIndex:sectionIndex]; + if (sectionIndex < [collectionView numberOfSections] + && sectionIndex < [collectionView.dataSource numberOfSectionsInCollectionView:collectionView] + && [collectionView numberOfItemsInSection:sectionIndex] == [collectionView.dataSource collectionView:collectionView numberOfItemsInSection:sectionIndex]) { + // Perfer to do item reloads instead, if the number of items in section is unchanged. + [itemUpdates addObjectsFromArray:convertSectionReloadToItemUpdates(localIndexSet, collectionView)]; + } else { + // Otherwise, fallback to convert into delete+insert section operation. + IGListConvertReloadToDeleteInsert(localIndexSet, deletes, inserts, diffResult, fromObjects); + } + }]; + } else { + // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts + IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, diffResult, fromObjects); + } + + NSMutableArray *itemInserts = batchUpdates.itemInserts; + NSMutableArray *itemDeletes = batchUpdates.itemDeletes; + NSMutableArray *itemMoves = batchUpdates.itemMoves; + + NSSet *uniqueDeletes = [NSSet setWithArray:itemDeletes]; + NSMutableSet *reloadDeletePaths = [NSMutableSet new]; + NSMutableSet *reloadInsertPaths = [NSMutableSet new]; + for (IGListReloadIndexPath *reload in batchUpdates.itemReloads) { + if (![uniqueDeletes containsObject:reload.fromIndexPath]) { + [reloadDeletePaths addObject:reload.fromIndexPath]; + [reloadInsertPaths addObject:reload.toIndexPath]; + } + } + [itemDeletes addObjectsFromArray:[reloadDeletePaths allObjects]]; + [itemInserts addObjectsFromArray:[reloadInsertPaths allObjects]]; + + const BOOL fixIndexPathImbalance = IGListExperimentEnabled(experiments, IGListExperimentFixIndexPathImbalance); + IGListBatchUpdateData *updateData = [[IGListBatchUpdateData alloc] initWithInsertSections:inserts + deleteSections:deletes + moveSections:moves + insertIndexPaths:itemInserts + deleteIndexPaths:itemDeletes + updateIndexPaths:itemUpdates + moveIndexPaths:itemMoves + fixIndexPathImbalance:fixIndexPathImbalance]; + [collectionView ig_applyBatchUpdateData:updateData]; + return updateData; +} + + diff --git a/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h b/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h index df022716..5fd5c459 100644 --- a/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h +++ b/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h @@ -16,12 +16,6 @@ NS_ASSUME_NONNULL_BEGIN -FOUNDATION_EXTERN void convertReloadToDeleteInsert(NSMutableIndexSet *reloads, - NSMutableIndexSet *deletes, - NSMutableIndexSet *inserts, - IGListIndexSetResult *result, - NSArray> *fromObjects); - @interface IGListAdapterUpdater () @property (nonatomic, copy, nullable) NSArray *fromObjects; diff --git a/Tests/IGListAdapterUpdaterTests.m b/Tests/IGListAdapterUpdaterTests.m index ca98620c..659ed800 100644 --- a/Tests/IGListAdapterUpdaterTests.m +++ b/Tests/IGListAdapterUpdaterTests.m @@ -14,6 +14,7 @@ #import "IGListTestUICollectionViewDataSource.h" #import "IGTestObject.h" #import "IGListMoveIndexInternal.h" +#import "IGListAdapterUpdaterHelpers.h" #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] #define waitExpectation [self waitForExpectationsWithTimeout:30 handler:nil] @@ -396,7 +397,7 @@ [reloads addIndex:2]; NSMutableIndexSet *deletes = [result.deletes mutableCopy]; NSMutableIndexSet *inserts = [result.inserts mutableCopy]; - convertReloadToDeleteInsert(reloads, deletes, inserts, result, from); + IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); XCTAssertEqual(reloads.count, 0); XCTAssertEqual(deletes.count, 1); XCTAssertEqual(inserts.count, 1); @@ -412,7 +413,7 @@ [reloads addIndex:2]; NSMutableIndexSet *deletes = [result.deletes mutableCopy]; NSMutableIndexSet *inserts = [result.inserts mutableCopy]; - convertReloadToDeleteInsert(reloads, deletes, inserts, result, from); + IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); XCTAssertEqual(reloads.count, 0); XCTAssertEqual(deletes.count, 1); XCTAssertEqual(inserts.count, 1); @@ -427,7 +428,7 @@ NSMutableIndexSet *reloads = [NSMutableIndexSet indexSetWithIndex:1]; NSMutableIndexSet *deletes = [NSMutableIndexSet new]; NSMutableIndexSet *inserts = [NSMutableIndexSet new]; - convertReloadToDeleteInsert(reloads, deletes, inserts, result, from); + IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); XCTAssertEqual(reloads.count, 0); XCTAssertEqual(deletes.count, 0); XCTAssertEqual(inserts.count, 0);