diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd9728c..15fe1a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ----- diff --git a/Source/Common/IGListExperiments.h b/Source/Common/IGListExperiments.h index 3ea84d5b..f38155cd 100644 --- a/Source/Common/IGListExperiments.h +++ b/Source/Common/IGListExperiments.h @@ -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, }; /** diff --git a/Source/IGListAdapter.m b/Source/IGListAdapter.m index acdb268c..7023b0a3 100644 --- a/Source/IGListAdapter.m +++ b/Source/IGListAdapter.m @@ -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 *attributes = nil; - + const NSInteger numberOfItems = [collectionView numberOfItemsInSection:section]; if (numberOfItems > 0) { attributes = [self _layoutAttributesForIndexPath:indexPathFirstElement supplementaryKinds:supplementaryKinds].mutableCopy; @@ -314,7 +321,7 @@ id 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)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 *)_layoutAttributesForIndexPath:(NSIndexPath *)indexPath - supplementaryKinds:(NSArray *)supplementaryKinds { + supplementaryKinds:(NSArray *)supplementaryKinds { UICollectionViewLayout *layout = self.collectionView.collectionViewLayout; NSMutableArray *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 *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 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> *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 + diff --git a/Source/IGListAdapterUpdater.m b/Source/IGListAdapterUpdater.m index 1ea295f8..6aff2726 100644 --- a/Source/IGListAdapterUpdater.m +++ b/Source/IGListAdapterUpdater.m @@ -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 diff --git a/Source/IGListReloadDataUpdater.m b/Source/IGListReloadDataUpdater.m index 737455e9..6f865451 100644 --- a/Source/IGListReloadDataUpdater.m +++ b/Source/IGListReloadDataUpdater.m @@ -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); } diff --git a/Source/IGListUpdatingDelegate.h b/Source/IGListUpdatingDelegate.h index 47a69464..c7d2dbd9 100644 --- a/Source/IGListUpdatingDelegate.h +++ b/Source/IGListUpdatingDelegate.h @@ -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> *)fromObjects - toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock - animated:(BOOL)animated - objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock - completion:(nullable IGListUpdatingCompletion)completion; +- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + fromObjects:(nullable NSArray> *)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 + diff --git a/Source/Internal/IGListAdapterUpdaterInternal.h b/Source/Internal/IGListAdapterUpdaterInternal.h index efc9b844..3edb209a 100644 --- a/Source/Internal/IGListAdapterUpdaterInternal.h +++ b/Source/Internal/IGListAdapterUpdaterInternal.h @@ -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; diff --git a/Tests/IGListAdapterE2ETests.m b/Tests/IGListAdapterE2ETests.m index 96d30025..c3b36d63 100644 --- a/Tests/IGListAdapterE2ETests.m +++ b/Tests/IGListAdapterE2ETests.m @@ -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 diff --git a/Tests/IGListAdapterUpdaterTests.m b/Tests/IGListAdapterUpdaterTests.m index fa818e90..b32659e8 100644 --- a/Tests/IGListAdapterUpdaterTests.m +++ b/Tests/IGListAdapterUpdaterTests.m @@ -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],