Add experimental collectionView getter fix

Summary:
Adding a fix to the `IGListAdapterUpdater` that requests the `UICollectionView` to perform updates on until just-before we update. This way if the `UICollectionView` is changed between update-queue and execution (b/c updates are async), we guarantee the update is performed on the correct view.

See the accompanying unit test that fails w/out the fix enabled.

Differential Revision: D7889908

fbshipit-source-id: 7178677f34951a1e42986b0289fc4abc708d6946
This commit is contained in:
Ryan Nystrom 2018-05-11 07:34:52 -07:00 committed by Facebook Github Bot
parent cce5a462bb
commit 583efb936b
9 changed files with 224 additions and 134 deletions

View file

@ -5,6 +5,12 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag
4.0.0 (upcoming release)
-----
### Enhancements
### Fixes
- Experimental fix to get the `UICollectionView` for batch updating immediately before applying the update. [Ryan Nystrom](https://github.com/rnystrom) (tbd)
3.4.0
-----

View file

@ -26,6 +26,8 @@ typedef NS_OPTIONS (NSInteger, IGListExperiment) {
IGListExperimentDedupeItemUpdates = 1 << 5,
/// Test deferring object creation until just before diffing.
IGListExperimentDeferredToObjectCreation = 1 << 6,
/// Test getting collection view at update time.
IGListExperimentGetCollectionViewAtUpdate = 1 << 7,
};
/**

View file

@ -99,6 +99,8 @@
_registeredSupplementaryViewIdentifiers = [NSMutableSet new];
_registeredSupplementaryViewNibNames = [NSMutableSet new];
const BOOL settingFirstCollectionView = _collectionView == nil;
_collectionView = collectionView;
_collectionView.dataSource = self;
@ -110,7 +112,12 @@
[_collectionView.collectionViewLayout invalidateLayout];
[self _updateCollectionViewDelegate];
[self _updateAfterPublicSettingsChange];
// only construct
if (!IGListExperimentEnabled(self.experiments, IGListExperimentGetCollectionViewAtUpdate)
|| settingFirstCollectionView) {
[self _updateAfterPublicSettingsChange];
}
}
}
@ -193,11 +200,11 @@
[collectionView layoutIfNeeded];
NSIndexPath *indexPathFirstElement = [NSIndexPath indexPathForItem:0 inSection:section];
// collect the layout attributes for the cell and supplementary views for the first index
// this will break if there are supplementary views beyond item 0
NSMutableArray<UICollectionViewLayoutAttributes *> *attributes = nil;
const NSInteger numberOfItems = [collectionView numberOfItemsInSection:section];
if (numberOfItems > 0) {
attributes = [self _layoutAttributesForIndexPath:indexPathFirstElement supplementaryKinds:supplementaryKinds].mutableCopy;
@ -314,7 +321,7 @@
id<IGListAdapterDataSource> dataSource = self.dataSource;
UICollectionView *collectionView = self.collectionView;
if (dataSource == nil || collectionView == nil) {
IGLKLog(@"Warning: Your call to %s is ignored as dataSource or collectionView haven't been set.", __PRETTY_FUNCTION__);
IGLKLog(@"Warning: Your call to %s is ignored as dataSource or collectionView haven't been set.", __PRETTY_FUNCTION__);
if (completion) {
completion(NO);
}
@ -341,26 +348,26 @@
}
[self _enterBatchUpdates];
[self.updater performUpdateWithCollectionView:collectionView
fromObjects:fromObjects
toObjectsBlock:toObjectsBlock
animated:animated
objectTransitionBlock:^(NSArray *toObjects) {
// temporarily capture the item map that we are transitioning from in case
// there are any item deletes at the same
weakSelf.previousSectionMap = [weakSelf.sectionMap copy];
[self.updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock]
fromObjects:fromObjects
toObjectsBlock:toObjectsBlock
animated:animated
objectTransitionBlock:^(NSArray *toObjects) {
// temporarily capture the item map that we are transitioning from in case
// there are any item deletes at the same
weakSelf.previousSectionMap = [weakSelf.sectionMap copy];
[weakSelf _updateObjects:toObjects dataSource:dataSource];
} completion:^(BOOL finished) {
// release the previous items
weakSelf.previousSectionMap = nil;
[weakSelf _updateObjects:toObjects dataSource:dataSource];
} completion:^(BOOL finished) {
// release the previous items
weakSelf.previousSectionMap = nil;
[weakSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated];
if (completion) {
completion(finished);
}
[weakSelf _exitBatchUpdates];
}];
[weakSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated];
if (completion) {
completion(finished);
}
[weakSelf _exitBatchUpdates];
}];
}
- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion {
@ -379,16 +386,17 @@
NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self]);
__weak __typeof__(self) weakSelf = self;
[self.updater reloadDataWithCollectionView:collectionView reloadUpdateBlock:^{
// purge all section controllers from the item map so that they are regenerated
[weakSelf.sectionMap reset];
[weakSelf _updateObjects:uniqueObjects dataSource:dataSource];
} completion:^(BOOL finished) {
[weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO];
if (completion) {
completion(finished);
}
}];
[self.updater reloadDataWithCollectionViewBlock:[self _collectionViewBlock]
reloadUpdateBlock:^{
// purge all section controllers from the item map so that they are regenerated
[weakSelf.sectionMap reset];
[weakSelf _updateObjects:uniqueObjects dataSource:dataSource];
} completion:^(BOOL finished) {
[weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO];
if (completion) {
completion(finished);
}
}];
}
- (void)reloadObjects:(NSArray *)objects {
@ -447,7 +455,7 @@
- (nullable IGListSectionController *)sectionControllerForSection:(NSInteger)section {
IGAssertMainThread();
return [self.sectionMap sectionControllerForSection:section];
}
@ -586,6 +594,16 @@
#pragma mark - Private API
- (IGListCollectionViewBlock)_collectionViewBlock {
if (IGListExperimentEnabled(self.experiments, IGListExperimentGetCollectionViewAtUpdate)) {
__weak __typeof__(self) weakSelf = self;
return ^UICollectionView *{ return weakSelf.collectionView; };
} else {
__weak UICollectionView *collectionView = _collectionView;
return ^UICollectionView *{ return collectionView; };
}
}
// this method is what updates the "source of truth"
// this should only be called just before the collection view is updated
- (void)_updateObjects:(NSArray *)objects dataSource:(id<IGListAdapterDataSource>)dataSource {
@ -637,7 +655,7 @@
[sectionControllers addObject:sectionController];
[validObjects addObject:object];
}
#if DEBUG
IGAssert([NSSet setWithArray:sectionControllers].count == sectionControllers.count,
@"Section controllers array is not filled with unique objects; section controllers are being reused");
@ -725,7 +743,7 @@
}
- (NSArray<UICollectionViewLayoutAttributes *> *)_layoutAttributesForIndexPath:(NSIndexPath *)indexPath
supplementaryKinds:(NSArray<NSString *> *)supplementaryKinds {
supplementaryKinds:(NSArray<NSString *> *)supplementaryKinds {
UICollectionViewLayout *layout = self.collectionView.collectionViewLayout;
NSMutableArray<UICollectionViewLayoutAttributes *> *attributes = [NSMutableArray new];
@ -933,7 +951,7 @@
- (void)selectItemAtIndex:(NSInteger)index
sectionController:(IGListSectionController *)sectionController
animated:(BOOL)animated
scrollPosition:(UICollectionViewScrollPosition)scrollPosition {
scrollPosition:(UICollectionViewScrollPosition)scrollPosition {
IGAssertMainThread();
IGParameterAssert(sectionController != nil);
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO];
@ -941,9 +959,9 @@
}
- (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass
withReuseIdentifier:(NSString *)reuseIdentifier
forSectionController:(IGListSectionController *)sectionController
atIndex:(NSInteger)index {
withReuseIdentifier:(NSString *)reuseIdentifier
forSectionController:(IGListSectionController *)sectionController
atIndex:(NSInteger)index {
IGAssertMainThread();
IGParameterAssert(sectionController != nil);
IGParameterAssert(cellClass != nil);
@ -1057,9 +1075,9 @@
IGAssert(collectionView != nil, @"Performing batch updates without a collection view.");
[self _enterBatchUpdates];
__weak __typeof__(self) weakSelf = self;
[self.updater performUpdateWithCollectionView:collectionView animated:animated itemUpdates:^{
[self.updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock] animated:animated itemUpdates:^{
weakSelf.isInUpdateBlock = YES;
// the adapter acts as the batch context with its API stripped to just the IGListBatchContext protocol
updates(weakSelf);
@ -1080,7 +1098,7 @@
animated:(BOOL)animated {
IGAssertMainThread();
IGParameterAssert(sectionController != nil);
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO];
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated];
}
@ -1089,16 +1107,16 @@
completion:(void (^)(BOOL finished))completion{
const NSInteger section = [self sectionForSectionController:sectionController];
const NSInteger items = [_collectionView numberOfItemsInSection:section];
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray new];
for (NSInteger item = 0; item < items; item++) {
[indexPaths addObject:[NSIndexPath indexPathForItem:item inSection:section]];
}
UICollectionViewLayout *layout = _collectionView.collectionViewLayout;
UICollectionViewLayoutInvalidationContext *context = [[[layout.class invalidationContextClass] alloc] init];
[context invalidateItemsAtIndexPaths:indexPaths];
__weak __typeof__(_collectionView) weakCollectionView = _collectionView;
// do not call -[UICollectionView performBatchUpdates:completion:] while already updating. defer it until completed.
@ -1228,7 +1246,7 @@
IGAssert(collectionView != nil, @"Moving section %@ without a collection view from index %li to index %li.",
sectionController, (long)fromIndex, (long)toIndex);
IGAssert(self.moveDelegate != nil, @"Moving section %@ without a moveDelegate set", sectionController);
if (fromIndex != toIndex) {
id<IGListAdapterDataSource> dataSource = self.dataSource;
@ -1250,18 +1268,18 @@
// inform the data source to update its model
[self.moveDelegate listAdapter:self moveObject:object from:previousObjects to:objects];
// update our model based on that provided by the data source
NSArray<id<IGListDiffable>> *updatedObjects = [dataSource objectsForListAdapter:self];
[self _updateObjects:updatedObjects dataSource:dataSource];
}
// even if from and to index are equal, we need to perform the "move"
// iOS interactively moves items, not sections, so we might have actually moved the item
// to the end of the preceeding section or beginning of the following section
[self.updater moveSectionInCollectionView:collectionView fromIndex:fromIndex toIndex:toIndex];
}
- (void)moveInSectionControllerInteractive:(IGListSectionController *)sectionController
fromIndex:(NSInteger)fromIndex
toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) {
@ -1269,7 +1287,7 @@
IGParameterAssert(sectionController != nil);
IGParameterAssert(fromIndex >= 0);
IGParameterAssert(toIndex >= 0);
[sectionController moveObjectFromIndex:fromIndex toIndex:toIndex];
}
@ -1278,9 +1296,10 @@
UICollectionView *collectionView = self.collectionView;
IGAssert(collectionView != nil, @"Reverting move without a collection view from %@ to %@.",
sourceIndexPath, destinationIndexPath);
// revert by moving back in the opposite direction
[collectionView moveItemAtIndexPath:destinationIndexPath toIndexPath:sourceIndexPath];
}
@end

View file

@ -42,10 +42,11 @@
|| self.toObjectsBlock != nil;
}
- (void)performReloadDataWithCollectionView:(UICollectionView *)collectionView {
- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
// bail early if the collection view has been deallocated in the time since the update was queued
UICollectionView *collectionView = collectionViewBlock();
if (collectionView == nil) {
return;
}
@ -92,11 +93,12 @@
self.state = IGListBatchUpdateStateIdle;
}
- (void)performBatchUpdatesWithCollectionView:(UICollectionView *)collectionView {
- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
IGAssert(self.state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle");
// bail early if the collection view has been deallocated in the time since the update was queued
UICollectionView *collectionView = collectionViewBlock();
if (collectionView == nil) {
return;
}
@ -208,11 +210,13 @@
// queue another update in case something changed during batch updates. this method will bail next runloop if
// there are no changes
[self _queueUpdateWithCollectionView:collectionView];
[self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
};
// block that executes the batch update and exception handling
void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){
[collectionView layoutIfNeeded];
@try {
[delegate listAdapterUpdater:self willPerformBatchUpdatesWithCollectionView:collectionView];
if (result.changeCount > 100 && IGListExperimentEnabled(experiments, IGListExperimentReloadDataFallback)) {
@ -369,16 +373,16 @@ void convertReloadToDeleteInsert(NSMutableIndexSet *reloads,
self.batchUpdates = [IGListBatchUpdates new];
}
- (void)_queueUpdateWithCollectionView:(UICollectionView *)collectionView {
- (void)_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
// callers may hold weak refs and lose the collection view by the time we requeue, bail if that's the case
if (collectionView == nil) {
return;
}
// if (collectionView == nil) {
// return;
// }
__weak __typeof__(self) weakSelf = self;
__weak __typeof__(collectionView) weakCollectionView = collectionView;
// __weak __typeof__(collectionView) weakCollectionView = collectionView;
// dispatch after a given amount of time to coalesce other updates and execute as one
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, self.coalescanceTime * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
@ -388,9 +392,9 @@ void convertReloadToDeleteInsert(NSMutableIndexSet *reloads,
}
if (weakSelf.hasQueuedReloadData) {
[weakSelf performReloadDataWithCollectionView:weakCollectionView];
[weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock];
} else {
[weakSelf performBatchUpdatesWithCollectionView:weakCollectionView];
[weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock];
}
});
}
@ -418,14 +422,14 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons
return functions;
}
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
fromObjects:(NSArray *)fromObjects
toObjectsBlock:(IGListToObjectBlock)toObjectsBlock
animated:(BOOL)animated
objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
completion:(IGListUpdatingCompletion)completion {
IGAssertMainThread();
IGParameterAssert(collectionView != nil);
IGParameterAssert(collectionViewBlock != nil);
IGParameterAssert(objectTransitionBlock != nil);
// only update the items that we are coming from if it has not been set
@ -448,15 +452,15 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons
[self.completionBlocks addObject:localCompletion];
}
[self _queueUpdateWithCollectionView:collectionView];
[self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
animated:(BOOL)animated
itemUpdates:(void (^)(void))itemUpdates
completion:(void (^)(BOOL))completion {
IGAssertMainThread();
IGParameterAssert(collectionView != nil);
IGParameterAssert(collectionViewBlock != nil);
IGParameterAssert(itemUpdates != nil);
IGListBatchUpdates *batchUpdates = self.batchUpdates;
@ -475,7 +479,7 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons
// reset to YES in -cleanupState
self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;
[self _queueUpdateWithCollectionView:collectionView];
[self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}
}
@ -566,11 +570,11 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons
}];
}
- (void)reloadDataWithCollectionView:(UICollectionView *)collectionView
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
completion:(nullable IGListUpdatingCompletion)completion {
IGAssertMainThread();
IGParameterAssert(collectionView != nil);
IGParameterAssert(collectionViewBlock != nil);
IGParameterAssert(reloadUpdateBlock != nil);
IGListUpdatingCompletion localCompletion = completion;
@ -580,7 +584,7 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons
self.reloadUpdates = reloadUpdateBlock;
self.queuedReloadData = YES;
[self _queueUpdateWithCollectionView:collectionView];
[self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}
- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections {
@ -595,5 +599,6 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons
}
}
@end

View file

@ -15,7 +15,7 @@
return [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality];
}
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
fromObjects:(NSArray *)fromObjects
toObjectsBlock:(IGListToObjectBlock)toObjectsBlock
animated:(BOOL)animated
@ -25,18 +25,18 @@
NSArray *toObjects = toObjectsBlock() ?: @[];
objectTransitionBlock(toObjects);
}
[self _synchronousReloadDataWithCollectionView:collectionView];
[self _synchronousReloadDataWithCollectionView:collectionViewBlock()];
if (completion) {
completion(YES);
}
}
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
animated:(BOOL)animated
itemUpdates:(IGListItemUpdateBlock)itemUpdates
completion:(IGListUpdatingCompletion)completion {
itemUpdates();
[self _synchronousReloadDataWithCollectionView:collectionView];
[self _synchronousReloadDataWithCollectionView:collectionViewBlock()];
if (completion) {
completion(YES);
}
@ -66,9 +66,9 @@
[self _synchronousReloadDataWithCollectionView:collectionView];
}
- (void)reloadDataWithCollectionView:(UICollectionView *)collectionView reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion {
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion {
reloadUpdateBlock();
[self _synchronousReloadDataWithCollectionView:collectionView];
[self _synchronousReloadDataWithCollectionView:collectionViewBlock()];
if (completion) {
completion(YES);
}

View file

@ -39,6 +39,10 @@ typedef void (^IGListReloadUpdateBlock)(void);
NS_SWIFT_NAME(ListToObjectBlock)
typedef NSArray * _Nullable (^IGListToObjectBlock)(void);
/// A block that returns a collection view to perform updates on.
NS_SWIFT_NAME(ListCollectionViewBlock)
typedef UICollectionView * _Nullable (^IGListCollectionViewBlock)(void);
/**
Implement this protocol in order to handle both section and row based update events. Implementation should forward or
coalesce these events to a backing store or collection.
@ -63,7 +67,7 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
/**
Tells the delegate to perform a section transition from an old array of objects to a new one.
@param collectionView The collection view to perform the transition on.
@param collectionViewBlock A block returning the collecion view to perform updates on.
@param fromObjects The previous objects in the collection view. Objects must conform to `IGListDiffable`.
@param toObjectsBlock A block returning the new objects in the collection view. Objects must conform to `IGListDiffable`.
@param animated A flag indicating if the transition should be animated.
@ -77,12 +81,12 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
The `objectTransitionBlock` block should be called prior to making any `UICollectionView` updates, passing in the `toObjects`
that the updater is applying.
*/
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
fromObjects:(nullable NSArray<id <IGListDiffable>> *)fromObjects
toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock
animated:(BOOL)animated
objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
completion:(nullable IGListUpdatingCompletion)completion;
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
fromObjects:(nullable NSArray<id <IGListDiffable>> *)fromObjects
toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock
animated:(BOOL)animated
objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
completion:(nullable IGListUpdatingCompletion)completion;
/**
Tells the delegate to perform item inserts at the given index paths.
@ -128,7 +132,7 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
/**
Tells the delegate to move a section from and to given indexes.
@param collectionView The collection view on which to perform the transition.
@param fromIndex The source index of the section to move.
@param toIndex The destination index of the section to move.
@ -136,17 +140,17 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
- (void)moveSectionInCollectionView:(UICollectionView *)collectionView
fromIndex:(NSInteger)fromIndex
toIndex:(NSInteger)toIndex;
/**
Completely reload data in the collection.
@param collectionView The collection view to reload.
@param collectionViewBlock A block returning the collecion view to reload.
@param reloadUpdateBlock A block that must be called when the adapter reloads the collection view.
@param completion A completion block to execute when the reload is finished.
*/
- (void)reloadDataWithCollectionView:(UICollectionView *)collectionView
reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
completion:(nullable IGListUpdatingCompletion)completion;
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
completion:(nullable IGListUpdatingCompletion)completion;
/**
Completely reload each section in the collection view.
@ -159,16 +163,17 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
/**
Perform an item update block in the collection view.
@param collectionView The collection view to update.
@param collectionViewBlock A block returning the collecion view to perform updates on.
@param animated A flag indicating if the transition should be animated.
@param itemUpdates A block containing all of the updates.
@param completion A completion block to execute when the update is finished.
*/
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
animated:(BOOL)animated
itemUpdates:(IGListItemUpdateBlock)itemUpdates
completion:(nullable IGListUpdatingCompletion)completion;
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
animated:(BOOL)animated
itemUpdates:(IGListItemUpdateBlock)itemUpdates
completion:(nullable IGListUpdatingCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View file

@ -41,8 +41,8 @@ FOUNDATION_EXTERN void convertReloadToDeleteInsert(NSMutableIndexSet *reloads,
@property (nonatomic, assign) IGListBatchUpdateState state;
@property (nonatomic, strong, nullable) IGListBatchUpdateData *applyingUpdateData;
- (void)performReloadDataWithCollectionView:(UICollectionView *)collectionView;
- (void)performBatchUpdatesWithCollectionView:(UICollectionView *)collectionView;
- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock;
- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock;
- (void)cleanStateBeforeUpdates;
- (BOOL)hasChanges;

View file

@ -1832,4 +1832,53 @@
IGAssertEqualPoint(finalAttribute.center, attribute.center.x + offset.x ,attribute.center.y + offset.y);
}
- (void)test_whenSwappingCollectionViewsAfterUpdate_thatUpdatePerformedOnTheCorrectCollectionView {
// BEGIN: setup of FIRST adapter+dataSource+collectionView
IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil];
adapter1.experiments |= IGListExperimentGetCollectionViewAtUpdate;
UICollectionView *collectionView1 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]];
[self.window addSubview:collectionView1];
adapter1.collectionView = collectionView1;
IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new];
dataSource1.objects = @[
genTestObject(@1, @1),
genTestObject(@2, @1)
];
adapter1.dataSource = dataSource1;
// END: setup of FIRST adapter+dataSource+collectionView
// BEGIN: setup of SECOND adapter+dataSource+collectionView
IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil];
adapter2.experiments |= IGListExperimentGetCollectionViewAtUpdate;
UICollectionView *collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]];
[self.window addSubview:collectionView2];
adapter2.collectionView = collectionView2;
IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new];
dataSource2.objects = @[
genTestObject(@3, @1)
];
adapter2.dataSource = dataSource2;
// END: setup of SECOND adapter+dataSource+collectionView
// delete the last-most section from the FIRST dataSource
dataSource1.objects = @[
genTestObject(@1, @1)
];
XCTestExpectation *expectation = genExpectation;
[adapter1 performUpdatesAnimated:YES completion:^(BOOL finished) {
[expectation fulfill];
}];
// simulate a collectionView swap (e.g. cell reuse) immediately after an async update is queued
adapter1.collectionView = collectionView2;
adapter2.collectionView = collectionView1;
[self waitForExpectationsWithTimeout:30 handler:nil];
}
@end

View file

@ -29,6 +29,10 @@
@implementation IGListAdapterUpdaterTests
- (IGListCollectionViewBlock)collectionViewBlock {
return ^UICollectionView *{ return self.collectionView; };
}
- (void)setUp {
[super setUp];
@ -56,27 +60,27 @@
}
- (void)test_whenUpdatingWithNil_thatUpdaterHasNoChanges {
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:nil toObjectsBlock:nil animated:YES objectTransitionBlock:self.updateBlock completion:nil];
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:nil toObjectsBlock:nil animated:YES objectTransitionBlock:self.updateBlock completion:nil];
XCTAssertFalse([self.updater hasChanges]);
}
- (void)test_whenUpdatingtoObjects_thatUpdaterHasChanges {
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:nil toObjectsBlock:^NSArray *{return @[@0];} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:nil toObjectsBlock:^NSArray *{return @[@0];} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
XCTAssertTrue([self.updater hasChanges]);
}
- (void)test_whenUpdatingfromObjects_thatUpdaterHasChanges {
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:@[@0] toObjectsBlock:^NSArray *{return nil;} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:@[@0] toObjectsBlock:^NSArray *{return nil;} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
XCTAssertTrue([self.updater hasChanges]);
}
- (void)test_whenUpdatingtoObjects_withfromObjects_thatUpdaterHasChanges {
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:@[@0] toObjectsBlock:^NSArray *{return @[@1];} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:@[@0] toObjectsBlock:^NSArray *{return @[@1];} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
XCTAssertTrue([self.updater hasChanges]);
}
- (void)test_whenCleaningUpState_withChanges_thatUpdaterHasNoChanges {
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:nil toObjectsBlock:^NSArray *{return @[@0];} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:nil toObjectsBlock:^NSArray *{return @[@0];} animated:YES objectTransitionBlock:self.updateBlock completion:nil];
XCTAssertTrue([self.updater hasChanges]);
[self.updater cleanStateBeforeUpdates];
XCTAssertFalse([self.updater hasChanges]);
@ -84,10 +88,10 @@
- (void)test_whenReloadingData_thatCollectionViewUpdates {
self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[]]];
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
XCTAssertEqual([self.collectionView numberOfSections], 1);
self.dataSource.sections = @[];
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
XCTAssertEqual([self.collectionView numberOfSections], 0);
}
@ -103,11 +107,11 @@
};
self.dataSource.sections = from;
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
XCTAssertEqual([self.collectionView numberOfSections], 1);
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 2);
[expectation fulfill];
}];
@ -126,11 +130,11 @@
};
self.dataSource.sections = from;
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
XCTAssertEqual([self.collectionView numberOfSections], 2);
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 1);
[expectation fulfill];
}];
@ -149,12 +153,12 @@
};
self.dataSource.sections = from;
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
XCTAssertEqual([self.collectionView numberOfSections], 1);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 2);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2);
@ -177,13 +181,13 @@
};
self.dataSource.sections = from;
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
XCTAssertEqual([self.collectionView numberOfSections], 2);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 3);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1);
@ -198,7 +202,7 @@
[IGSectionObject sectionWithObjects:@[@0, @1]],
[IGSectionObject sectionWithObjects:@[@0, @1]]
];
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
XCTAssertEqual([self.collectionView numberOfSections], 2);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2);
@ -226,7 +230,7 @@
};
self.dataSource.sections = from;
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
// the collection view has been setup with 1 section and now needs layout
// calling performBatchUpdates: on a collection view needing layout will force layout
@ -234,7 +238,7 @@
[self.collectionView setNeedsLayout];
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 1);
[expectation fulfill];
}];
@ -253,7 +257,7 @@
};
self.dataSource.sections = from;
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
__block NSInteger completionCounter = 0;
@ -266,7 +270,7 @@
[IGSectionObject sectionWithObjects:@[]]
];
};
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:anotherTo animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:anotherTo animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
completionCounter++;
XCTAssertEqual([self.collectionView numberOfSections], 3);
XCTAssertEqual(completionCounter, 2);
@ -275,7 +279,7 @@
};
XCTestExpectation *expectation2 = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:^(NSArray *toObjects) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:^(NSArray *toObjects) {
// executing this block within the updater is just before performBatchUpdates: are applied
// should be able to queue another update here, similar to an update being queued between it beginning and executing
// the performBatchUpdates: block
@ -292,13 +296,13 @@
}
- (void)test_whenQueuingItemUpdates_thatUpdaterHasChanges {
[self.updater performUpdateWithCollectionView:self.collectionView animated:YES itemUpdates:^{} completion:nil];
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{} completion:nil];
XCTAssertTrue([self.updater hasChanges]);
}
- (void)test_whenOnlyQueueingItemUpdates_thatUpdateBlockExecutes {
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView animated:YES itemUpdates:^{
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{
// expectation should be triggered. test failure is a timeout
[expectation fulfill];
} completion:nil];
@ -309,7 +313,7 @@
__block BOOL itemUpdateBlockExecuted = NO;
__block BOOL sectionUpdateBlockExecuted = NO;
[self.updater performUpdateWithCollectionView:self.collectionView
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock]
fromObjects:nil
toObjectsBlock:^NSArray *{return @[[IGSectionObject sectionWithObjects:@[@1]]];}
animated:YES objectTransitionBlock:^(NSArray * toObjects) {
@ -319,7 +323,7 @@
completion:nil];
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView animated:YES itemUpdates:^{
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{
itemUpdateBlockExecuted = YES;
} completion:^(BOOL finished) {
// test in the item completion block that the SECTION operations have been performed
@ -355,13 +359,13 @@
};
self.dataSource.sections = from;
[self.updater performReloadDataWithCollectionView:self.collectionView];
[self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]];
// without moves as inserts, we would assert b/c the # of items in each section changes
self.updater.movesAsDeletesInserts = YES;
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 3);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1);
XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2);
@ -422,7 +426,7 @@
id compilerFriendlyNil = nil;
[[mockDelegate reject] listAdapterUpdater:self.updater willReloadDataWithCollectionView:compilerFriendlyNil];
[[mockDelegate reject] listAdapterUpdater:self.updater didReloadDataWithCollectionView:compilerFriendlyNil];
[self.updater performReloadDataWithCollectionView:compilerFriendlyNil];
[self.updater performReloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; }];
[mockDelegate verify];
}
@ -432,7 +436,7 @@
id compilerFriendlyNil = nil;
[[mockDelegate reject] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:compilerFriendlyNil];
[[mockDelegate reject] listAdapterUpdater:self.updater didPerformBatchUpdates:[OCMArg any] collectionView:compilerFriendlyNil];
[self.updater performBatchUpdatesWithCollectionView:compilerFriendlyNil];
[self.updater performBatchUpdatesWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; }];
[mockDelegate verify];
}
@ -461,7 +465,7 @@
];
IGListAdapterUpdater *updater = [IGListAdapterUpdater new];
[updater performReloadDataWithCollectionView:collectionView];
[updater performReloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; }];
XCTAssertEqual([collectionView numberOfSections], 1);
XCTAssertEqual([collectionView numberOfItemsInSection:0], 1);
@ -470,7 +474,7 @@
[IGSectionObject sectionWithObjects:@[@1]],
[IGSectionObject sectionWithObjects:@[@1, @2, @3, @4]]
];
[updater performReloadDataWithCollectionView:collectionView];
[updater performReloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; }];
XCTAssertEqual([collectionView numberOfSections], 2);
XCTAssertEqual([collectionView numberOfItemsInSection:0], 1);
@ -493,7 +497,7 @@
[IGSectionObject sectionWithObjects:@[]]
];
};
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:self.dataSource.sections toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:self.dataSource.sections toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[expectation fulfill];
}];
waitExpectation;
@ -520,7 +524,7 @@
[IGSectionObject sectionWithObjects:@[]]
];
};
[self.updater performUpdateWithCollectionView:self.collectionView fromObjects:self.dataSource.sections toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:self.dataSource.sections toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) {
[expectation fulfill];
}];
waitExpectation;
@ -532,12 +536,12 @@
self.dataSource.sections = @[object];
__block BOOL reloadDataCompletionExecuted = NO;
[self.updater reloadDataWithCollectionView:self.collectionView reloadUpdateBlock:^{} completion:^(BOOL finished) {
[self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:^(BOOL finished) {
reloadDataCompletionExecuted = YES;
}];
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView animated:YES itemUpdates:^{
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{
object.objects = @[@2, @1, @4, @5];
[self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[
[NSIndexPath indexPathForItem:2 inSection:0],
@ -565,7 +569,7 @@
__block BOOL objectTransitionBlockExecuted = NO;
__block BOOL completionBlockExecuted = NO;
[self.updater performUpdateWithCollectionView:self.collectionView
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock]
fromObjects:self.dataSource.sections
toObjectsBlock:^NSArray *{return self.dataSource.sections;}
animated:YES
@ -577,7 +581,7 @@
}];
XCTestExpectation *expectation = genExpectation;
[self.updater performUpdateWithCollectionView:self.collectionView animated:YES itemUpdates:^{
[self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{
object.objects = @[@2, @1, @4, @5];
[self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[
[NSIndexPath indexPathForItem:2 inSection:0],