diff --git a/Examples/Examples-tvOS/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h b/Examples/Examples-tvOS/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h index ef11732d6..708ff821a 100644 --- a/Examples/Examples-tvOS/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h +++ b/Examples/Examples-tvOS/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h @@ -32,7 +32,6 @@ #import "IGListCollectionViewLayout.h" #import "IGListCollectionViewLayoutCompatible.h" #import "IGListDisplayDelegate.h" -#import "IGListExperimentalAdapterUpdater.h" #import "IGListGenericSectionController.h" #import "IGListKit.h" #import "IGListReloadDataUpdater.h" @@ -48,4 +47,3 @@ FOUNDATION_EXPORT double IGListKitVersionNumber; FOUNDATION_EXPORT const unsigned char IGListKitVersionString[]; - diff --git a/Source/IGListDiffKit/IGListExperiments.h b/Source/IGListDiffKit/IGListExperiments.h index d8d98d2e3..7d4078a62 100644 --- a/Source/IGListDiffKit/IGListExperiments.h +++ b/Source/IGListDiffKit/IGListExperiments.h @@ -20,7 +20,7 @@ typedef NS_OPTIONS (NSInteger, IGListExperiment) { IGListExperimentBackgroundDiffing = 1 << 2, /// Test invalidating layout when cell reloads/updates in IGListBindingSectionController. IGListExperimentInvalidateLayoutForUpdates = 1 << 3, - /// Test skipping performBatchUpdate if we don't have any updates. `IGListExperimentalAdapterUpdater` only. + /// Test skipping performBatchUpdate if we don't have any updates. IGListExperimentSkipPerformUpdateIfPossible = 1 << 4, /// Test skipping creating {view : section controller} map, which has inconsistency issue. IGListExperimentSkipViewSectionControllerMap = 1 << 5 diff --git a/Source/IGListKit/IGListAdapterUpdater.h b/Source/IGListKit/IGListAdapterUpdater.h index e330abf4e..629e4a74f 100644 --- a/Source/IGListKit/IGListAdapterUpdater.h +++ b/Source/IGListKit/IGListAdapterUpdater.h @@ -9,6 +9,7 @@ #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -22,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN */ IGLK_SUBCLASSING_RESTRICTED NS_SWIFT_NAME(ListAdapterUpdater) -@interface IGListAdapterUpdater : NSObject +@interface IGListAdapterUpdater : NSObject @end diff --git a/Source/IGListKit/IGListAdapterUpdater.m b/Source/IGListKit/IGListAdapterUpdater.m index 301f784fb..b5041ae81 100644 --- a/Source/IGListKit/IGListAdapterUpdater.m +++ b/Source/IGListKit/IGListAdapterUpdater.m @@ -6,20 +6,24 @@ */ #import "IGListAdapterUpdater.h" -#import "IGListAdapterUpdaterInternal.h" #import #import "IGListAdapterUpdaterHelpers.h" -#import "IGListArrayUtilsInternal.h" #import "IGListIndexSetResultInternal.h" #import "IGListMoveIndexPathInternal.h" #import "IGListReloadIndexPath.h" +#import "IGListTransitionData.h" +#import "IGListUpdateTransactable.h" +#import "IGListUpdateTransactionBuilder.h" #import "UICollectionView+IGListBatchUpdateData.h" -typedef void (^IGListAdapterUpdaterDiffResultBlock)(IGListIndexSetResult *); -typedef void (^IGListAdapterUpdaterBlock)(void); -typedef void (^IGListAdapterUpdaterCompletionBlock)(BOOL); +@interface IGListAdapterUpdater () +@property (nonatomic, strong) IGListUpdateTransactionBuilder *transactionBuilder; +@property (nonatomic, strong, nullable) IGListUpdateTransactionBuilder *lastTransactionBuilder; +@property (nonatomic, strong, nullable) id transaction; +@property (nonatomic, assign) BOOL hasQueuedUpdate; +@end @implementation IGListAdapterUpdater @@ -35,340 +39,90 @@ - (instancetype)init { IGAssertMainThread(); if (self = [super init]) { - // the default is to use animations unless NO is passed - _queuedUpdateIsAnimated = YES; - _completionBlocks = [NSMutableArray new]; - _batchUpdates = [IGListBatchUpdates new]; + _transactionBuilder = [IGListUpdateTransactionBuilder new]; _allowsBackgroundReloading = YES; _allowsReloadingOnTooManyUpdates = YES; } return self; } -#pragma mark - Private API +#pragma mark - Update - (BOOL)hasChanges { - return self.hasQueuedReloadData - || [self.batchUpdates hasChanges] - || self.fromObjects != nil - || self.toObjectsBlock != nil; + return [self.transactionBuilder hasChanges]; } -- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { +- (void)_queueUpdateIfNeeded { IGAssertMainThread(); - id delegate = self.delegate; - void (^reloadUpdates)(void) = self.reloadUpdates; - IGListBatchUpdates *batchUpdates = self.batchUpdates; - NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy]; - - [self cleanStateBeforeUpdates]; - - void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { - for (IGListUpdatingCompletion block in completionBlocks) { - block(finished); - } - - self.state = IGListBatchUpdateStateIdle; - }; - - // bail early if the collection view has been deallocated in the time since the update was queued - UICollectionView *collectionView = collectionViewBlock(); - if (collectionView == nil) { - [self _cleanStateAfterUpdates]; - executeCompletionBlocks(NO); - [_delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView]; + if (self.hasQueuedUpdate || !self.transactionBuilder.hasChanges) { return; } - // item updates must not send mutations to the collection view while we are reloading - self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock; - - if (reloadUpdates) { - reloadUpdates(); - } - - // execute all stored item update blocks even if we are just calling reloadData. the actual collection view - // mutations will be discarded, but clients are encouraged to put their actual /data/ mutations inside the - // update block as well, so if we don't execute the block the changes will never happen - for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) { - itemUpdateBlock(); - } - - // add any completion blocks from item updates. added after item blocks are executed in order to capture any - // re-entrant updates - [completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks]; - - self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; - - [self _cleanStateAfterUpdates]; - - [delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView isFallbackReload:NO]; - [collectionView reloadData]; - [collectionView.collectionViewLayout invalidateLayout]; - [collectionView layoutIfNeeded]; - [delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView isFallbackReload:NO]; + __weak __typeof__(self) weakSelf = self; - executeCompletionBlocks(YES); + // dispatch_async to give the main queue time to collect more batch updates so that a minimum amount of work + // (diffing, etc) is done on main. dispatch_async does not garauntee a full runloop turn will pass though. + // see -performUpdateWithCollectionView:fromObjects:toObjects:animated:objectTransitionBlock:completion: for more + // details on how coalescence is done. + self.hasQueuedUpdate = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + strongSelf.hasQueuedUpdate = NO; + [strongSelf update]; + }); } -- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { +- (void)update { IGAssertMainThread(); - IGAssert(self.state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle"); - - // create local variables so we can immediately clean our state but pass these items into the batch update block - id delegate = self.delegate; - NSArray *fromObjects = [self.fromObjects copy]; - IGListToObjectBlock toObjectsBlock = [self.toObjectsBlock copy]; - NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy]; - void (^objectTransitionBlock)(NSArray *) = [self.objectTransitionBlock copy]; - const BOOL animated = self.queuedUpdateIsAnimated; - const BOOL allowsReloadingOnTooManyUpdates = self.allowsReloadingOnTooManyUpdates; - const IGListExperiment experiments = self.experiments; - IGListBatchUpdates *batchUpdates = self.batchUpdates; - - // clean up all state so that new updates can be coalesced while the current update is in flight - [self cleanStateBeforeUpdates]; - - void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { - self.applyingUpdateData = nil; - self.state = IGListBatchUpdateStateIdle; - - for (IGListUpdatingCompletion block in completionBlocks) { - block(finished); - } - }; - // bail early if the collection view has been deallocated in the time since the update was queued - UICollectionView *collectionView = collectionViewBlock(); - if (collectionView == nil) { - [self _cleanStateAfterUpdates]; - executeCompletionBlocks(NO); - [_delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView]; + if (![self.transactionBuilder hasChanges]) { return; } - NSArray *toObjects = nil; - if (toObjectsBlock != nil) { - toObjects = objectsWithDuplicateIdentifiersRemoved(toObjectsBlock()); - } -#ifdef DEBUG - for (id obj in toObjects) { - IGAssert([obj conformsToProtocol:@protocol(IGListDiffable)], - @"In order to use IGListAdapterUpdater, object %@ must conform to IGListDiffable", obj); - IGAssert([obj diffIdentifier] != nil, - @"Cannot have a nil diffIdentifier for object %@", obj); + if (self.transaction && self.transaction.state != IGListBatchUpdateStateIdle) { + return; } -#endif - - void (^executeUpdateBlocks)(void) = ^{ - self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock; - - // run the update block so that the adapter can set its items. this makes sure that just before the update is - // committed that the data source is updated to the /latest/ "toObjects". this makes the data source in sync - // with the items that the updater is transitioning to - if (objectTransitionBlock != nil) { - objectTransitionBlock(toObjects); - } - - // execute each item update block which should make calls like insert, delete, and reload for index paths - // we collect all mutations in corresponding sets on self, then filter based on UICollectionView shortcomings - // call after the objectTransitionBlock so section level mutations happen before any items - for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) { - itemUpdateBlock(); - } - // add any completion blocks from item updates. added after item blocks are executed in order to capture any - // re-entrant updates - [completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks]; - - self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; - }; - - void (^reloadDataFallback)(void) = ^{ - [delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView isFallbackReload:YES]; - executeUpdateBlocks(); - [self _cleanStateAfterUpdates]; - [self _performBatchUpdatesItemBlockApplied]; - [collectionView reloadData]; - [collectionView layoutIfNeeded]; - executeCompletionBlocks(YES); - [delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView isFallbackReload:YES]; - - // queue another update in case something changed during batch updates. this method will bail next runloop if - // there are no changes - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; + IGListUpdateTransactationConfig config = (IGListUpdateTransactationConfig) { + .sectionMovesAsDeletesInserts = _sectionMovesAsDeletesInserts, + .singleItemSectionUpdates = _singleItemSectionUpdates, + .preferItemReloadsForSectionReloads = _preferItemReloadsForSectionReloads, + .allowsBackgroundReloading = _allowsBackgroundReloading, + .allowsReloadingOnTooManyUpdates = _allowsReloadingOnTooManyUpdates, + .experiments = _experiments, }; - // disables multiple performBatchUpdates: from happening at the same time - [self _beginPerformBatchUpdatesToObjects:toObjects]; + id transaction = [self.transactionBuilder buildWithConfig:config delegate:_delegate updater:self]; + self.transaction = transaction; + self.lastTransactionBuilder = self.transactionBuilder; + self.transactionBuilder = [IGListUpdateTransactionBuilder new]; - // if the collection view isn't in a visible window, skip diffing and batch updating. execute all transition blocks, - // reload data, execute completion blocks, and get outta here - if (self.allowsBackgroundReloading && collectionView.window == nil) { - reloadDataFallback(); + if (!transaction) { + // If we don't have enough information, we might not be able to create a transaction. return; } - // block executed in the first param block of -[UICollectionView performBatchUpdates:completion:] - void (^batchUpdatesBlock)(IGListIndexSetResult *result) = ^(IGListIndexSetResult *result){ - executeUpdateBlocks(); - if (self.singleItemSectionUpdates) { - [collectionView deleteSections:result.deletes]; - [collectionView insertSections:result.inserts]; - for (IGListMoveIndex *move in result.moves) { - [collectionView moveSection:move.from toSection:move.to]; - } - // NOTE: for section updates, it's updated in the IGListSectionController's -didUpdateToObject:, since there is *only* 1 cell for the section, we can just update that cell. - - self.applyingUpdateData = [[IGListBatchUpdateData alloc] - initWithInsertSections:result.inserts - deleteSections:result.deletes - moveSections:[NSSet setWithArray:result.moves] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[] - moveIndexPaths:@[]]; - } else { - self.applyingUpdateData = IGListApplyUpdatesToCollectionView(collectionView, - result, - self.batchUpdates.sectionReloads, - self.batchUpdates.itemInserts, - self.batchUpdates.itemDeletes, - self.batchUpdates.itemReloads, - self.batchUpdates.itemMoves, - fromObjects, - self.sectionMovesAsDeletesInserts, - self.preferItemReloadsForSectionReloads); - } - - [self _cleanStateAfterUpdates]; - [self _performBatchUpdatesItemBlockApplied]; - }; - - // block used as the second param of -[UICollectionView performBatchUpdates:completion:] - void (^fallbackWithoutUpdates)(void) = ^(void) { - executeCompletionBlocks(NO); - - [delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView]; - - // queue another update in case something changed during batch updates. this method will bail next runloop if - // there are no changes - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; - }; - - // block used as the second param of -[UICollectionView performBatchUpdates:completion:] - void (^batchUpdatesCompletionBlock)(BOOL) = ^(BOOL finished) { - IGListBatchUpdateData *oldApplyingUpdateData = self.applyingUpdateData; - executeCompletionBlocks(finished); - - [delegate listAdapterUpdater:self didPerformBatchUpdates:oldApplyingUpdateData collectionView:collectionView]; - - // queue another update in case something changed during batch updates. this method will bail next runloop if - // there are no changes - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; - }; - - void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){ - [delegate listAdapterUpdater:self -willPerformBatchUpdatesWithCollectionView:collectionView - fromObjects:fromObjects - toObjects:toObjects - listIndexSetResult:result - animated:animated]; - - // Wrap `[UICollectionView performBatchUpdates ...]` so that in case it crashes, the first app symbol will not be a block. A block name includes the - // line number, which means if you change the block line number, it will be categorized as a different crash. This makes tracking crashes - // across multiple app-versions a pain. - IGListAdapterUpdaterPerformBatchUpdate(collectionView, animated, ^{ - batchUpdatesBlock(result); - }, batchUpdatesCompletionBlock); - }; - - // block that executes the batch update and exception handling - void (^tryToPerformUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){ - @try { - if (collectionView.dataSource == nil) { - // If the data source is nil, we should not call any collection view update. - fallbackWithoutUpdates(); - } else if (result.changeCount > 100 && allowsReloadingOnTooManyUpdates) { - reloadDataFallback(); - } else { - performUpdate(result); - } - } @catch (NSException *exception) { - [delegate listAdapterUpdater:self - collectionView:collectionView - willCrashWithException:exception - fromObjects:fromObjects - toObjects:toObjects - diffResult:result - updates:(id)self.applyingUpdateData]; - @throw exception; - } - }; - - const BOOL onBackgroundThread = IGListExperimentEnabled(experiments, IGListExperimentBackgroundDiffing); - [delegate listAdapterUpdater:self willDiffFromObjects:fromObjects toObjects:toObjects]; - IGListAdapterUpdaterPerformDiffing(fromObjects, toObjects, IGListDiffEquality, onBackgroundThread, ^(IGListIndexSetResult *result){ - [delegate listAdapterUpdater:self didDiffWithResults:result onBackgroundThread:onBackgroundThread]; - tryToPerformUpdate(result); - }); -} - -- (void)_beginPerformBatchUpdatesToObjects:(NSArray *)toObjects { - self.pendingTransitionToObjects = toObjects; - self.state = IGListBatchUpdateStateQueuedBatchUpdate; -} - -- (void)_performBatchUpdatesItemBlockApplied { - self.pendingTransitionToObjects = nil; -} - -- (void)cleanStateBeforeUpdates { - self.queuedUpdateIsAnimated = YES; - - // destroy to/from transition items - self.fromObjects = nil; - self.toObjectsBlock = nil; - - // destroy reloadData state - self.reloadUpdates = nil; - self.queuedReloadData = NO; - - // remove indexpath/item changes - self.objectTransitionBlock = nil; - - // removes all object completion blocks. done before updates to start collecting completion blocks for coalesced - // or re-entrant object updates - [self.completionBlocks removeAllObjects]; -} - -- (void)_cleanStateAfterUpdates { - self.batchUpdates = [IGListBatchUpdates new]; -} - -- (void)_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { - IGAssertMainThread(); - __weak __typeof__(self) weakSelf = self; - - // dispatch_async to give the main queue time to collect more batch updates so that a minimum amount of work - // (diffing, etc) is done on main. dispatch_async does not garauntee a full runloop turn will pass though. - // see -performUpdateWithCollectionView:fromObjects:toObjects:animated:objectTransitionBlock:completion: for more - // details on how coalescence is done. - dispatch_async(dispatch_get_main_queue(), ^{ - if (weakSelf.state != IGListBatchUpdateStateIdle - || ![weakSelf hasChanges]) { + __weak __typeof__(transaction) weakTransaction = transaction; + [transaction addCompletionBlock:^(BOOL finished) { + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { return; } + if (strongSelf.transaction == weakTransaction) { + strongSelf.transaction = nil; + strongSelf.lastTransactionBuilder = nil; - if (weakSelf.hasQueuedReloadData) { - [weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock]; - } else { - [weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock]; + // queue another update in case something changed during batch updates. this method will bail next runloop if + // there are no changes + [strongSelf _queueUpdateIfNeeded]; } - }); + }]; + [transaction begin]; } #pragma mark - IGListUpdatingDelegate @@ -394,72 +148,112 @@ - (NSPointerFunctions *)objectLookupPointerFunctions { } - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - fromObjects:(NSArray *)fromObjects - toObjectsBlock:(IGListToObjectBlock)toObjectsBlock - animated:(BOOL)animated - objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock - completion:(IGListUpdatingCompletion)completion { + fromObjects:(NSArray *)fromObjects + toObjectsBlock:(IGListToObjectBlock)toObjectsBlock + animated:(BOOL)animated + objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock + completion:(IGListUpdatingCompletion)completion { + IGFailAssert(@"IGListExperimentalAdapterUpdater works with IGListUpdatingDelegateExperimental and doesn't implement the regular -performUpdateWithCollectionViewBlock method"); + completion(NO); +} + +- (void)performExperimentalUpdateAnimated:(BOOL)animated + collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + dataBlock:(IGListTransitionDataBlock)dataBlock + applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock + completion:(IGListUpdatingCompletion)completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil); - IGParameterAssert(objectTransitionBlock != nil); - - // only update the items that we are coming from if it has not been set - // this allows multiple updates to be called while an update is already in progress, and the transition from > to - // will be done on the first "fromObjects" received and the last "toObjects" - // if performBatchUpdates: hasn't applied the update block, then data source hasn't transitioned its state. if an - // update is queued in between then we must use the pending toObjects - self.fromObjects = self.fromObjects ?: self.pendingTransitionToObjects ?: fromObjects; - self.toObjectsBlock = toObjectsBlock; - - // disabled animations will always take priority - // reset to YES in -cleanupState - self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated; - - // always use the last update block, even though this should always do the exact same thing - self.objectTransitionBlock = objectTransitionBlock; - - IGListUpdatingCompletion localCompletion = completion; - if (localCompletion) { - [self.completionBlocks addObject:localCompletion]; - } + IGParameterAssert(dataBlock != nil); + IGParameterAssert(applyDataBlock != nil); + + [self.transactionBuilder addSectionBatchUpdateAnimated:animated + collectionViewBlock:collectionViewBlock + dataBlock:dataBlock + applyDataBlock:applyDataBlock + completion:completion]; - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; + [self _queueUpdateIfNeeded]; } + - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - animated:(BOOL)animated - itemUpdates:(void (^)(void))itemUpdates - completion:(void (^)(BOOL))completion { + animated:(BOOL)animated + itemUpdates:(void (^)(void))itemUpdates + completion:(void (^)(BOOL))completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil); IGParameterAssert(itemUpdates != nil); - IGListBatchUpdates *batchUpdates = self.batchUpdates; - if (completion != nil) { - [batchUpdates.itemCompletionBlocks addObject:completion]; - } - // if already inside the execution of the update block, immediately unload the itemUpdates block. // the completion blocks are executed later in the lifecycle, so that still needs to be added to the batch - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + if (completion != nil) { + [self.transaction addCompletionBlock:completion]; + } itemUpdates(); } else { - [batchUpdates.itemUpdateBlocks addObject:itemUpdates]; + [self.transactionBuilder addItemBatchUpdateAnimated:animated + collectionViewBlock:collectionViewBlock + itemUpdates:itemUpdates + completion:completion]; - // disabled animations will always take priority - // reset to YES in -cleanupState - self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated; + [self _queueUpdateIfNeeded]; + } +} - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; +- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock + completion:(nullable IGListUpdatingCompletion)completion { + IGAssertMainThread(); + IGParameterAssert(collectionViewBlock != nil); + IGParameterAssert(reloadUpdateBlock != nil); + + [self.transactionBuilder addReloadDataWithCollectionViewBlock:collectionViewBlock + reloadBlock:reloadUpdateBlock + completion:completion]; + + [self _queueUpdateIfNeeded]; +} + +- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block { + // Unlike the other "performs", we need the dataSource change to be synchronous. + // Which means we need to cancel the current transaction, flatten the changes from + // both the current transtion and builder, and execute that new transaction. + + if (!self.transaction && ![self.transactionBuilder hasChanges]) { + // If nothing is going on, lets take a shortcut. + block(); + return; } + + IGListUpdateTransactionBuilder *builder = [IGListUpdateTransactionBuilder new]; + [builder addDataSourceChange:block]; + + // Lets try to cancel any current transactions. + if ([self.transaction cancel] && self.lastTransactionBuilder) { + // We still need to apply the item-updates and completion-blocks, so lets merge the builders. + [builder addChangesFromBuilder:(IGListUpdateTransactionBuilder *)self.lastTransactionBuilder]; + } + + // Lets merge pending changes + [builder addChangesFromBuilder:self.transactionBuilder]; + + // Clear the current state + self.transaction = nil; + self.lastTransactionBuilder = nil; + self.transactionBuilder = builder; + + // Update synchronously + [self update]; } - (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { IGAssertMainThread(); IGParameterAssert(collectionView != nil); IGParameterAssert(indexPaths != nil); - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.batchUpdates.itemInserts addObjectsFromArray:indexPaths]; + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.transaction insertItemsAtIndexPaths:indexPaths]; } else { [self.delegate listAdapterUpdater:self willInsertIndexPaths:indexPaths collectionView:collectionView]; [collectionView insertItemsAtIndexPaths:indexPaths]; @@ -470,8 +264,8 @@ - (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPa IGAssertMainThread(); IGParameterAssert(collectionView != nil); IGParameterAssert(indexPaths != nil); - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.batchUpdates.itemDeletes addObjectsFromArray:indexPaths]; + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.transaction deleteItemsAtIndexPaths:indexPaths]; } else { [self.delegate listAdapterUpdater:self willDeleteIndexPaths:indexPaths collectionView:collectionView]; [collectionView deleteItemsAtIndexPaths:indexPaths]; @@ -481,9 +275,8 @@ - (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPa - (void)moveItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:fromIndexPath to:toIndexPath]; - [self.batchUpdates.itemMoves addObject:move]; + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.transaction moveItemFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; } else { [self.delegate listAdapterUpdater:self willMoveFromIndexPath:fromIndexPath toIndexPath:toIndexPath collectionView:collectionView]; [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; @@ -493,15 +286,26 @@ - (void)moveItemInCollectionView:(UICollectionView *)collectionView - (void)reloadItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - IGListReloadIndexPath *reload = [[IGListReloadIndexPath alloc] initWithFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; - [self.batchUpdates.itemReloads addObject:reload]; + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.transaction reloadItemFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; } else { [self.delegate listAdapterUpdater:self willReloadIndexPaths:@[fromIndexPath] collectionView:collectionView]; [collectionView reloadItemsAtIndexPaths:@[fromIndexPath]]; } } +- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { + IGAssertMainThread(); + IGParameterAssert(collectionView != nil); + IGParameterAssert(sections != nil); + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.transaction reloadSections:sections]; + } else { + [self.delegate listAdapterUpdater:self willReloadSections:sections collectionView:collectionView]; + [collectionView reloadSections:sections]; + } +} + - (void)moveSectionInCollectionView:(UICollectionView *)collectionView fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { @@ -542,63 +346,4 @@ - (void)moveSectionInCollectionView:(UICollectionView *)collectionView }]; } -- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock - completion:(nullable IGListUpdatingCompletion)completion { - IGAssertMainThread(); - IGParameterAssert(collectionViewBlock != nil); - IGParameterAssert(reloadUpdateBlock != nil); - - IGListUpdatingCompletion localCompletion = completion; - if (localCompletion) { - [self.completionBlocks addObject:localCompletion]; - } - - self.reloadUpdates = reloadUpdateBlock; - self.queuedReloadData = YES; - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; -} - -- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { - IGAssertMainThread(); - IGParameterAssert(collectionView != nil); - IGParameterAssert(sections != nil); - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.batchUpdates.sectionReloads addIndexes:sections]; - } else { - [self.delegate listAdapterUpdater:self willReloadSections:sections collectionView:collectionView]; - [collectionView reloadSections:sections]; - } -} - -#pragma mark - Helpers - -static void IGListAdapterUpdaterPerformBatchUpdate(UICollectionView *collectionView, BOOL animated, IGListAdapterUpdaterBlock updates, IGListAdapterUpdaterCompletionBlock completion) { - if (animated) { - [collectionView performBatchUpdates:updates completion:completion]; - } else { - [UIView performWithoutAnimation:^{ - [collectionView performBatchUpdates:updates completion:completion]; - }]; - } -} - -static void IGListAdapterUpdaterPerformDiffing(NSArray> *_Nullable oldArray, - NSArray> *_Nullable newArray, - IGListDiffOption option, - BOOL onBackgroundThread, - IGListAdapterUpdaterDiffResultBlock completion) { - if (onBackgroundThread) { - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ - IGListIndexSetResult *result = IGListDiff(oldArray, newArray, option); - dispatch_async(dispatch_get_main_queue(), ^{ - completion(result); - }); - }); - } else { - IGListIndexSetResult *result = IGListDiff(oldArray, newArray, option); - completion(result); - } -} - @end diff --git a/Source/IGListKit/IGListExperimentalAdapterUpdater.h b/Source/IGListKit/IGListExperimentalAdapterUpdater.h deleted file mode 100644 index 1d6a1a372..000000000 --- a/Source/IGListKit/IGListExperimentalAdapterUpdater.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - Temporary class to test a more reliable, performant, and cleaner `IGListAdapterUpdater`. - - An `IGListAdapterUpdater` is a concrete type that conforms to `IGListUpdatingDelegate`. - It is an out-of-box updater for `IGListAdapter` objects to use. - - @note This updater performs re-entrant, coalesced updating for a list. It also uses a least-minimal diff - for calculating UI updates when `IGListAdapter` calls - `-performUpdateWithCollectionView:fromObjects:toObjects:completion:`. - */ -IGLK_SUBCLASSING_RESTRICTED -NS_SWIFT_NAME(ListExperimentalAdapterUpdater) -@interface IGListExperimentalAdapterUpdater : NSObject - -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/IGListExperimentalAdapterUpdater.m b/Source/IGListKit/IGListExperimentalAdapterUpdater.m deleted file mode 100644 index 4f377b9ed..000000000 --- a/Source/IGListKit/IGListExperimentalAdapterUpdater.m +++ /dev/null @@ -1,345 +0,0 @@ -/* - * 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 "IGListExperimentalAdapterUpdater.h" - -#import - -#import "IGListAdapterUpdaterHelpers.h" -#import "IGListIndexSetResultInternal.h" -#import "IGListMoveIndexPathInternal.h" -#import "IGListReloadIndexPath.h" -#import "IGListTransitionData.h" -#import "IGListUpdateTransactable.h" -#import "IGListUpdateTransactionBuilder.h" -#import "UICollectionView+IGListBatchUpdateData.h" - -@interface IGListExperimentalAdapterUpdater () -@property (nonatomic, strong) IGListUpdateTransactionBuilder *transactionBuilder; -@property (nonatomic, strong, nullable) IGListUpdateTransactionBuilder *lastTransactionBuilder; -@property (nonatomic, strong, nullable) id transaction; -@property (nonatomic, assign) BOOL hasQueuedUpdate; -@end - -@implementation IGListExperimentalAdapterUpdater - -@synthesize delegate = _delegate; -@synthesize sectionMovesAsDeletesInserts = _sectionMovesAsDeletesInserts; -@synthesize singleItemSectionUpdates = _singleItemSectionUpdates; -@synthesize preferItemReloadsForSectionReloads = _preferItemReloadsForSectionReloads; -@synthesize allowsBackgroundReloading = _allowsBackgroundReloading; -@synthesize allowsReloadingOnTooManyUpdates = _allowsReloadingOnTooManyUpdates; -@synthesize experiments = _experiments; - -- (instancetype)init { - IGAssertMainThread(); - - if (self = [super init]) { - _transactionBuilder = [IGListUpdateTransactionBuilder new]; - _allowsBackgroundReloading = YES; - _allowsReloadingOnTooManyUpdates = YES; - } - return self; -} - -#pragma mark - Update - -- (BOOL)hasChanges { - return [self.transactionBuilder hasChanges]; -} - -- (void)_queueUpdateIfNeeded { - IGAssertMainThread(); - - if (self.hasQueuedUpdate || !self.transactionBuilder.hasChanges) { - return; - } - - __weak __typeof__(self) weakSelf = self; - - // dispatch_async to give the main queue time to collect more batch updates so that a minimum amount of work - // (diffing, etc) is done on main. dispatch_async does not garauntee a full runloop turn will pass though. - // see -performUpdateWithCollectionView:fromObjects:toObjects:animated:objectTransitionBlock:completion: for more - // details on how coalescence is done. - self.hasQueuedUpdate = YES; - dispatch_async(dispatch_get_main_queue(), ^{ - weakSelf.hasQueuedUpdate = NO; - [weakSelf update]; - }); -} - -- (void)update { - IGAssertMainThread(); - - if (![self.transactionBuilder hasChanges]) { - return; - } - - if (self.transaction && self.transaction.state != IGListBatchUpdateStateIdle) { - return; - } - - IGListUpdateTransactationConfig config = (IGListUpdateTransactationConfig) { - .sectionMovesAsDeletesInserts = _sectionMovesAsDeletesInserts, - .singleItemSectionUpdates = _singleItemSectionUpdates, - .preferItemReloadsForSectionReloads = _preferItemReloadsForSectionReloads, - .allowsBackgroundReloading = _allowsBackgroundReloading, - .allowsReloadingOnTooManyUpdates = _allowsReloadingOnTooManyUpdates, - .experiments = _experiments, - }; - - id transaction = [self.transactionBuilder buildWithConfig:config delegate:_delegate updater:self]; - self.transaction = transaction; - self.lastTransactionBuilder = self.transactionBuilder; - self.transactionBuilder = [IGListUpdateTransactionBuilder new]; - - if (!transaction) { - // If we don't have enough information, we might not be able to create a transaction. - return; - } - - __weak __typeof__(self) weakSelf = self; - __weak __typeof__(transaction) weakTransaction = transaction; - [transaction addCompletionBlock:^(BOOL finished) { - __typeof__(self) strongSelf = weakSelf; - if (strongSelf == nil) { - return; - } - if (strongSelf.transaction == weakTransaction) { - strongSelf.transaction = nil; - strongSelf.lastTransactionBuilder = nil; - - // queue another update in case something changed during batch updates. this method will bail next runloop if - // there are no changes - [strongSelf _queueUpdateIfNeeded]; - } - }]; - [transaction begin]; -} - -#pragma mark - IGListUpdatingDelegate - -static BOOL IGListIsEqual(const void *a, const void *b, NSUInteger (*size)(const void *item)) { - const id left = (__bridge id)a; - const id right = (__bridge id)b; - return [left class] == [right class] - && [[left diffIdentifier] isEqual:[right diffIdentifier]]; -} - -// since the diffing algo used in this updater keys items based on their -diffIdentifier, we must use a map table that -// precisely mimics this behavior -static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(const void *item)) { - return [[(__bridge id)item diffIdentifier] hash]; -} - -- (NSPointerFunctions *)objectLookupPointerFunctions { - NSPointerFunctions *functions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; - functions.hashFunction = IGListIdentifierHash; - functions.isEqualFunction = IGListIsEqual; - return functions; -} - -- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - fromObjects:(NSArray *)fromObjects - toObjectsBlock:(IGListToObjectBlock)toObjectsBlock - animated:(BOOL)animated - objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock - completion:(IGListUpdatingCompletion)completion { - IGFailAssert(@"IGListExperimentalAdapterUpdater works with IGListUpdatingDelegateExperimental and doesn't implement the regular -performUpdateWithCollectionViewBlock method"); - completion(NO); -} - -- (void)performExperimentalUpdateAnimated:(BOOL)animated - collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - dataBlock:(IGListTransitionDataBlock)dataBlock - applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock - completion:(IGListUpdatingCompletion)completion { - IGAssertMainThread(); - IGParameterAssert(collectionViewBlock != nil); - IGParameterAssert(dataBlock != nil); - IGParameterAssert(applyDataBlock != nil); - - [self.transactionBuilder addSectionBatchUpdateAnimated:animated - collectionViewBlock:collectionViewBlock - dataBlock:dataBlock - applyDataBlock:applyDataBlock - completion:completion]; - - [self _queueUpdateIfNeeded]; -} - - -- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - animated:(BOOL)animated - itemUpdates:(void (^)(void))itemUpdates - completion:(void (^)(BOOL))completion { - IGAssertMainThread(); - IGParameterAssert(collectionViewBlock != nil); - IGParameterAssert(itemUpdates != nil); - - // if already inside the execution of the update block, immediately unload the itemUpdates block. - // the completion blocks are executed later in the lifecycle, so that still needs to be added to the batch - if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - if (completion != nil) { - [self.transaction addCompletionBlock:completion]; - } - itemUpdates(); - } else { - [self.transactionBuilder addItemBatchUpdateAnimated:animated - collectionViewBlock:collectionViewBlock - itemUpdates:itemUpdates - completion:completion]; - - [self _queueUpdateIfNeeded]; - } -} - -- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock - reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock - completion:(nullable IGListUpdatingCompletion)completion { - IGAssertMainThread(); - IGParameterAssert(collectionViewBlock != nil); - IGParameterAssert(reloadUpdateBlock != nil); - - [self.transactionBuilder addReloadDataWithCollectionViewBlock:collectionViewBlock - reloadBlock:reloadUpdateBlock - completion:completion]; - - [self _queueUpdateIfNeeded]; -} - -- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block { - // Unlike the other "performs", we need the dataSource change to be synchronous. - // Which means we need to cancel the current transaction, flatten the changes from - // both the current transtion and builder, and execute that new transaction. - - if (!self.transaction && ![self.transactionBuilder hasChanges]) { - // If nothing is going on, lets take a shortcut. - block(); - return; - } - - IGListUpdateTransactionBuilder *builder = [IGListUpdateTransactionBuilder new]; - [builder addDataSourceChange:block]; - - // Lets try to cancel any current transactions. - if ([self.transaction cancel] && self.lastTransactionBuilder) { - // We still need to apply the item-updates and completion-blocks, so lets merge the builders. - [builder addChangesFromBuilder:(IGListUpdateTransactionBuilder *)self.lastTransactionBuilder]; - } - - // Lets merge pending changes - [builder addChangesFromBuilder:self.transactionBuilder]; - - // Clear the current state - self.transaction = nil; - self.lastTransactionBuilder = nil; - self.transactionBuilder = builder; - - // Update synchronously - [self update]; -} - -- (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { - IGAssertMainThread(); - IGParameterAssert(collectionView != nil); - IGParameterAssert(indexPaths != nil); - if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.transaction insertItemsAtIndexPaths:indexPaths]; - } else { - [self.delegate listAdapterUpdater:self willInsertIndexPaths:indexPaths collectionView:collectionView]; - [collectionView insertItemsAtIndexPaths:indexPaths]; - } -} - -- (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { - IGAssertMainThread(); - IGParameterAssert(collectionView != nil); - IGParameterAssert(indexPaths != nil); - if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.transaction deleteItemsAtIndexPaths:indexPaths]; - } else { - [self.delegate listAdapterUpdater:self willDeleteIndexPaths:indexPaths collectionView:collectionView]; - [collectionView deleteItemsAtIndexPaths:indexPaths]; - } -} - -- (void)moveItemInCollectionView:(UICollectionView *)collectionView - fromIndexPath:(NSIndexPath *)fromIndexPath - toIndexPath:(NSIndexPath *)toIndexPath { - if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.transaction moveItemFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; - } else { - [self.delegate listAdapterUpdater:self willMoveFromIndexPath:fromIndexPath toIndexPath:toIndexPath collectionView:collectionView]; - [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; - } -} - -- (void)reloadItemInCollectionView:(UICollectionView *)collectionView - fromIndexPath:(NSIndexPath *)fromIndexPath - toIndexPath:(NSIndexPath *)toIndexPath { - if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.transaction reloadItemFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; - } else { - [self.delegate listAdapterUpdater:self willReloadIndexPaths:@[fromIndexPath] collectionView:collectionView]; - [collectionView reloadItemsAtIndexPaths:@[fromIndexPath]]; - } -} - -- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { - IGAssertMainThread(); - IGParameterAssert(collectionView != nil); - IGParameterAssert(sections != nil); - if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.transaction reloadSections:sections]; - } else { - [self.delegate listAdapterUpdater:self willReloadSections:sections collectionView:collectionView]; - [collectionView reloadSections:sections]; - } -} - -- (void)moveSectionInCollectionView:(UICollectionView *)collectionView - fromIndex:(NSInteger)fromIndex - toIndex:(NSInteger)toIndex { - IGAssertMainThread(); - IGParameterAssert(collectionView != nil); - - // iOS expects interactive reordering to be movement of items not sections - // after moving a single-item section controller, - // you end up with two items in the section for the drop location, - // and zero items in the section originating at the drag location - // so, we have to reload data rather than doing a section move - - [collectionView reloadData]; - - // It seems that reloadData called during UICollectionView's moveItemAtIndexPath - // delegate call does not reload all cells as intended - // So, we further reload all visible sections to make sure none of our cells - // are left with data that's out of sync with our dataSource - - id delegate = self.delegate; - - NSMutableIndexSet *visibleSections = [NSMutableIndexSet new]; - NSArray *visibleIndexPaths = [collectionView indexPathsForVisibleItems]; - for (NSIndexPath *visibleIndexPath in visibleIndexPaths) { - [visibleSections addIndex:visibleIndexPath.section]; - } - - [delegate listAdapterUpdater:self willReloadSections:visibleSections collectionView:collectionView]; - - // prevent double-animation from reloadData + reloadSections - - [CATransaction begin]; - [CATransaction setDisableActions:YES]; - [collectionView performBatchUpdates:^{ - [collectionView reloadSections:visibleSections]; - } completion:^(BOOL finished) { - [CATransaction commit]; - }]; -} - -@end diff --git a/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m b/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m index 99a1678db..16a766019 100644 --- a/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m +++ b/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m @@ -10,30 +10,27 @@ #import "IGListAdapterUpdaterInternal.h" #import "IGListBatchUpdateData+DebugDescription.h" #import "IGListDebuggingUtilities.h" - -#if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED -static NSMutableArray *linesFromObjects(NSArray *objects) { - NSMutableArray *lines = [NSMutableArray new]; - for (id object in objects) { - [lines addObject:[NSString stringWithFormat:@"Object %p of type %@ with identifier %@", - object, NSStringFromClass([object class]), [object diffIdentifier]]]; - } - return lines; -} -#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED +#import "IGListUpdateTransactable.h" @implementation IGListAdapterUpdater (DebugDescription) - (NSArray *)debugDescriptionLines { NSMutableArray *debug = [NSMutableArray new]; #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED - [debug addObject:[NSString stringWithFormat:@"Section moves as deletes+inserts: %@", IGListDebugBOOL(self.sectionMovesAsDeletesInserts)]]; - [debug addObject:[NSString stringWithFormat:@"Allows background reloading: %@", IGListDebugBOOL(self.allowsBackgroundReloading)]]; - [debug addObject:[NSString stringWithFormat:@"Has queued reload data: %@", IGListDebugBOOL(self.hasQueuedReloadData)]]; - [debug addObject:[NSString stringWithFormat:@"Queued update is animated: %@", IGListDebugBOOL(self.queuedUpdateIsAnimated)]]; + [debug addObject:@"Options:"]; + NSArray *options = @[ + [NSString stringWithFormat:@"sectionMovesAsDeletesInserts: %@", IGListDebugBOOL(self.sectionMovesAsDeletesInserts)], + [NSString stringWithFormat:@"singleItemSectionUpdates: %@", IGListDebugBOOL(self.singleItemSectionUpdates)], + [NSString stringWithFormat:@"preferItemReloadsForSectionReloads: %@", IGListDebugBOOL(self.preferItemReloadsForSectionReloads)], + [NSString stringWithFormat:@"allowsBackgroundReloading: %@", IGListDebugBOOL(self.allowsBackgroundReloading)], + [NSString stringWithFormat:@"allowsReloadingOnTooManyUpdates: %@", IGListDebugBOOL(self.allowsReloadingOnTooManyUpdates)] + ]; + [debug addObjectsFromArray:IGListDebugIndentedLines(options)]; + + const IGListBatchUpdateState state = self.transaction ? [self.transaction state] : IGListBatchUpdateStateIdle; NSString *stateString; - switch (self.state) { + switch (state) { case IGListBatchUpdateStateIdle: stateString = @"Idle"; break; @@ -49,25 +46,6 @@ @implementation IGListAdapterUpdater (DebugDescription) } [debug addObject:[NSString stringWithFormat:@"State: %@", stateString]]; - if (self.applyingUpdateData != nil) { - [debug addObject:@"Batch update data:"]; - [debug addObjectsFromArray:IGListDebugIndentedLines([self.applyingUpdateData debugDescriptionLines])]; - } - - if (self.fromObjects != nil) { - [debug addObject:@"From objects:"]; - [debug addObjectsFromArray:IGListDebugIndentedLines(linesFromObjects(self.fromObjects))]; - } - - if (self.toObjectsBlock != nil) { - [debug addObject:@"To objects:"]; - [debug addObjectsFromArray:IGListDebugIndentedLines(linesFromObjects(self.toObjectsBlock()))]; - } - - if (self.pendingTransitionToObjects != nil) { - [debug addObject:@"Pending objects:"]; - [debug addObjectsFromArray:IGListDebugIndentedLines(linesFromObjects(self.pendingTransitionToObjects))]; - } #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED return debug; } diff --git a/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h b/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h index 721e333c8..a7cc5f98a 100644 --- a/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h +++ b/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h @@ -6,39 +6,25 @@ */ #import -#import - -#import #import "IGListAdapterUpdater.h" #import "IGListBatchUpdateState.h" -#import "IGListBatchUpdates.h" + +@class IGListUpdateTransactionBuilder; +@protocol IGListUpdateTransactable; NS_ASSUME_NONNULL_BEGIN @interface IGListAdapterUpdater () -@property (nonatomic, copy, nullable) NSArray *fromObjects; -@property (nonatomic, copy, nullable) IGListToObjectBlock toObjectsBlock; -@property (nonatomic, copy, nullable) NSArray *pendingTransitionToObjects; -@property (nonatomic, strong) NSMutableArray *completionBlocks; - -@property (nonatomic, assign) BOOL queuedUpdateIsAnimated; - -@property (nonatomic, strong) IGListBatchUpdates *batchUpdates; - -@property (nonatomic, copy, nullable) IGListObjectTransitionBlock objectTransitionBlock; - -@property (nonatomic, copy, nullable) IGListReloadUpdateBlock reloadUpdates; -@property (nonatomic, assign, getter=hasQueuedReloadData) BOOL queuedReloadData; +- (BOOL)hasChanges; -@property (nonatomic, assign) IGListBatchUpdateState state; -@property (nonatomic, strong, nullable) IGListBatchUpdateData *applyingUpdateData; +/// Force an update to start +- (void)update; -- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock; -- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock; -- (void)cleanStateBeforeUpdates; -- (BOOL)hasChanges; +- (id)transaction; +- (IGListUpdateTransactionBuilder *)transactionBuilder; +- (IGListUpdateTransactionBuilder *)lastTransactionBuilder; @end diff --git a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdater+DebugDescription.h b/Source/IGListKit/Internal/IGListExperimentalAdapterUpdater+DebugDescription.h index cd4901ff3..2f0a1ec59 100644 --- a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdater+DebugDescription.h +++ b/Source/IGListKit/Internal/IGListExperimentalAdapterUpdater+DebugDescription.h @@ -5,9 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import -@interface IGListExperimentalAdapterUpdater (DebugDescription) +@interface IGListAdapterUpdater (DebugDescription) - (NSArray *)debugDescriptionLines; diff --git a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdater+DebugDescription.m b/Source/IGListKit/Internal/IGListExperimentalAdapterUpdater+DebugDescription.m deleted file mode 100644 index 3bac627f3..000000000 --- a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdater+DebugDescription.m +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 "IGListExperimentalAdapterUpdater+DebugDescription.h" - -#import "IGListBatchUpdateData+DebugDescription.h" -#import "IGListDebuggingUtilities.h" -#import "IGListExperimentalAdapterUpdaterInternal.h" -#import "IGListUpdateTransactable.h" - -@implementation IGListExperimentalAdapterUpdater (DebugDescription) - -- (NSArray *)debugDescriptionLines { - NSMutableArray *debug = [NSMutableArray new]; -#if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED - [debug addObject:@"Options:"]; - NSArray *options = @[ - [NSString stringWithFormat:@"sectionMovesAsDeletesInserts: %@", IGListDebugBOOL(self.sectionMovesAsDeletesInserts)], - [NSString stringWithFormat:@"singleItemSectionUpdates: %@", IGListDebugBOOL(self.singleItemSectionUpdates)], - [NSString stringWithFormat:@"preferItemReloadsForSectionReloads: %@", IGListDebugBOOL(self.preferItemReloadsForSectionReloads)], - [NSString stringWithFormat:@"allowsBackgroundReloading: %@", IGListDebugBOOL(self.allowsBackgroundReloading)], - [NSString stringWithFormat:@"allowsReloadingOnTooManyUpdates: %@", IGListDebugBOOL(self.allowsReloadingOnTooManyUpdates)] - ]; - [debug addObjectsFromArray:IGListDebugIndentedLines(options)]; - - const IGListBatchUpdateState state = self.transaction ? [self.transaction state] : IGListBatchUpdateStateIdle; - - NSString *stateString; - switch (state) { - case IGListBatchUpdateStateIdle: - stateString = @"Idle"; - break; - case IGListBatchUpdateStateQueuedBatchUpdate: - stateString = @"Queued batch update"; - break; - case IGListBatchUpdateStateExecutedBatchUpdateBlock: - stateString = @"Executed batch update block"; - break; - case IGListBatchUpdateStateExecutingBatchUpdateBlock: - stateString = @"Executing batch update block"; - break; - } - [debug addObject:[NSString stringWithFormat:@"State: %@", stateString]]; - -#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED - return debug; -} - -@end diff --git a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdaterInternal.h b/Source/IGListKit/Internal/IGListExperimentalAdapterUpdaterInternal.h deleted file mode 100644 index 472506f54..000000000 --- a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdaterInternal.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 "IGListExperimentalAdapterUpdater.h" -#import "IGListBatchUpdateState.h" - -@class IGListUpdateTransactionBuilder; -@protocol IGListUpdateTransactable; - -NS_ASSUME_NONNULL_BEGIN - -@interface IGListExperimentalAdapterUpdater () - -- (BOOL)hasChanges; - -/// Force an update to start -- (void)update; - -- (id)transaction; -- (IGListUpdateTransactionBuilder *)transactionBuilder; -- (IGListUpdateTransactionBuilder *)lastTransactionBuilder; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Tests/IGListAdapterE2ETests.m b/Tests/IGListAdapterE2ETests.m index 4b8ac1bff..31da8cff0 100644 --- a/Tests/IGListAdapterE2ETests.m +++ b/Tests/IGListAdapterE2ETests.m @@ -13,9 +13,12 @@ #import "IGListAdapterInternal.h" #import "IGListAdapterUpdateTester.h" +#import "IGListAdapterUpdater.h" +#import "IGListAdapterUpdaterInternal.h" #import "IGListTestCase.h" #import "IGListTestHelpers.h" #import "IGListTestOffsettingLayout.h" +#import "IGListUpdateTransactionBuilder.h" #import "IGTestCell.h" #import "IGTestDelegateController.h" #import "IGTestDelegateDataSource.h" @@ -773,34 +776,6 @@ - (void)test_whenPerformingUpdates_withItemsMovingInBlocks_thatCollectionViewWor [self waitForExpectationsWithTimeout:30 handler:nil]; } -- (void)test_whenReleasingObjects_thatAssertDoesntFire { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - // if the adapter keeps a strong ref to self and uses an async method, this will hit asserts that a list item - // controller is nil. the adapter should be released and the completion block never called. - @autoreleasepool { - IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:2]; - adapter.collectionView = self.collectionView; - adapter.dataSource = self.dataSource; - [adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - XCTAssertTrue(NO, @"Should not reach completion block for adapter"); - }]; - } - - self.collectionView = nil; - self.dataSource = nil; - - // queued after perform updates - XCTestExpectation *expectation = genExpectation; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [expectation fulfill]; - }); - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - - (void)test_whenItemDeleted_withDisplayDelegate_thatDelegateReceivesDeletedItem { [self setupWithObjects:@[ genTestObject(@1, @1), @@ -1097,69 +1072,6 @@ - (void)test_whenReloadingItems_withSectionDeletedInFront_thatUpdateCanBeApplied [self waitForExpectationsWithTimeout:30 handler:nil]; } -- (void)test_whenDataSourceDeallocatedAfterUpdateQueued_thatUpdateSuccesfullyCompletes { - IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; - dataSource.objects = @[genTestObject(@1, @1)]; - self.adapter.collectionView = self.collectionView; - self.adapter.dataSource = dataSource; - [self.collectionView layoutIfNeeded]; - - dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @2), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - [expectation fulfill]; - }]; - - dataSource = nil; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenQueuingUpdate_withSectionControllerBatchUpdate_thatSectionControllerNotRetained { - __weak id weakSectionController = nil; - __weak id weakAdapter = nil; - __weak id weakCollectionView = nil; - - @autoreleasepool { - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; - IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; - IGTestObject *object = genTestObject(@1, @2); - dataSource.objects = @[object]; - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]]; - adapter.collectionView = collectionView; - adapter.dataSource = dataSource; - [collectionView layoutIfNeeded]; - XCTAssertEqual([collectionView numberOfSections], 1); - XCTAssertEqual([collectionView numberOfItemsInSection:0], 2); - - IGListSectionController *section = [adapter sectionControllerForObject:object]; - - [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished) {}]; - - dataSource.objects = @[object, genTestObject(@2, @2)]; - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) {}]; - - weakAdapter = adapter; - weakCollectionView = collectionView; - weakSectionController = section; - - XCTAssertNotNil(weakAdapter); - XCTAssertNotNil(weakCollectionView); - XCTAssertNotNil(weakSectionController); - } - XCTAssertNil(weakAdapter); - XCTAssertNil(weakCollectionView); - XCTAssertNil(weakSectionController); -} - - (void)test_whenMovingItems_withObjectMoving_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @2), @@ -1548,88 +1460,6 @@ - (void)test_whenMassiveUpdate_thatUpdateApplied { [self waitForExpectationsWithTimeout:30 handler:nil]; } -- (void)test_whenInvalidatingInsideBatchUpdate_withSystemReleased_thatSystemNil_andCollectionViewDoesntCrashOnDealloc { - __weak id weakAdapter = nil; - __block BOOL executedItemUpdate = NO; - XCTestExpectation *expectation = genExpectation; - - @autoreleasepool { - self.dataSource.objects = @[ - genTestObject(@1, @"Bar"), - genTestObject(@0, @"Foo") - ]; - - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView]; - IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; - adapter.dataSource = self.dataSource; - adapter.collectionView = collectionView; - [collectionView layoutIfNeeded]; - - IGTestDelegateController *section = [adapter sectionControllerForObject:self.dataSource.objects.firstObject]; - - __weak typeof(section) weakSection = section; - section.itemUpdateBlock = ^{ - executedItemUpdate = YES; - [weakSection.collectionContext invalidateLayoutForSectionController:weakSection completion:nil]; - }; - - self.dataSource.objects = @[ - genTestObject(@1, @"Bar"), - genTestObject(@0, @"Foo") - ]; - - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertNotNil(collectionView); - XCTAssertNotNil(adapter); - [collectionView removeFromSuperview]; - [expectation fulfill]; - }]; - - weakAdapter = adapter; - XCTAssertNotNil(weakAdapter); - } - - [self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) { - XCTAssertTrue(executedItemUpdate); - XCTAssertNil(weakAdapter); - }]; -} - -- (void)test_whenInvalidatingInsideBatchUpdate_andRemoveThatSectionController_thatCollectionViewDoesntCrash { - IGTestObject *foo = genTestObject(@1, @"Foo"); - IGTestObject *bar = genTestObject(@0, @"Bar"); - self.dataSource.objects = @[foo, bar]; - - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView]; - IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; - adapter.dataSource = self.dataSource; - adapter.collectionView = collectionView; - [collectionView layoutIfNeeded]; - - IGTestDelegateController *sectionToRemove = [adapter sectionControllerForObject:bar]; - - self.dataSource.objects = @[foo]; - - XCTestExpectation *expectation = genExpectation; - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertTrue(finished); - [expectation fulfill]; - }]; - - XCTestExpectation *expectation2 = genExpectation; - [sectionToRemove.collectionContext invalidateLayoutForSectionController:sectionToRemove completion:^(BOOL finished) { - // That section-controller is about to be removed, so this should not finish. - XCTAssertFalse(finished); - [expectation2 fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - - (void)test_whenAddingMultipleUpdateListeners_withPerformUpdatesAnimated_thatEventsReceived { [self setupWithObjects:@[ genTestObject(@1, @1) @@ -2006,4 +1836,758 @@ - (void)test_whenCollectionViewBecomesNilDuringReloadData_thatStateCleanedCorrec [self waitForExpectationsWithTimeout:30 handler:nil]; } +- (void)test_whenUpdating_withMissingSectionController_thatDoesNotCrash { + [self setupWithObjects:@[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar") + ]]; + + // Adding an object that won't have a corresponding section-controller + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar"), + kIGTestDelegateDataSourceSkipObject + ]; + + // Perform updates on the adapter + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + // Checked that the update worked + XCTAssertTrue(finished); + // Check that we skipped the object with a missing section-controller + XCTAssertEqual([self.collectionView numberOfSections], 2); + XCTAssertEqual(self.adapter.objects.count, 2); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +#pragma mark - Dealloc checks + +- (void)test_whenReleasingObjects_thatAssertDoesntFire { + [self setupWithObjects:@[ + genTestObject(@1, @1) + ]]; + + // if the adapter keeps a strong ref to self and uses an async method, this will hit asserts that a list item + // controller is nil. the adapter should be released and the completion block never called. + @autoreleasepool { + IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; + IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:2]; + adapter.collectionView = self.collectionView; + adapter.dataSource = self.dataSource; + [adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + XCTAssertTrue(NO, @"Should not reach completion block for adapter"); + }]; + } + + self.collectionView = nil; + self.dataSource = nil; + + // queued after perform updates + XCTestExpectation *expectation = genExpectation; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenDataSourceDeallocatedAfterUpdateQueued_thatUpdateSuccesfullyCompletes { + IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; + dataSource.objects = @[genTestObject(@1, @1)]; + self.adapter.collectionView = self.collectionView; + self.adapter.dataSource = dataSource; + [self.collectionView layoutIfNeeded]; + + dataSource.objects = @[ + genTestObject(@1, @1), + genTestObject(@2, @2), + ]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { + XCTAssertEqual([self.collectionView numberOfSections], 2); + [expectation fulfill]; + }]; + + dataSource = nil; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenQueuingUpdate_withSectionControllerBatchUpdate_thatSectionControllerNotRetained { + __weak id weakSectionController = nil; + __weak id weakAdapter = nil; + __weak id weakCollectionView = nil; + + @autoreleasepool { + IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; + IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; + IGTestObject *object = genTestObject(@1, @2); + dataSource.objects = @[object]; + UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]]; + adapter.collectionView = collectionView; + adapter.dataSource = dataSource; + [collectionView layoutIfNeeded]; + XCTAssertEqual([collectionView numberOfSections], 1); + XCTAssertEqual([collectionView numberOfItemsInSection:0], 2); + + IGListSectionController *section = [adapter sectionControllerForObject:object]; + + [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { + object.value = @3; + [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; + } completion:^(BOOL finished) {}]; + + dataSource.objects = @[object, genTestObject(@2, @2)]; + [adapter performUpdatesAnimated:YES completion:^(BOOL finished) {}]; + + weakAdapter = adapter; + weakCollectionView = collectionView; + weakSectionController = section; + + XCTAssertNotNil(weakAdapter); + XCTAssertNotNil(weakCollectionView); + XCTAssertNotNil(weakSectionController); + } + XCTAssertNil(weakAdapter); + XCTAssertNil(weakCollectionView); + XCTAssertNil(weakSectionController); +} + +- (void)test_whenInvalidatingInsideBatchUpdate_withSystemReleased_thatSystemNil_andCollectionViewDoesntCrashOnDealloc { + __weak id weakAdapter = nil; + __block BOOL executedItemUpdate = NO; + XCTestExpectation *expectation = genExpectation; + + @autoreleasepool { + self.dataSource.objects = @[ + genTestObject(@1, @"Bar"), + genTestObject(@0, @"Foo") + ]; + + UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; + [self.window addSubview:collectionView]; + IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; + IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; + adapter.dataSource = self.dataSource; + adapter.collectionView = collectionView; + [collectionView layoutIfNeeded]; + + IGTestDelegateController *section = [adapter sectionControllerForObject:self.dataSource.objects.firstObject]; + + __weak typeof(section) weakSection = section; + section.itemUpdateBlock = ^{ + executedItemUpdate = YES; + [weakSection.collectionContext invalidateLayoutForSectionController:weakSection completion:nil]; + }; + + self.dataSource.objects = @[ + genTestObject(@1, @"Bar"), + genTestObject(@0, @"Foo") + ]; + + [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { + XCTAssertNotNil(collectionView); + XCTAssertNotNil(adapter); + [collectionView removeFromSuperview]; + [expectation fulfill]; + }]; + + weakAdapter = adapter; + XCTAssertNotNil(weakAdapter); + } + + [self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) { + XCTAssertTrue(executedItemUpdate); + XCTAssertNil(weakAdapter); + }]; +} + +- (void)test_whenInvalidatingInsideBatchUpdate_andRemoveThatSectionController_thatCollectionViewDoesntCrash { + IGTestObject *foo = genTestObject(@1, @"Foo"); + IGTestObject *bar = genTestObject(@0, @"Bar"); + self.dataSource.objects = @[foo, bar]; + + UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; + [self.window addSubview:collectionView]; + IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; + IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; + adapter.dataSource = self.dataSource; + adapter.collectionView = collectionView; + [collectionView layoutIfNeeded]; + + IGTestDelegateController *sectionToRemove = [adapter sectionControllerForObject:bar]; + + self.dataSource.objects = @[foo]; + + XCTestExpectation *expectation = genExpectation; + [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { + XCTAssertTrue(finished); + [expectation fulfill]; + }]; + + XCTestExpectation *expectation2 = genExpectation; + [sectionToRemove.collectionContext invalidateLayoutForSectionController:sectionToRemove completion:^(BOOL finished) { + // That section-controller is about to be removed, so this should not finish. + XCTAssertFalse(finished); + [expectation2 fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenPerformingBatchSectionUpdate_thatTransactionObjectsGetsDeallocated { + __weak IGListUpdateTransactionBuilder *transactionBuilder = nil; + __block __weak IGListUpdateTransactionBuilder *lastTransactionBuilder = nil; + __block __weak id transaction = nil; + + IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; + + @autoreleasepool { + [self setupWithObjects:@[ + genTestObject(@0, @"Foo") + ]]; + + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar") + ]; + + // Grab the current builder + transactionBuilder = [updater transactionBuilder]; + + [self.adapter performBatchAnimated:NO updates:^(id _Nonnull batchContext) { + // Take advantage of `performBatchAnimated` to grab the transaction, but we don't perform any changes. + lastTransactionBuilder = [updater lastTransactionBuilder]; + XCTAssertNotNil(lastTransactionBuilder); + transaction = [updater transaction]; + XCTAssertNotNil(transaction); + } completion:nil]; + + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + XCTAssertNil(transactionBuilder); + XCTAssertNil(lastTransactionBuilder); + XCTAssertNil(transaction); + [expectation fulfill]; + }); + }]; + + // Force the update to happen right away + [updater update]; + } + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +#pragma mark - Changing the collectionView/dataSource + +- (void)test_whenChangingDataSourceWithADifferentCount_thenPerformBatchUpdate_thatLastestDataIsApplied { + [self setupWithObjects:@[ + genTestObject(@0, @"Foo") + ]]; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: 1 section + + self.dataSource = [IGTestDelegateDataSource new]; + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar") + ]; + self.adapter.dataSource = self.dataSource; + + // STATE + // DataSource: 2 sections + // Adapter: 2 sections + // CollectionView: Invalidated count + + // Schedule update + XCTestExpectation *expectation2 = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + XCTAssertTrue(finished); + XCTAssertEqual([self.collectionView numberOfSections], 2); + XCTAssertEqual(self.adapter.objects.count, 2); + + // STATE + // DataSource: 2 sections + // Adapter: 2 sections + // CollectionView: 2 sections + + [expectation2 fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenChangingCollectionView_thenScheduleSectionUpdate_thatLastestDataIsApplied { + [self setupWithObjects:@[ + genTestObject(@0, @"Foo") + ]]; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: 1 section + + // Force dataSource <> adapater sync by changing the collection view + self.layout = [UICollectionViewFlowLayout new]; + self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame + collectionViewLayout:self.layout]; + self.adapter.collectionView = self.collectionView; + + // STATE + // DataSource: 1 sections + // Adapter: 1 sections + // CollectionView: Invalidated count + + XCTAssertEqual([self.collectionView numberOfSections], 1); + XCTAssertEqual(self.adapter.objects.count, 1); + + // STATE + // DataSource: 1 sections + // Adapter: 1 sections + // CollectionView: 1 sections +} + +- (void)test_settingCollectionViewAndDataSource_thatDontCreateCellsUntilLayout { + self.dataSource.objects = @[ + genTestObject(@0, @"Foo") + ]; + self.adapter.collectionView = self.collectionView; + self.adapter.dataSource = self.dataSource; + + // Make sure we didn't create the cells just yet, since we might want to scroll way without animating. + XCTAssertNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); + + [self.collectionView layoutIfNeeded]; + XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); +} + +#pragma mark - Changing the collectionView/dataSource with pending SECTION updates + +- (void)test_whenSchedulingSectionUpdate_thenChangeCollectionView_thatLastestDataIsApplied { + [self setupWithObjects:@[ + genTestObject(@0, @"Foo") + ]]; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: 1 section + + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar") + ]; + + // STATE + // DataSource: 2 sections + // Adapter: 1 section + // CollectionView: 1 section + + // Schedule update + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + + // STATE + // DataSource: 2 sections + // Adapter: 2 sections + // CollectionView: Invalidated count + + // Force collectionView <> adapter sync + XCTAssertEqual([self.collectionView numberOfSections], 2); + XCTAssertEqual(self.adapter.objects.count, 2); + XCTAssertTrue(finished); + + // STATE + // DataSource: 2 sections + // Adapter: 2 sections + // CollectionView: 2 sections + + [expectation fulfill]; + }]; + + // Force dataSource <> adapater sync by changing the collection view + self.layout = [UICollectionViewFlowLayout new]; + self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame + collectionViewLayout:self.layout]; + self.adapter.collectionView = self.collectionView; + + // Although all the syncs should have been checked by now, lets still make + // sure the counts are right. + XCTAssertEqual([self.collectionView numberOfSections], 2); + XCTAssertEqual(self.adapter.objects.count, 2); + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenSchedulingSectionUpdate_thenChangeTheDataSource_thatLastestDataIsApplied { + [self setupWithObjects:@[ + genTestObject(@0, @"Foo") + ]]; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: 1 section + + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar") + ]; + + // STATE + // DataSource: 2 section + // Adapter: 1 section + // CollectionView: 1 section + + // Schedule update + XCTestExpectation *expectation2 = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + // STATE + // DataSource: 3 sections + // Adapter: 3 sections + // CollectionView: Invalidated count + + XCTAssertTrue(finished); + XCTAssertEqual([self.collectionView numberOfSections], 3); + XCTAssertEqual(self.adapter.objects.count, 3); + + // STATE + // DataSource: 3 sections + // Adapter: 3 sections + // CollectionView: 3 sections + + [expectation2 fulfill]; + }]; + + // Force dataSource <> adapater sync by changing the dataSource + self.dataSource = [IGTestDelegateDataSource new]; + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar"), + genTestObject(@2, @"Baz") + ]; + self.adapter.dataSource = self.dataSource; + + // Although all the syncs should have been checked by now, lets still make + // sure the counts are right. + XCTAssertEqual([self.collectionView numberOfSections], 3); + XCTAssertEqual(self.adapter.objects.count, 3); + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +#pragma mark - Changing the collectionView/dataSource with pending ITEM updates + +- (void)test_whenSchedulingItemUpdate_thenChangeCollectionView_thatLastestDataIsApplied { + [self setupWithObjects:@[ + genTestObject(@0, @1) + ]]; + + // STATE + // Section Controller: 1 cell + // CollectionView: 1 cell + + IGTestDelegateController *contoller = (IGTestDelegateController *)[self.adapter sectionControllerForSection:0]; + XCTAssertNotNil(contoller); + XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); + + XCTestExpectation *expectation1 = genExpectation; + [contoller.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { + // Just change the item count for section 0 + contoller.item = genTestObject(@0, @2); + [batchContext insertInSectionController:contoller atIndexes:[NSIndexSet indexSetWithIndex:0]]; + } completion:^(BOOL finished) { + XCTAssertTrue(finished); + XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); + [expectation1 fulfill]; + }]; + + // Force dataSource <> adapater sync by changing the collection view + self.layout = [UICollectionViewFlowLayout new]; + self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame + collectionViewLayout:self.layout]; + self.adapter.collectionView = self.collectionView; + + // STATE + // Section Controller: 2 cells + // CollectionView: Invalidated count + + XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); + + // STATE + // Section Controller: 2 cells + // CollectionView: 2 cells + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenSchedulingItemUpdate_thenChangeDataSource_thatLastestDataIsApplied { + [self setupWithObjects:@[ + genTestObject(@0, @1) + ]]; + + // STATE + // Section Controller: 1 cell + // CollectionView: 1 cell + + IGTestDelegateController *contoller = (IGTestDelegateController *)[self.adapter sectionControllerForSection:0]; + XCTAssertNotNil(contoller); + XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); + + XCTestExpectation *expectation1 = genExpectation; + [contoller.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { + // Just change the item count for section 0 + contoller.item = genTestObject(@0, @2); + [batchContext insertInSectionController:contoller atIndexes:[NSIndexSet indexSetWithIndex:0]]; + } completion:^(BOOL finished) { + XCTAssertTrue(finished); + XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); + [expectation1 fulfill]; + }]; + + // Force dataSource <> adapater sync by changing the dataSource. + // Note that we keep the old object here, but that should not matter since + // it didn't change, it won't call -didUpdateToObject on that section-controller. + IGTestDelegateDataSource *oldDataSource = self.dataSource; + self.dataSource = [IGTestDelegateDataSource new]; + self.dataSource.objects = oldDataSource.objects; + self.adapter.dataSource = self.dataSource; + + // STATE + // Section Controller: 2 cells + // CollectionView: Invalidated count + + XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); + + // STATE + // Section Controller: 2 cells + // CollectionView: 2 cells + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +#pragma mark - Changing the collectionView/dataSource in middle of diffing + +- (void)test_whenSchedulingSectionUpdate_thenBeginDiffing_thenChangeCollectionView_thatLastestDataIsApplied { + IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; + updater.experiments |= IGListExperimentBackgroundDiffing; + + [self setupWithObjects:@[ + genTestObject(@0, @"Foo") + ]]; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: 1 section + + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar") + ]; + + // STATE + // DataSource: 2 sections + // Adapter: 1 section + // CollectionView: 1 section + + // Schedule update + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + // STATE + // DataSource: 2 sections + // Adapter: 2 sections + // CollectionView: Invalidated count + + XCTAssertTrue(finished); + XCTAssertEqual([self.collectionView numberOfSections], 2); + XCTAssertEqual(self.adapter.objects.count, 2); + + // STATE + // DataSource: 2 sections + // Adapter: 2 sections + // CollectionView: 2 sections + + [expectation fulfill]; + }]; + + // Force the update to happen right way, so that the diffing starts + [updater update]; + + // Force dataSource <> adapater sync by changing the collection view + self.layout = [UICollectionViewFlowLayout new]; + self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame + collectionViewLayout:self.layout]; + self.adapter.collectionView = self.collectionView; + + // Although all the syncs should have been checked by now, lets still make + // sure the counts are right. + XCTAssertEqual([self.collectionView numberOfSections], 2); + XCTAssertEqual(self.adapter.objects.count, 2); + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenSchedulingSectionUpdate_thenBeginDiffing_thenChangeTheDataSource_thatLastestDataIsApplied { + IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; + updater.experiments |= IGListExperimentBackgroundDiffing; + + [self setupWithObjects:@[ + genTestObject(@0, @"Foo") + ]]; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: 1 section + + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar") + ]; + + // STATE + // DataSource: 2 sections + // Adapter: 1 section + // CollectionView: 1 section + + // Schedule update + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + // STATE + // DataSource: 3 sections + // Adapter: 3 sections + // CollectionView: Invalidated count + + XCTAssertTrue(finished); + XCTAssertEqual([self.collectionView numberOfSections], 3); + XCTAssertEqual(self.adapter.objects.count, 3); + + // STATE + // DataSource: 3 sections + // Adapter: 3 sections + // CollectionView: 3 sections + + [expectation fulfill]; + }]; + + // Force the update to happen right way, so that the diffing starts + [updater update]; + + // Force dataSource <> adapater sync by changing the dataSource + self.dataSource = [IGTestDelegateDataSource new]; + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar"), + genTestObject(@2, @"Baz") + ]; + self.adapter.dataSource = self.dataSource; + + // Although all the syncs should have been checked by now, lets still make + // sure the counts are right. + XCTAssertEqual([self.collectionView numberOfSections], 3); + XCTAssertEqual(self.adapter.objects.count, 3); + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +#pragma mark - Sync the collectionView before setting a adapter.dataSource + +- (void)test_whenCollectionViewSyncsBeforeTheAdapterDataSourceIsSet_thatLastestDataIsApplied { + self.adapter.collectionView = self.collectionView; + + // Force the adapter <> collectionView to sync + XCTAssertEqual([self.collectionView numberOfSections], 0); + XCTAssertEqual([self.adapter objects].count, 0); + + // STATE + // DataSource: Nil + // Adapter: 0 sections + // CollectionView: 0 sections + + // Changing the `adapter.dataSource` will sync the adapter <> dataSource, and + // invalidate the collectionView's internal section/item counts. + self.dataSource.objects = @[genTestObject(@1, @"Foo")]; + self.adapter.dataSource = self.dataSource; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: Invalidated counts (UICollectionView will ask for counts on next layout) + + XCTAssertEqual([self.collectionView numberOfSections], 1); + XCTAssertEqual([self.adapter objects].count, 1); + + // Test that collectionView syncs with the adapter + [self.collectionView layoutIfNeeded]; + XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: 1 section +} + +- (void)test_whenCollectionViewSyncsBeforeTheAdapterDataSourceIsSet_thenSchedulingSectionUpdate_thatLastestDataIsApplied { + self.adapter.collectionView = self.collectionView; + + // Force the adapter <> collectionView to sync + XCTAssertEqual([self.collectionView numberOfSections], 0); + XCTAssertEqual([self.adapter objects].count, 0); + + // STATE + // DataSource: Nil + // Adapter: 0 sections + // CollectionView: 0 sections + + // Changing the `adapter.dataSource` will sync the adapter <> dataSource, and + // invalidate the collectionView's internal section/item counts. + self.dataSource.objects = @[genTestObject(@0, @"Foo")]; + self.adapter.dataSource = self.dataSource; + + // STATE + // DataSource: 1 section + // Adapter: 1 section + // CollectionView: Invalidated counts (UICollectionView will ask for counts on next layout) + + XCTAssertEqual([self.adapter objects].count, 1); + + // Adding an object + self.dataSource.objects = @[ + genTestObject(@0, @"Foo"), + genTestObject(@1, @"Bar"), + ]; + + // STATE + // DataSource: 2 sections + // Adapter: 1 section + // CollectionView: Invalidated counts (Still) + + // Test that a batchUpdate from 1 -> 2 objects works, even though + // the collectionView has not synced yet. + XCTestExpectation *expectation = genExpectation; + [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { + // Checked that the update worked + XCTAssertTrue(finished); + // Check that the we have the correct counts + XCTAssertEqual([self.collectionView numberOfSections], 2); + XCTAssertEqual(self.adapter.objects.count, 2); + [expectation fulfill]; + + // STATE + // DataSource: 2 sections + // Adapter: 2 section + // CollectionView: 2 sections + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + @end diff --git a/Tests/IGListAdapterUpdaterTests.m b/Tests/IGListAdapterUpdaterTests.m index 3947522ae..1cdb65c55 100644 --- a/Tests/IGListAdapterUpdaterTests.m +++ b/Tests/IGListAdapterUpdaterTests.m @@ -15,6 +15,7 @@ #import "IGListAdapterUpdaterInternal.h" #import "IGListMoveIndexInternal.h" #import "IGListTestUICollectionViewDataSource.h" +#import "IGListTransitionData.h" #import "IGTestObject.h" #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] @@ -27,7 +28,7 @@ @interface IGListAdapterUpdaterTests : XCTestCase @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListTestUICollectionViewDataSource *dataSource; @property (nonatomic, strong) IGListAdapterUpdater *updater; -@property (nonatomic, strong) IGListObjectTransitionBlock updateBlock; +@property (nonatomic, strong) IGListTransitionDataApplyBlock applyDataBlock; @end @@ -37,6 +38,12 @@ - (IGListCollectionViewBlock)collectionViewBlock { return ^UICollectionView *{ return self.collectionView; }; } +- (IGListTransitionDataBlock)dataBlockFromObjects:(NSArray *)fromObjects toObjects:(NSArray *)toObjects { + return ^IGListTransitionData *{ + return [[IGListTransitionData alloc] initFromObjects:fromObjects toObjects:toObjects toSectionControllers:@[]]; + }; +} + - (void)setUp { [super setUp]; @@ -48,10 +55,10 @@ - (void)setUp { [self.window addSubview:self.collectionView]; self.dataSource = [[IGListTestUICollectionViewDataSource alloc] initWithCollectionView:self.collectionView]; - self.updater = [[IGListAdapterUpdater alloc] init]; + self.updater = [IGListAdapterUpdater new]; __weak __typeof__(self) weakSelf = self; - self.updateBlock = ^(NSArray *obj) { - weakSelf.dataSource.sections = obj; + self.applyDataBlock = ^(IGListTransitionData *data) { + weakSelf.dataSource.sections = data.toObjects; }; } @@ -63,70 +70,77 @@ - (void)tearDown { self.updater = nil; } -- (void)test_whenUpdatingWithNil_thatUpdaterHasNoChanges { - [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 performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:nil toObjectsBlock:^NSArray *{return @[@0];} animated:YES objectTransitionBlock:self.updateBlock completion:nil]; + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:@[] toObjects:@[@0]] + applyDataBlock:self.applyDataBlock + completion:nil]; XCTAssertTrue([self.updater hasChanges]); } - (void)test_whenUpdatingfromObjects_thatUpdaterHasChanges { - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:@[@0] toObjectsBlock:^NSArray *{return nil;} animated:YES objectTransitionBlock:self.updateBlock completion:nil]; + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:@[@0] toObjects:@[]] + applyDataBlock:self.applyDataBlock + completion:nil]; XCTAssertTrue([self.updater hasChanges]); } - (void)test_whenUpdatingtoObjects_withfromObjects_thatUpdaterHasChanges { - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:@[@0] toObjectsBlock:^NSArray *{return @[@1];} animated:YES objectTransitionBlock:self.updateBlock completion:nil]; + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:@[@0] toObjects:@[@1]] + applyDataBlock:self.applyDataBlock + completion:nil]; XCTAssertTrue([self.updater hasChanges]); } -- (void)test_whenCleaningUpState_withChanges_thatUpdaterHasNoChanges { - [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]); -} - - (void)test_whenReloadingData_thatCollectionViewUpdates { self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[]]]; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); self.dataSource.sections = @[]; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 0); } - (void)test_whenReloadingDataWithNilDataSourceBefore_thatCollectionViewNotCrash { self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[@1]], [IGSectionObject sectionWithObjects:@[@2]]]; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); self.collectionView.dataSource = nil; self.dataSource.sections = @[]; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); // Setting collectionView's dataSource to nil would yield a single section by default. } - (void)test_whenInsertingSection_thatCollectionViewUpdates { NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - }; + [IGSectionObject sectionWithObjects:@[]] + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]] + ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; @@ -135,21 +149,24 @@ - (void)test_whenInsertingSection_thatCollectionViewUpdates { - (void)test_whenDeletingSection_thatCollectionViewUpdates { NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - }; + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]] + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[]] + ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 1); [expectation fulfill]; }]; @@ -158,22 +175,25 @@ - (void)test_whenDeletingSection_thatCollectionViewUpdates { - (void)test_whenInsertingSection_withItemChanges_thatCollectionViewUpdates { NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[@0]] - ]; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[@0, @1]], - [IGSectionObject sectionWithObjects:@[@0, @1]] - ]; - }; + [IGSectionObject sectionWithObjects:@[@0]] + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[@0, @1]], + [IGSectionObject sectionWithObjects:@[@0, @1]] + ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); @@ -184,25 +204,28 @@ - (void)test_whenInsertingSection_withItemChanges_thatCollectionViewUpdates { - (void)test_whenInsertingSection_withDeletedSection_thatCollectionViewUpdates { NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[@0, @1, @2]], - [IGSectionObject sectionWithObjects:@[]] - ]; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[@1, @1]], - [IGSectionObject sectionWithObjects:@[@0]], - [IGSectionObject sectionWithObjects:@[@0, @2, @3]] - ]; - }; + [IGSectionObject sectionWithObjects:@[@0, @1, @2]], + [IGSectionObject sectionWithObjects:@[]] + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[@1, @1]], + [IGSectionObject sectionWithObjects:@[@0]], + [IGSectionObject sectionWithObjects:@[@0, @2, @3]] + ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); @@ -217,7 +240,8 @@ - (void)test_whenReloadingSections_thatCollectionViewUpdates { [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); @@ -235,17 +259,16 @@ - (void)test_whenReloadingSections_thatCollectionViewUpdates { - (void)test_whenCollectionViewNeedsLayout_thatPerformBatchUpdateWorks { NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - }; + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]] + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[]] + ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; // the collection view has been setup with 1 section and now needs layout // calling performBatchUpdates: on a collection view needing layout will force layout @@ -253,7 +276,11 @@ - (void)test_whenCollectionViewNeedsLayout_thatPerformBatchUpdateWorks { [self.collectionView setNeedsLayout]; XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 1); [expectation fulfill]; }]; @@ -262,30 +289,31 @@ - (void)test_whenCollectionViewNeedsLayout_thatPerformBatchUpdateWorks { - (void)test_whenUpdatesAreReentrant_thatUpdatesExecuteSerially { NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - ]; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - ]; - }; + [IGSectionObject sectionWithObjects:@[]], + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]], + ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; __block NSInteger completionCounter = 0; XCTestExpectation *expectation1 = genExpectation; void (^preUpdateBlock)(void) = ^{ - NSArray *(^anotherTo)(void) = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - }; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:anotherTo animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + NSArray *anotherTo = @[ + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]] + ]; + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:to toObjects:anotherTo] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { completionCounter++; XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual(completionCounter, 2); @@ -294,14 +322,18 @@ - (void)test_whenUpdatesAreReentrant_thatUpdatesExecuteSerially { }; XCTestExpectation *expectation2 = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:^(NSArray *toObjects) { + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:^(IGListTransitionData *data) { // 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 preUpdateBlock(); - self.dataSource.sections = toObjects; - } completion:^(BOOL finished) { + self.dataSource.sections = data.toObjects; + } + completion:^(BOOL finished) { completionCounter++; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(completionCounter, 1); @@ -328,14 +360,14 @@ - (void)test_whenQueueingItemUpdates_withBatchUpdate_thatItemUpdateBlockExecutes __block BOOL itemUpdateBlockExecuted = NO; __block BOOL sectionUpdateBlockExecuted = NO; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] - fromObjects:nil - toObjectsBlock:^NSArray *{return @[[IGSectionObject sectionWithObjects:@[@1]]];} - animated:YES objectTransitionBlock:^(NSArray * toObjects) { - self.dataSource.sections = toObjects; - sectionUpdateBlockExecuted = YES; - } - completion:nil]; + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:@[] toObjects:@[[IGSectionObject sectionWithObjects:@[@1]]]] + applyDataBlock:^(IGListTransitionData * data) { + self.dataSource.sections = data.toObjects; + sectionUpdateBlockExecuted = YES; + } + completion:nil]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{ @@ -353,10 +385,10 @@ - (void)test_whenQueueingItemUpdates_withBatchUpdate_thatItemUpdateBlockExecutes - (void)test_whenItemsMoveAndUpdate_thatCollectionViewWorks { NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - ]; + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]], + ]; // change the number of items in the section, which a move would be unable to handle and would throw // keep the same pointers so that the objects are equal @@ -364,23 +396,25 @@ - (void)test_whenItemsMoveAndUpdate_thatCollectionViewWorks { [from[0] setObjects:@[@1, @1]]; [from[1] setObjects:@[@1, @1, @1]]; - IGListToObjectBlock to = ^NSArray *{ - // rearrange the modified objects - return @[ - from[2], - from[0], - from[1] - ]; - }; + NSArray *to = @[ + from[2], + from[0], + from[1] + ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; // without moves as inserts, we would assert b/c the # of items in each section changes self.updater.sectionMovesAsDeletesInserts = YES; XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:to animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); @@ -440,7 +474,8 @@ - (void)test_whenReloadingData_withNilCollectionView_thatDelegateFinishesWithout self.updater.delegate = mockDelegate; id compilerFriendlyNil = nil; [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:nil]; - [self.updater performReloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; }]; + [self.updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; } reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; [mockDelegate verify]; } @@ -449,7 +484,8 @@ - (void)test_whenPerformingUpdates_withNilCollectionView_thatDelegateFinishesWit self.updater.delegate = mockDelegate; id compilerFriendlyNil = nil; [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:nil]; - [self.updater performBatchUpdatesWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; }]; + [self.updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; } reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; [mockDelegate verify]; } @@ -478,7 +514,8 @@ - (void)test_whenCallingReloadData_withUICollectionViewFlowLayout_withEstimatedS ]; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; - [updater performReloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; }]; + [updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; } reloadUpdateBlock:^{} completion:nil]; + [updater update]; XCTAssertEqual([collectionView numberOfSections], 1); XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); @@ -487,7 +524,8 @@ - (void)test_whenCallingReloadData_withUICollectionViewFlowLayout_withEstimatedS [IGSectionObject sectionWithObjects:@[@1]], [IGSectionObject sectionWithObjects:@[@1, @2, @3, @4]] ]; - [updater performReloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; }]; + [updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; } reloadUpdateBlock:^{} completion:nil]; + [updater update]; XCTAssertEqual([collectionView numberOfSections], 2); XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); @@ -508,7 +546,11 @@ - (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isSetNO_diffH [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:OCMOCK_ANY collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:self.dataSource.sections toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -526,12 +568,14 @@ - (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isDefaultYES_ [[mockDelegate expect] listAdapterUpdater:self.updater didReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; XCTestExpectation *expectation = genExpectation; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - }; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:self.dataSource.sections toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[]] + ]; + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -549,13 +593,15 @@ - (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isDefaultYES_ [[mockDelegate expect] listAdapterUpdater:self.updater didReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; XCTestExpectation *expectation = genExpectation; - IGListToObjectBlock to = ^NSArray *{ - return @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - }; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[]] + ]; self.collectionView.dataSource = nil; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:self.dataSource.sections toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -572,7 +618,8 @@ - (void)test_whenReloadBatchedWithUpdate_thatCompletionBlockStillExecuted { }]; XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] 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], @@ -592,6 +639,56 @@ - (void)test_whenReloadBatchedWithUpdate_thatCompletionBlockStillExecuted { [self waitForExpectationsWithTimeout:30 handler:nil]; } +- (void)test_whenPerformingItemUpdateInMiddleOfReload_thatCompletionBlockStillExecuted { + IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; + self.dataSource.sections = @[object]; + + XCTestExpectation *expectation = genExpectation; + + // Section-controllers can schedule item updates in -didUpdateToObject, so lets make sure the completion block works. + IGListReloadUpdateBlock reloadUpdateBlock = ^{ + [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] + animated:YES + itemUpdates:^{} + completion:^(BOOL finished) { + [expectation fulfill]; + }]; + }; + + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] + reloadUpdateBlock:reloadUpdateBlock + completion:nil]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenPerformingItemUpdateInMiddleOfUpdate_thatCompletionBlockStillExecuted { + IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; + self.dataSource.sections = @[object]; + + XCTestExpectation *expectation = genExpectation; + + // Section-controllers can schedule item updates in -didUpdateToObject, so lets make sure the completion block works. + void (^updateItemBlock)(void) = ^{ + [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] + animated:YES + itemUpdates:^{} + completion:^(BOOL finished) { + [expectation fulfill]; + }]; + }; + + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:self.dataSource.sections] + applyDataBlock:^(IGListTransitionData * _Nonnull data) { + updateItemBlock(); + } + completion:nil]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + - (void)test_whenNotInViewHierarchy_thatUpdatesStillExecuteBlocks { [self.collectionView removeFromSuperview]; @@ -600,16 +697,15 @@ - (void)test_whenNotInViewHierarchy_thatUpdatesStillExecuteBlocks { __block BOOL objectTransitionBlockExecuted = NO; __block BOOL completionBlockExecuted = NO; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] - fromObjects:self.dataSource.sections - toObjectsBlock:^NSArray *{return self.dataSource.sections;} - animated:YES - objectTransitionBlock:^(NSArray *toObjects) { - objectTransitionBlockExecuted = YES; - } - completion:^(BOOL finished) { - completionBlockExecuted = YES; - }]; + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:self.dataSource.sections] + applyDataBlock:^(IGListTransitionData *data) { + objectTransitionBlockExecuted = YES; + } + completion:^(BOOL finished) { + completionBlockExecuted = YES; + }]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{ @@ -712,7 +808,11 @@ - (void)test_whenReloadIsCalledWithSameItemCount_deleteInsertSectionHappen { XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -734,8 +834,12 @@ - (void)test_whenPerformUpdates_dataSourceWasSetToNil_shouldNotCrash { // Manually set the data source to be nil. self->_collectionView.dataSource = nil; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:^(NSArray * _Nonnull toObjects) { - } completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:^(IGListTransitionData *data) { + } + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -785,23 +889,26 @@ - (void)test_whenPerformingUpdatesMultipleTimesInARow_thenUpdateWorks { [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; - IGListToObjectBlock toObjectsBlock2 = ^NSArray *{ - return objects2; - }; - IGListToObjectBlock toObjectsBlock3 = ^NSArray *{ - return objects3; - }; self.dataSource.sections = objects1; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:objects1 toObjectsBlock:toObjectsBlock2 animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:objects1 toObjects:objects2] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:objects2 toObjectsBlock:toObjectsBlock3 animated:YES objectTransitionBlock:self.updateBlock completion:^(BOOL finished2) { + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:objects2 toObjects:objects3] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); @@ -822,12 +929,10 @@ - (void)test_whenPerformingUpdate_thatCallsDiffingDelegate { [IGSectionObject sectionWithObjects:@[] identifier:@"0"], [IGSectionObject sectionWithObjects:@[] identifier:@"1"] ]; - IGListToObjectBlock toBlock = ^NSArray *{ - return to; - }; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; @@ -842,13 +947,99 @@ - (void)test_whenPerformingUpdate_thatCallsDiffingDelegate { XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:toBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } +- (void)test_whenCollectionViewSectionCountIsIncorrect_thatDoesNotCrash { + NSArray *from = @[ + [IGSectionObject sectionWithObjects:@[]] + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]] + ]; + + self.dataSource.sections = from; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; + XCTAssertEqual([self.collectionView numberOfSections], 1); + + XCTestExpectation *expectation = genExpectation; + [self.updater performExperimentalUpdateAnimated:YES + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { + XCTAssertEqual([self.collectionView numberOfSections], 2); + [expectation fulfill]; + }]; + + // Lets say we change the dataSource without the updater on accident. + self.dataSource.sections = @[ + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]], + [IGSectionObject sectionWithObjects:@[]] + ]; + + // Lets force the collectionView to sync + [self.collectionView reloadData]; + [self.collectionView layoutIfNeeded]; + XCTAssertEqual([self.collectionView numberOfSections], 3); + + // No we wait for the update, which should fallback to a reload. + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)test_whenNoChanges_thatPerformUpdateExitsEarly { + self.updater.experiments |= IGListExperimentSkipPerformUpdateIfPossible; + + NSArray *from = @[ + [IGSectionObject sectionWithObjects:@[] identifier:@"Foo"] + ]; + NSArray *to = @[ + [IGSectionObject sectionWithObjects:@[] identifier:@"Foo"] + ]; + + self.dataSource.sections = from; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; + self.updater.delegate = mockDelegate; + [mockDelegate setExpectationOrderMatters:YES]; + + [[mockDelegate expect] listAdapterUpdater:self.updater + willPerformBatchUpdatesWithCollectionView:self.collectionView + fromObjects:from + toObjects:to + listIndexSetResult:OCMOCK_ANY + animated:NO]; + + [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:self.collectionView]; + + XCTestExpectation *expectation = genExpectation; + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { + XCTAssertTrue(finished); + XCTAssertEqual([self.collectionView numberOfSections], 1); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; + [mockDelegate verify]; +} # pragma mark - preferItemReloadsFroSectionReloads @@ -876,7 +1067,11 @@ - (void)test_whenReloadIsCalledWithSameItemCount_andPreferItemReload_updateIndex XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -908,7 +1103,11 @@ - (void)test_whenReloadIsCalledWithDifferentItemCount_andPreferItemReload_delete XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -946,7 +1145,11 @@ - (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_andPreferItemReload_dele XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -984,7 +1187,11 @@ - (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_withDifferentSectionLeng XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -1024,7 +1231,11 @@ - (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_withThreeSections_delete XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; @@ -1059,7 +1270,11 @@ - (void)test_whenReloadIsCalledWithSectionInsertAndUpdate_andPreferItemReload_no XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [self.updater performExperimentalUpdateAnimated:NO + collectionViewBlock:[self collectionViewBlock] + dataBlock:[self dataBlockFromObjects:from toObjects:to] + applyDataBlock:self.applyDataBlock + completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; diff --git a/Tests/IGListDebuggerTests.m b/Tests/IGListDebuggerTests.m index bcac7619d..938930abe 100644 --- a/Tests/IGListDebuggerTests.m +++ b/Tests/IGListDebuggerTests.m @@ -27,15 +27,7 @@ - (void)test_whenSearchingAdapterInstances_thatCorrectCountReturned { UIViewController *controller = [UIViewController new]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; - IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; - NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; - updater.applyingUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:1] - deleteSections:[NSIndexSet indexSetWithIndex:2] - moveSections:[NSSet setWithObject:[[IGListMoveIndex alloc] initWithFrom:3 to:4]] - insertIndexPaths:@[path] - deleteIndexPaths:@[path] - updateIndexPaths:@[] - moveIndexPaths:@[[[IGListMoveIndexPath alloc] initWithFrom:path to:path]]]; + IGListTestAdapterDataSource *dataSource = [IGListTestAdapterDataSource new]; dataSource.objects = @[@1, @2, @3]; IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0]; diff --git a/Tests/IGListExperimentalAdapterE2ETests.m b/Tests/IGListExperimentalAdapterE2ETests.m deleted file mode 100644 index 71472e8b8..000000000 --- a/Tests/IGListExperimentalAdapterE2ETests.m +++ /dev/null @@ -1,2596 +0,0 @@ -/* - * 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 - -#import - -#import "IGListAdapterInternal.h" -#import "IGListAdapterUpdateTester.h" -#import "IGListExperimentalAdapterUpdater.h" -#import "IGListExperimentalAdapterUpdaterInternal.h" -#import "IGListTestCase.h" -#import "IGListTestHelpers.h" -#import "IGListTestOffsettingLayout.h" -#import "IGTestCell.h" -#import "IGTestDelegateController.h" -#import "IGTestDelegateDataSource.h" -#import "IGTestObject.h" - -/// Equivalent of `IGListAdapterE2ETests` to test the new `IGListExperimentalAdapterUpdater` -@interface IGListExperimentalAdapterE2ETests : IGListTestCase -@end - -@implementation IGListExperimentalAdapterE2ETests - -- (void)setUp { - self.workingRangeSize = 2; - self.dataSource = [IGTestDelegateDataSource new]; - self.updater = [IGListExperimentalAdapterUpdater new]; - [super setUp]; -} - -- (void)test_whenSettingUpTest_thenCollectionViewIsLoaded { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @3) - ]]; - XCTAssertEqual(self.collectionView.numberOfSections, 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); -} - -- (void)test_whenUsingStringValue_thenCellLabelsAreConfigured { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]]; - - IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; - XCTAssertEqualObjects(cell.label.text, @"Foo"); - XCTAssertEqual(cell.delegate, [self.adapter sectionControllerForObject:self.dataSource.objects[0]]); -} - -- (void)test_whenUpdating_withEqualObjects_thatCellConfigurationDoesntChange { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]]; - - // Get the section controller before we change the data source or perform updates - id c0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - - // Set equal but new-instance objects on the data source - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]; - - // Perform updates on the adapter and check that the cell config uses the same section controller as before the updates - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; - XCTAssertEqualObjects(cell.label.text, @"Foo"); - XCTAssertNotNil(cell.delegate); - XCTAssertEqual(cell.delegate, c0); - XCTAssertEqual(cell.delegate, [self.adapter sectionControllerForObject:self.dataSource.objects[0]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadingItem_cellConfigurationChanges { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]]; - - // make sure our cells are propertly configured - IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; - IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1, 0)]; - XCTAssertEqualObjects(cell1.label.text, @"Foo"); - XCTAssertEqualObjects(cell2.label.text, @"Bar"); - - // Change the string value of both instances in the data source - IGTestObject *item1 = self.dataSource.objects[0]; - item1.value = @"Baz"; - IGTestObject *item2 = self.dataSource.objects[1]; - item2.value = @"Quz"; - - // Only reload the first item, not the second - [self.adapter reloadObjects:@[item1]]; - - // The collection view will likely create new cells - cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; - cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1, 0)]; - - // Make sure that the cell in the first section was reloaded - XCTAssertEqualObjects(cell1.label.text, @"Baz"); - // The cell in the second section should not be reloaded and should equal the string value from setup - XCTAssertEqualObjects(cell2.label.text, @"Bar"); -} - -- (void)test_whenObjectEqualityChanges_thatSectionCountChanges { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - - self.dataSource.objects = @[ - genTestObject(@1, @2), - genTestObject(@2, @3), // updated to 3 items (from 2) - genTestObject(@3, @2), // insert new object - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - XCTAssertEqual(self.collectionView.numberOfSections, 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenUpdatesComplete_thatCellsExist { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - ]]; - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]); - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:1]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadDataCompletes_thatCellsExist { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - XCTestExpectation *expectation = genExpectation; - [self.adapter reloadDataWithCompletion:^(BOOL finished) { - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]); - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:1]]); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerInsertsIndexes_thatCountsAreUpdated { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerDeletesIndexes_thatCountsAreUpdated { - // 2 sections each with 2 objects - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @1; - [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerReloadsIndexes_thatCellConfigurationUpdates { - [self setupWithObjects:@[ - genTestObject(@1, @"a"), - genTestObject(@2, @"b") - ]]; - XCTAssertEqual([self.collectionView numberOfSections], 2); - IGTestCell *cell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - XCTAssertEqualObjects(cell.label.text, @"a"); - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @"c"; - [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - IGTestCell *updatedCell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - XCTAssertEqualObjects(updatedCell.label.text, @"c"); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerReloads_thatCountsAreUpdated { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext reloadSectionController:sectionController]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerReloads_withPreferItemReload_thatCountsAreUpdated { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - // Prefer to use item reloads for section reloads if available. - [(IGListExperimentalAdapterUpdater *)self.adapter.updater setPreferItemReloadsForSectionReloads:YES]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext reloadSectionController:sectionController]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withSectionControllerMutations_thatCollectionCountsAreUpdated { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - IGTestObject *object1 = self.dataSource.objects[0]; - IGTestObject *object2 = self.dataSource.objects[1]; - - // insert a new object in front of the one we are doing an item-level insert on - self.dataSource.objects = @[ - genTestObject(@3, @1), // new - object1, - object2, - ]; - - IGListSectionController *sectionController1 = [self.adapter sectionControllerForObject:object1]; - IGListSectionController *sectionController2 = [self.adapter sectionControllerForObject:object2]; - - [self.adapter performUpdatesAnimated:YES completion:nil]; - - XCTestExpectation *expectation = genExpectation; - [sectionController1.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object1.value = @1; - object2.value = @3; - [batchContext deleteInSectionController:sectionController1 atIndexes:[NSIndexSet indexSetWithIndex:0]]; - [batchContext insertInSectionController:sectionController2 atIndexes:[NSIndexSet indexSetWithIndex:2]]; - [batchContext reloadInSectionController:sectionController2 atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - // 3 sections now b/c of the insert - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerMoves_withSectionControllerMutations_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - self.dataSource.objects = @[ - genTestObject(@2, @2), - object, // moved from 0 to 1 - ]; - - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - // queue the update that performs the section move - [self.adapter performUpdatesAnimated:YES completion:nil]; - - XCTestExpectation *expectation = genExpectation; - - // queue an item update that gets batched with the section move - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - // the object we are tracking should now be in section 1 and have 3 items - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenItemIsRemoved_withSectionControllerMutations_thatCollectionViewWorks { - // 2 sections each with 2 objects - [self setupWithObjects:@[ - genTestObject(@2, @2), - genTestObject(@1, @2) - ]]; - IGTestObject *object = self.dataSource.objects[1]; - - // object at index 1 deleted - self.dataSource.objects = @[ - genTestObject(@2, @2), - ]; - - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - [self.adapter performUpdatesAnimated:YES completion:nil]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @1; - [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 1); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withUnequalItem_withItemMoving_thatCollectionViewCountsUpdate { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - ]]; - - self.dataSource.objects = @[ - genTestObject(@3, @2), - genTestObject(@1, @3), // moved from index 0 to 1, value changed from 2 to 3 - genTestObject(@2, @2), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withItemMoving_withSectionControllerReloadIndexes_thatCollectionViewCountsUpdate { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @3), - ]]; - - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - - self.dataSource.objects = @[ - genTestObject(@2, @3), - genTestObject(@1, @2), // moved from index 0 to 1 - ]; - - [self.adapter performUpdatesAnimated:YES completion:nil]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withSectionControllerReloadIndexes_withItemDeleted_thatCollectionViewCountsUpdate { - [self setupWithObjects:@[ - genTestObject(@1, @2), // item that will be deleted - genTestObject(@2, @3), - ]]; - - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - - self.dataSource.objects = @[ - genTestObject(@2, @3), - ]; - - [self.adapter performUpdatesAnimated:YES completion:nil]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withNewItemInstances_thatSectionControllersEqual { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2) - ]]; - - // grab section controllers before updating the objects - NSArray *beforeupdateObjects = self.dataSource.objects; - IGListSectionController *sectionController1 = [self.adapter sectionControllerForObject:beforeupdateObjects.firstObject]; - IGListSectionController *sectionController2 = [self.adapter sectionControllerForObject:beforeupdateObjects.lastObject]; - - self.dataSource.objects = @[ - genTestObject(@1, @3), // new instance, value changed from 2 to 3 - genTestObject(@2, @2), // new instance but unchanged - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - - NSArray *afterupdateObjects = [self.adapter objects]; - // pointer equality - XCTAssertEqual([self.adapter sectionControllerForObject:afterupdateObjects.firstObject], sectionController1); - XCTAssertEqual([self.adapter sectionControllerForObject:afterupdateObjects.lastObject], sectionController2); - - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingMultipleUpdates_withNewItemInstances_thatSectionControllersReceiveNewInstances { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - ]]; - - id object = self.dataSource.objects[0]; - IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; - - // test delegate controller counts the number of times it receives -didUpdateToItem: - XCTAssertEqual(sectionController.updateCount, 1); - - self.dataSource.objects = @[ - object, // same object instance - genTestObject(@3, @2), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - XCTAssertEqual(sectionController, [self.adapter sectionControllerForObject:[self.adapter objects][0]]); - - // should not have received -didUpdateToItem: since the instance did not change - XCTAssertEqual(sectionController.updateCount, 1); - - self.dataSource.objects = @[ - genTestObject(@1, @2), // new instance but equal - genTestObject(@3, @2), - ]; - - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished3) { - XCTAssertEqual(sectionController, [self.adapter sectionControllerForObject:[self.adapter objects][0]]); - - // a new instance was used, make sure the section controller was updated - XCTAssertEqual(sectionController.updateCount, 2); - - [expectation fulfill]; - }]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenQueryingCollectionContext_withNewItemInstances_thatSectionMatchesCurrentIndex { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - ]]; - - IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - self.dataSource.objects = @[ - genTestObject(@2, @2), - genTestObject(@1, @2), // new instance but equal - genTestObject(@3, @2), - ]; - - __block BOOL executedUpdateBlock = NO; - __weak __typeof__(sectionController) weakSectionController = sectionController; - sectionController.itemUpdateBlock = ^{ - executedUpdateBlock = YES; - XCTAssertEqual(weakSectionController.section, 1); - }; - - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished3) { - XCTAssertTrue(executedUpdateBlock); - - [expectation fulfill]; - }]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerMutates_withReloadData_thatSectionControllerMutationIsApplied { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - ]]; - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]]; - } completion:nil]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter reloadDataWithCompletion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - - // check that the count of items in section 0 was updated from the previous batch update block - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenContentOffsetChanges_withPerformUpdates_thatCollectionViewWorks { - // this test layout changes the offset in -prepareLayout which occurs somewhere between the update block being - // applied and the completion block - self.collectionView.collectionViewLayout = [IGListTestOffsettingLayout new]; - - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - genTestObject(@3, @2), - ]]; - - // remove the last object to check that we don't access OOB section controller when the layout changes the offset - self.dataSource.objects = @[ - genTestObject(@1, @2), - genTestObject(@2, @2), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadingItems_withNewItemInstances_thatSectionControllersReceiveNewInstances { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - genTestObject(@3, @2), - ]]; - - IGTestDelegateController *sectionController1 = [self.adapter sectionControllerForObject:genTestObject(@1, @2)]; - IGTestDelegateController *sectionController2 = [self.adapter sectionControllerForObject:genTestObject(@2, @2)]; - - NSArray *newObjects = @[ - genTestObject(@1, @3), - genTestObject(@2, @3), - ]; - [self.adapter reloadObjects:newObjects]; - - XCTAssertEqual(sectionController1.item, newObjects[0]); - XCTAssertEqual(sectionController2.item, newObjects[1]); - XCTAssertTrue([[self.adapter.sectionMap objects] indexOfObjectIdenticalTo:newObjects[0]] != NSNotFound); - XCTAssertTrue([[self.adapter.sectionMap objects] indexOfObjectIdenticalTo:newObjects[1]] != NSNotFound); -} - -- (void)test_whenReloadingItems_withPerformUpdates_thatReloadIsApplied { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @2), - genTestObject(@3, @3), - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; - - // using performBatchAnimated: to mimic re-entrant item reload - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @4; // from @1 - [self.adapter reloadObjects:@[object]]; - } completion:nil]; - - // object is moved from position 0 to 1 - // it is also mutated in the previous update block AND queued for a reload - self.dataSource.objects = @[ - genTestObject(@3, @3), - object, - genTestObject(@2, @2), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); // reloaded section - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSectionControllerMutates_whenThereIsNoWindow_thatCollectionViewCountsAreUpdated { - // remove the collection view from self.window so that we use reloadData - [self.collectionView removeFromSuperview]; - - [self setupWithObjects:@[ - genTestObject(@1, @8) - ]]; - IGTestObject *object = self.dataSource.objects[0]; - - IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - // using performBatchAnimated: to mimic re-entrant item reload - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @6; // from @1 - - [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(5, 3)]]; - [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 6); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withoutSettingDataSource_thatCompletionBlockExecutes { - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListExperimentalAdapterUpdater new] viewController:nil]; - adapter.collectionView = collectionView; - - self.dataSource.objects = @[ - genTestObject(@1, @1) - ]; - - XCTestExpectation *expectation = genExpectation; - - // call -performUpdatesAnimated: before we have set the data source - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - - // since the data source isnt set, we complete syncronously. dispatch_async simulates setting the data source - // in a different runloop from the completion block so it should be set by the time we make our subsequent - // -performUpdatesAnimated: call - dispatch_async(dispatch_get_main_queue(), ^{ - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @2) - ]; - [adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - XCTAssertEqual([collectionView numberOfSections], 2); - [expectation fulfill]; - }]; - }); - }]; - - // setting the data source immediately queries it, since the collection view is also set - adapter.dataSource = self.dataSource; - // simulate display reloading data on the collection view - [collectionView layoutIfNeeded]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withItemsMovingInBlocks_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @0), - genTestObject(@2, @7), - genTestObject(@3, @8), - genTestObject(@4, @8), - genTestObject(@5, @8), - genTestObject(@6, @5), - genTestObject(@7, @8), - genTestObject(@8, @8), - genTestObject(@9, @8), - ]]; - - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView]; - IGListExperimentalAdapterUpdater *updater = [IGListExperimentalAdapterUpdater new]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; - adapter.dataSource = self.dataSource; - adapter.collectionView = collectionView; - [collectionView layoutSubviews]; - - XCTAssertEqual([collectionView numberOfSections], 9); - - self.dataSource.objects = @[ - genTestObject(@1, @0), - genTestObject(@10, @5), - genTestObject(@11, @7), - genTestObject(@2, @7), - genTestObject(@3, @8), - genTestObject(@6, @5), // "moves" in front of 4, 5 but doesn't change index in array - genTestObject(@4, @8), - genTestObject(@5, @8), - genTestObject(@7, @8), - genTestObject(@8, @8), - ]; - - XCTestExpectation *expectation = genExpectation; - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual([collectionView numberOfSections], 10); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenItemDeleted_withDisplayDelegate_thatDelegateReceivesDeletedItem { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @2), - ]]; - IGTestObject *object = self.dataSource.objects[0]; - - self.dataSource.objects = @[ - genTestObject(@2, @2), - ]; - - id mockDisplayHandler = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; - self.adapter.delegate = mockDisplayHandler; - - [[mockDisplayHandler expect] listAdapter:self.adapter didEndDisplayingObject:object atIndex:0]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { - [mockDisplayHandler verify]; - XCTAssertTrue(finished2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenItemReloaded_withDisplacingMutations_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @1), - genTestObject(@3, @1), - genTestObject(@4, @1), - genTestObject(@5, @1), - ]]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @2), // reloaded - genTestObject(@5, @2), // reloaded - genTestObject(@4, @2), // reloaded - genTestObject(@3, @1), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertTrue(finished); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenCollectionViewAppears_thatWillDisplayEventsAreSent { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @2), - ]]; - IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - XCTAssertEqual(ic1.willDisplayCount, 1); - XCTAssertEqual(ic1.didEndDisplayCount, 0); - XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1); - XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0); - - IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; - XCTAssertEqual(ic2.willDisplayCount, 1); - XCTAssertEqual(ic2.didEndDisplayCount, 0); - XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@0], 1); - XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1); - XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 0); - XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 0); -} - -- (void)test_whenAdapterUpdates_withItemUpdated_thatdidEndDisplayEventsAreSent { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @2), - ]]; - IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1), // reloaded w/ 1 cell removed - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual(ic1.willDisplayCount, 1); - XCTAssertEqual(ic1.didEndDisplayCount, 0); - XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1); - XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0); - - XCTAssertEqual(ic2.willDisplayCount, 1); - XCTAssertEqual(ic2.didEndDisplayCount, 0); - XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1); - XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAdapterUpdates_withItemRemoved_thatdidEndDisplayEventsAreSent { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @2), - ]]; - IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; - - self.dataSource.objects = @[ - genTestObject(@1, @1) - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual(ic1.willDisplayCount, 1); - XCTAssertEqual(ic1.didEndDisplayCount, 0); - XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1); - XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0); - - XCTAssertEqual(ic2.willDisplayCount, 1); - XCTAssertEqual(ic2.didEndDisplayCount, 1); - XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@0], 1); - XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1); - XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 1); - XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAdapterUpdates_withEmptyItems_thatdidEndDisplayEventsAreSent { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @2), - ]]; - IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; - - self.dataSource.objects = @[]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual(ic1.didEndDisplayCount, 1); - XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 1); - - XCTAssertEqual(ic2.didEndDisplayCount, 1); - XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 1); - XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenBatchUpdating_withCellQuery_thatCellIsNil { - __block BOOL executed = NO; - __weak __typeof__(self) weakSelf = self; - void (^block)(IGTestDelegateController *) = ^(IGTestDelegateController *ic) { - executed = YES; - XCTAssertNil([weakSelf.adapter cellForItemAtIndex:0 sectionController:ic]); - }; - ((IGTestDelegateDataSource *)self.dataSource).cellConfigureBlock = block; - - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @1), - genTestObject(@3, @1), - ]]; - - // delete the last object from the original array - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1), - genTestObject(@4, @1), - genTestObject(@5, @1), - genTestObject(@6, @1), - ]; - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdates_withWorkingRange_thatAccessingCellDoesntCrash { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @1), - genTestObject(@3, @1), - ]]; - - // section controller try to access a cell in -listAdapter:sectionControllerWillEnterWorkingRange: - // add items beyond the 100x100 frame so they access unavailable cells - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1), - genTestObject(@3, @1), - genTestObject(@4, @1), - genTestObject(@5, @1), - genTestObject(@6, @1), - genTestObject(@7, @1), - genTestObject(@8, @1), - genTestObject(@9, @1), - genTestObject(@10, @1), - genTestObject(@11, @1), - ]; - XCTestExpectation *expectation = genExpectation; - - // this will call -collectionView:performBatchUpdates:, trigger collectionView:willDisplayCell:forItemAtIndexPath:, - // which kicks off the working range logic - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadingItems_withDeleteAndInsertCollision_thatUpdateCanBeApplied { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @5), - genTestObject(@3, @1), - ]]; - - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; - - XCTestExpectation *expectation = genExpectation; - [section.collectionContext performBatchAnimated:NO updates:^(id batchContext) { - [batchContext deleteInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]]; - [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]]; - [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]]; - } completion:^(BOOL finished) { - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadingItems_withSectionInsertedInFront_thatUpdateCanBeApplied { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @5), - genTestObject(@3, @1), - ]]; - - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; - - XCTestExpectation *expectation1 = genExpectation; - [section.collectionContext performBatchAnimated:NO updates:^(id batchContext) { - [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]]; - } completion:^(BOOL finished) { - [expectation1 fulfill]; - }]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@4, @1), // insert to shift object @2 - genTestObject(@2, @5), - genTestObject(@3, @1), - ]; - - XCTestExpectation *expectation2 = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - [expectation2 fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadingItems_withSectionDeletedInFront_thatUpdateCanBeApplied { - [self setupWithObjects:@[ - genTestObject(@1, @1), - genTestObject(@2, @5), - genTestObject(@3, @1), - ]]; - - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; - - XCTestExpectation *expectation1 = genExpectation; - [section.collectionContext performBatchAnimated:NO updates:^(id batchContext) { - [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]]; - } completion:^(BOOL finished) { - [expectation1 fulfill]; - }]; - - self.dataSource.objects = @[ - genTestObject(@2, @5), - genTestObject(@3, @1), - ]; - - XCTestExpectation *expectation2 = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - [expectation2 fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenMovingItems_withObjectMoving_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @2), - genTestObject(@2, @2), - genTestObject(@3, @2), - ]]; - - __block BOOL executed = NO; - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; - [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; - executed = YES; - } completion:nil]; - - self.dataSource.objects = @[ - genTestObject(@3, @2), - genTestObject(@1, @2), - genTestObject(@2, @2), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertTrue(executed); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenMovingItems_withObjectReloaded_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @2), - ]]; - - __block BOOL executed = NO; - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; - [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; - executed = YES; - } completion:nil]; - - self.dataSource.objects = @[ - genTestObject(@1, @3), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertTrue(executed); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenMovingItems_withObjectDeleted_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @2), - ]]; - - __block BOOL executed = NO; - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; - [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; - executed = YES; - } completion:nil]; - - self.dataSource.objects = @[]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertTrue(executed); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenMovingItems_withObjectInsertedBefore_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @2), - ]]; - - __block BOOL executed = NO; - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; - [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; - executed = YES; - } completion:nil]; - - [self setupWithObjects:@[ - genTestObject(@2, @2), - genTestObject(@1, @2), - ]]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertTrue(executed); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenMovingItems_thatCollectionViewWorks { - [self setupWithObjects:@[ - genTestObject(@1, @2), - ]]; - - IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; - cell1.label.text = @"foo"; - cell2.label.text = @"bar"; - - XCTestExpectation *expectation = genExpectation; - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; - [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; - } completion:^(BOOL finished) { - IGTestCell *movedCell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - IGTestCell *movedCell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; - XCTAssertEqualObjects(movedCell1.label.text, @"bar"); - XCTAssertEqualObjects(movedCell2.label.text, @"foo"); - - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenInvalidatingSectionController_withSizeChange_thatCellsAreSameInstance_thatCellsFrameChanged { - [self setupWithObjects:@[ - genTestObject(@1, @2), - ]]; - - NSIndexPath *path1 = [NSIndexPath indexPathForItem:0 inSection:0]; - NSIndexPath *path2 = [NSIndexPath indexPathForItem:1 inSection:0]; - IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path1]; - IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path2]; - - XCTAssertEqual(cell1.frame.size.height, 10); - XCTAssertEqual(cell2.frame.size.height, 10); - - IGTestDelegateController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; - section.height = 20.0; - - XCTestExpectation *expectation = genExpectation; - [section.collectionContext invalidateLayoutForSectionController:section completion:^(BOOL finished) { - XCTAssertEqual(cell1, [self.collectionView cellForItemAtIndexPath:path1]); - XCTAssertEqual(cell2, [self.collectionView cellForItemAtIndexPath:path2]); - XCTAssertEqual(cell1.frame.size.height, 20); - XCTAssertEqual(cell2.frame.size.height, 20); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAdaptersSwapCollectionViews_thatOldAdapterDoesntUpdateOldCollectionView { - IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListExperimentalAdapterUpdater new] viewController:nil]; - IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new]; - dataSource1.objects = @[genTestObject(@1, @2)]; - adapter1.dataSource = dataSource1; - adapter1.collectionView = self.collectionView; - - [self.collectionView layoutIfNeeded]; - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - - IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListExperimentalAdapterUpdater new] viewController:nil]; - IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new]; - dataSource2.objects = @[genTestObject(@1, @1), genTestObject(@2, @1)]; - adapter2.dataSource = dataSource2; - adapter2.collectionView = self.collectionView; - - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - - dataSource1.objects = @[genTestObject(@1, @2), genTestObject(@2, @2), genTestObject(@3, @2), genTestObject(@4, @2)]; - XCTestExpectation *expectation = genExpectation; - - [adapter1 performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAdaptersSwapCollectionViews_ { - IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListExperimentalAdapterUpdater new] viewController:nil]; - IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new]; - dataSource1.objects = @[genTestObject(@1, @2)]; - adapter1.dataSource = dataSource1; - adapter1.collectionView = self.collectionView; - - [self.collectionView layoutIfNeeded]; - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - - IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListExperimentalAdapterUpdater new] viewController:nil]; - IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new]; - dataSource2.objects = @[genTestObject(@1, @1), genTestObject(@2, @1)]; - adapter2.dataSource = dataSource2; - adapter2.collectionView = self.collectionView; - - [self.collectionView layoutIfNeeded]; - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - - dataSource2.objects = @[genTestObject(@1, @2), genTestObject(@2, @1), genTestObject(@3, @1), genTestObject(@4, @1)]; - XCTestExpectation *expectation = genExpectation; - - [adapter2 performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 4); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenDidUpdateAsyncReloads_withBatchUpdatesInProgress_thatReloadIsExecuted { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGTestDelegateController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; - - XCTestExpectation *expectation1 = genExpectation; - __weak __typeof__(section) weakSection = section; - section.itemUpdateBlock = ^{ - // currently inside -[IGListSectionController didUpdateToObject:], change the item (note: NEVER do this) manually - // so that the data powering numberOfItems changes (1 to 2). dispatch_async the update to skip outside of the - // -[UICollectionView performBatchUpdates:completion:] block execution - [weakSection.collectionContext performBatchAnimated:NO updates:^(id batchContext) { - weakSection.item = genTestObject(@1, @2); - [batchContext reloadSectionController:weakSection]; - } completion:^(BOOL finished) { - [expectation1 fulfill]; - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - }]; - }; - - // add an object so that a batch update is triggered (diff result has changes) - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - XCTestExpectation *expectation2 = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - // verify that the section still has 2 items since this completion executes AFTER the reload block above - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - [expectation2 fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)_test_whenInsertingItemsTwice_withDataUpdatedTwice_thatAllUpdatesAppliedWithoutException { - [self setupWithObjects:@[ - genTestObject(@1, @2), - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @4; - [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)FIXME_test_whenDeletingItemsTwice_withDataUpdatedTwice_thatAllUpdatesAppliedWithoutException { - [self setupWithObjects:@[ - genTestObject(@1, @4), - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @2; - [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadingSameItemTwice_thatDeletesAndInsertsAreBalanced { - [self setupWithObjects:@[ - genTestObject(@1, @4), - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expectation = genExpectation; - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenUpdateQueuedDuringBatch_thatUpdateCompletesWithoutCrashing { - [self setupWithObjects:@[ - genTestObject(@1, @4), - genTestObject(@2, @4), - genTestObject(@3, @4), - genTestObject(@4, @4), - ]]; - - IGTestObject *object = self.dataSource.objects[0]; - IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; - - XCTestExpectation *expect1 = genExpectation; - XCTestExpectation *expect2 = genExpectation; - - [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - - self.dataSource.objects = @[ - genTestObject(@2, @4), - genTestObject(@4, @4), - genTestObject(@1, @3), - ]; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); - [expect1 fulfill]; - }]; - } completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 4); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 4); - XCTAssertEqual([self.collectionView numberOfItemsInSection:3], 4); - [expect2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenMassiveUpdate_thatUpdateApplied { - // init empty - [self setupWithObjects:@[]]; - - NSMutableArray *objects = [NSMutableArray new]; - for (NSInteger i = 0; i < 3000; i++) { - [objects addObject:genTestObject(@(i + 1), @4)]; - } - self.dataSource.objects = objects; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 3000); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAddingMultipleUpdateListeners_withPerformUpdatesAnimated_thatEventsReceived { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; - IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; - - [self.adapter addUpdateListener:listener1]; - [self.adapter addUpdateListener:listener2]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual(listener1.hits, 1); - XCTAssertEqual(listener1.animated, YES); - XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); - XCTAssertEqual(listener2.hits, 1); - XCTAssertEqual(listener2.animated, YES); - XCTAssertEqual(listener2.type, IGListAdapterUpdateTypePerformUpdates); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAddingMultipleUpdateListeners_withPerformUpdatesNotAnimated_thatEventsReceived { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; - IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; - - [self.adapter addUpdateListener:listener1]; - [self.adapter addUpdateListener:listener2]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - XCTAssertEqual(listener1.hits, 1); - XCTAssertEqual(listener1.animated, NO); - XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); - XCTAssertEqual(listener2.hits, 1); - XCTAssertEqual(listener2.animated, NO); - XCTAssertEqual(listener2.type, IGListAdapterUpdateTypePerformUpdates); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAddingMultipleUpdateListeners_withReloadData_thatEventsReceived { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; - IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; - - [self.adapter addUpdateListener:listener1]; - [self.adapter addUpdateListener:listener2]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter reloadDataWithCompletion:^(BOOL finished) { - XCTAssertEqual(listener1.hits, 1); - XCTAssertEqual(listener1.animated, NO); - XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeReloadData); - XCTAssertEqual(listener2.hits, 1); - XCTAssertEqual(listener2.animated, NO); - XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeReloadData); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAddingMultipleUpdateListeners_withItemUpdatesAnimated_thatEventsReceived { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; - IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; - - [self.adapter addUpdateListener:listener1]; - [self.adapter addUpdateListener:listener2]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; - - XCTestExpectation *expectation = genExpectation; - [section.collectionContext performBatchAnimated:YES updates:^(id _Nonnull batchContext) { - [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished) { - XCTAssertEqual(listener1.hits, 1); - XCTAssertEqual(listener1.animated, YES); - XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeItemUpdates); - XCTAssertEqual(listener2.hits, 1); - XCTAssertEqual(listener2.animated, YES); - XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeItemUpdates); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAddingMultipleUpdateListeners_withItemUpdatesNotAnimated_thatEventsReceived { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; - IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; - - [self.adapter addUpdateListener:listener1]; - [self.adapter addUpdateListener:listener2]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; - - XCTestExpectation *expectation = genExpectation; - [section.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { - [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished) { - XCTAssertEqual(listener1.hits, 1); - XCTAssertEqual(listener1.animated, NO); - XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeItemUpdates); - XCTAssertEqual(listener2.hits, 1); - XCTAssertEqual(listener2.animated, NO); - XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeItemUpdates); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAddingMultipleUpdateListeners_thenRemovingListener_thatRemainingReceives { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; - IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; - - [self.adapter addUpdateListener:listener1]; - [self.adapter addUpdateListener:listener2]; - [self.adapter removeUpdateListener:listener2]; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual(listener1.hits, 1); - XCTAssertEqual(listener1.animated, YES); - XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); - XCTAssertEqual(listener2.hits, 0); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenAddingUpdateListener_thenListenerReferenceHitsZero_thatListenerReleased { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - IGListAdapterUpdateTester *listener = [IGListAdapterUpdateTester new]; - __weak id weakListener = listener; - [self.adapter addUpdateListener:listener]; - listener = nil; - - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertNil(weakListener); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenModifyingInitialAndFinalAttribute_thatLayoutIsCorrect { - // set up the custom layout - IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:YES]; - self.collectionView.collectionViewLayout = layout; - - IGTestObject *object = genTestObject(@1, @2); - [self setupWithObjects:@ [object]]; - - // set up the section controller - IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; - sectionController.transitionDelegate = sectionController; - - CGPoint offset = CGPointMake(10, 10); - NSIndexPath *indexPath = genIndexPath(0, 0); - UICollectionViewLayoutAttributes *attribute = [layout layoutAttributesForItemAtIndexPath:indexPath]; - - // set up the custom initial attribute transformation - sectionController.initialAttributesOffset = offset; - UICollectionViewLayoutAttributes *initialAttribute = [layout initialLayoutAttributesForAppearingItemAtIndexPath:indexPath]; - - // set up the custom final attribute transformation - sectionController.finalAttributesOffset = offset; - UICollectionViewLayoutAttributes *finalAttribute = [layout finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath]; - - IGAssertEqualPoint(initialAttribute.center, attribute.center.x + offset.x, attribute.center.y + offset.y); - 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:[IGListExperimentalAdapterUpdater new] viewController:nil]; - - 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:[IGListExperimentalAdapterUpdater new] viewController:nil]; - - 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]; -} - -- (void)test_whenCollectionViewBecomesNilDuringPerformUpdates_thatStateCleanedCorrectly { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - // perform update on listAdapter - XCTestExpectation *expectation1 = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - [expectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; - - // update the underlying contents before performing another update - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - // perform update, but set the listAdapter's collectionView to nil during the update - XCTestExpectation *expectation2 = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - [expectation2 fulfill]; - }]; - self.adapter.collectionView = nil; - [self waitForExpectationsWithTimeout:30 handler:nil]; - - // add a new collectionView to the listAdapter - UICollectionView *collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView2]; - self.adapter.collectionView = collectionView2; - - // update the underlying contents before performing update - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1), - genTestObject(@3, @1) - ]; - - // perform update on listAdapter (now with a non-nil collectionView) - XCTestExpectation *expectation3 = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - [expectation3 fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenCollectionViewBecomesNilDuringReloadData_thatStateCleanedCorrectly { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - // reload data on listAdapter - XCTestExpectation *expectation1 = genExpectation; - [self.adapter reloadDataWithCompletion:^(BOOL finished) { - [expectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; - - // update the underlying contents before reloading again - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1) - ]; - - // reload data, but set the listAdapter's collectionView to nil during the update - XCTestExpectation *expectation2 = genExpectation; - [self.adapter reloadDataWithCompletion:^(BOOL finished) { - [expectation2 fulfill]; - }]; - self.adapter.collectionView = nil; - [self waitForExpectationsWithTimeout:30 handler:nil]; - - // add a new collectionView to the listAdapter - UICollectionView *collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView2]; - self.adapter.collectionView = collectionView2; - self.dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @1), - genTestObject(@3, @1) - ]; - - // reload data on listAdapter (now with a non-nil collectionView) - XCTestExpectation *expectation3 = genExpectation; - [self.adapter reloadDataWithCompletion:^(BOOL finished) { - [expectation3 fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenUpdating_withMissingSectionController_thatDoesNotCrash { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]]; - - // Adding an object that won't have a corresponding section-controller - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar"), - kIGTestDelegateDataSourceSkipObject - ]; - - // Perform updates on the adapter - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - // Checked that the update worked - XCTAssertTrue(finished); - // Check that we skipped the object with a missing section-controller - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(self.adapter.objects.count, 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -#pragma mark - Dealloc checks - -- (void)test_whenReleasingObjects_thatAssertDoesntFire { - [self setupWithObjects:@[ - genTestObject(@1, @1) - ]]; - - // if the adapter keeps a strong ref to self and uses an async method, this will hit asserts that a list item - // controller is nil. the adapter should be released and the completion block never called. - @autoreleasepool { - IGListExperimentalAdapterUpdater *updater = [[IGListExperimentalAdapterUpdater alloc] init]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:2]; - adapter.collectionView = self.collectionView; - adapter.dataSource = self.dataSource; - [adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - XCTAssertTrue(NO, @"Should not reach completion block for adapter"); - }]; - } - - self.collectionView = nil; - self.dataSource = nil; - - // queued after perform updates - XCTestExpectation *expectation = genExpectation; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [expectation fulfill]; - }); - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenDataSourceDeallocatedAfterUpdateQueued_thatUpdateSuccesfullyCompletes { - IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; - dataSource.objects = @[genTestObject(@1, @1)]; - self.adapter.collectionView = self.collectionView; - self.adapter.dataSource = dataSource; - [self.collectionView layoutIfNeeded]; - - dataSource.objects = @[ - genTestObject(@1, @1), - genTestObject(@2, @2), - ]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - [expectation fulfill]; - }]; - - dataSource = nil; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenQueuingUpdate_withSectionControllerBatchUpdate_thatSectionControllerNotRetained { - __weak id weakSectionController = nil; - __weak id weakAdapter = nil; - __weak id weakCollectionView = nil; - - @autoreleasepool { - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListExperimentalAdapterUpdater new] viewController:nil]; - IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; - IGTestObject *object = genTestObject(@1, @2); - dataSource.objects = @[object]; - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]]; - adapter.collectionView = collectionView; - adapter.dataSource = dataSource; - [collectionView layoutIfNeeded]; - XCTAssertEqual([collectionView numberOfSections], 1); - XCTAssertEqual([collectionView numberOfItemsInSection:0], 2); - - IGListSectionController *section = [adapter sectionControllerForObject:object]; - - [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { - object.value = @3; - [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished) {}]; - - dataSource.objects = @[object, genTestObject(@2, @2)]; - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) {}]; - - weakAdapter = adapter; - weakCollectionView = collectionView; - weakSectionController = section; - - XCTAssertNotNil(weakAdapter); - XCTAssertNotNil(weakCollectionView); - XCTAssertNotNil(weakSectionController); - } - XCTAssertNil(weakAdapter); - XCTAssertNil(weakCollectionView); - XCTAssertNil(weakSectionController); -} - -- (void)test_whenInvalidatingInsideBatchUpdate_withSystemReleased_thatSystemNil_andCollectionViewDoesntCrashOnDealloc { - __weak id weakAdapter = nil; - __block BOOL executedItemUpdate = NO; - XCTestExpectation *expectation = genExpectation; - - @autoreleasepool { - self.dataSource.objects = @[ - genTestObject(@1, @"Bar"), - genTestObject(@0, @"Foo") - ]; - - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView]; - IGListExperimentalAdapterUpdater *updater = [IGListExperimentalAdapterUpdater new]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; - adapter.dataSource = self.dataSource; - adapter.collectionView = collectionView; - [collectionView layoutIfNeeded]; - - IGTestDelegateController *section = [adapter sectionControllerForObject:self.dataSource.objects.firstObject]; - - __weak typeof(section) weakSection = section; - section.itemUpdateBlock = ^{ - executedItemUpdate = YES; - [weakSection.collectionContext invalidateLayoutForSectionController:weakSection completion:nil]; - }; - - self.dataSource.objects = @[ - genTestObject(@1, @"Bar"), - genTestObject(@0, @"Foo") - ]; - - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertNotNil(collectionView); - XCTAssertNotNil(adapter); - [collectionView removeFromSuperview]; - [expectation fulfill]; - }]; - - weakAdapter = adapter; - XCTAssertNotNil(weakAdapter); - } - - [self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) { - XCTAssertTrue(executedItemUpdate); - XCTAssertNil(weakAdapter); - }]; -} - -- (void)test_whenInvalidatingInsideBatchUpdate_andRemoveThatSectionController_thatCollectionViewDoesntCrash { - IGTestObject *foo = genTestObject(@1, @"Foo"); - IGTestObject *bar = genTestObject(@0, @"Bar"); - self.dataSource.objects = @[foo, bar]; - - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; - [self.window addSubview:collectionView]; - IGListExperimentalAdapterUpdater *updater = [IGListExperimentalAdapterUpdater new]; - IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; - adapter.dataSource = self.dataSource; - adapter.collectionView = collectionView; - [collectionView layoutIfNeeded]; - - IGTestDelegateController *sectionToRemove = [adapter sectionControllerForObject:bar]; - - self.dataSource.objects = @[foo]; - - XCTestExpectation *expectation = genExpectation; - [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - XCTAssertTrue(finished); - [expectation fulfill]; - }]; - - XCTestExpectation *expectation2 = genExpectation; - [sectionToRemove.collectionContext invalidateLayoutForSectionController:sectionToRemove completion:^(BOOL finished) { - // That section-controller is about to be removed, so this should not finish. - XCTAssertFalse(finished); - [expectation2 fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingBatchSectionUpdate_thatTransactionObjectsGetsDeallocated { - __weak IGListUpdateTransactionBuilder *transactionBuilder = nil; - __block __weak IGListUpdateTransactionBuilder *lastTransactionBuilder = nil; - __block __weak id transaction = nil; - - // It should crash if the updater isn't a IGListExperimentalAdapterUpdater. - // Once IGListExperimentalAdapterUpdater ships, it will be renamed to IGListAdapterUpdater. - IGListExperimentalAdapterUpdater *updater = (IGListExperimentalAdapterUpdater *)self.updater; - - @autoreleasepool { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo") - ]]; - - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]; - - // Grab the current builder - transactionBuilder = [updater transactionBuilder]; - - [self.adapter performBatchAnimated:NO updates:^(id _Nonnull batchContext) { - // Take advantage of `performBatchAnimated` to grab the transaction, but we don't perform any changes. - lastTransactionBuilder = [updater lastTransactionBuilder]; - XCTAssertNotNil(lastTransactionBuilder); - transaction = [updater transaction]; - XCTAssertNotNil(transaction); - } completion:nil]; - - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - XCTAssertNil(transactionBuilder); - XCTAssertNil(lastTransactionBuilder); - XCTAssertNil(transaction); - [expectation fulfill]; - }); - }]; - - // Force the update to happen right away - [updater update]; - } - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -#pragma mark - Changing the collectionView/dataSource - -- (void)test_whenChangingDataSourceWithADifferentCount_thenPerformBatchUpdate_thatLastestDataIsApplied { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo") - ]]; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: 1 section - - self.dataSource = [IGTestDelegateDataSource new]; - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]; - self.adapter.dataSource = self.dataSource; - - // STATE - // DataSource: 2 sections - // Adapter: 2 sections - // CollectionView: Invalidated count - - // Schedule update - XCTestExpectation *expectation2 = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - XCTAssertTrue(finished); - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(self.adapter.objects.count, 2); - - // STATE - // DataSource: 2 sections - // Adapter: 2 sections - // CollectionView: 2 sections - - [expectation2 fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenChangingCollectionView_thenScheduleSectionUpdate_thatLastestDataIsApplied { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo") - ]]; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: 1 section - - // Force dataSource <> adapater sync by changing the collection view - self.layout = [UICollectionViewFlowLayout new]; - self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame - collectionViewLayout:self.layout]; - self.adapter.collectionView = self.collectionView; - - // STATE - // DataSource: 1 sections - // Adapter: 1 sections - // CollectionView: Invalidated count - - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual(self.adapter.objects.count, 1); - - // STATE - // DataSource: 1 sections - // Adapter: 1 sections - // CollectionView: 1 sections -} - -- (void)test_settingCollectionViewAndDataSource_thatDontCreateCellsUntilLayout { - self.dataSource.objects = @[ - genTestObject(@0, @"Foo") - ]; - self.adapter.collectionView = self.collectionView; - self.adapter.dataSource = self.dataSource; - - // Make sure we didn't create the cells just yet, since we might want to scroll way without animating. - XCTAssertNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); - - [self.collectionView layoutIfNeeded]; - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); -} - -#pragma mark - Changing the collectionView/dataSource with pending SECTION updates - -- (void)test_whenSchedulingSectionUpdate_thenChangeCollectionView_thatLastestDataIsApplied { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo") - ]]; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: 1 section - - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]; - - // STATE - // DataSource: 2 sections - // Adapter: 1 section - // CollectionView: 1 section - - // Schedule update - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - - // STATE - // DataSource: 2 sections - // Adapter: 2 sections - // CollectionView: Invalidated count - - // Force collectionView <> adapter sync - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(self.adapter.objects.count, 2); - XCTAssertTrue(finished); - - // STATE - // DataSource: 2 sections - // Adapter: 2 sections - // CollectionView: 2 sections - - [expectation fulfill]; - }]; - - // Force dataSource <> adapater sync by changing the collection view - self.layout = [UICollectionViewFlowLayout new]; - self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame - collectionViewLayout:self.layout]; - self.adapter.collectionView = self.collectionView; - - // Although all the syncs should have been checked by now, lets still make - // sure the counts are right. - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(self.adapter.objects.count, 2); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSchedulingSectionUpdate_thenChangeTheDataSource_thatLastestDataIsApplied { - [self setupWithObjects:@[ - genTestObject(@0, @"Foo") - ]]; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: 1 section - - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]; - - // STATE - // DataSource: 2 section - // Adapter: 1 section - // CollectionView: 1 section - - // Schedule update - XCTestExpectation *expectation2 = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - // STATE - // DataSource: 3 sections - // Adapter: 3 sections - // CollectionView: Invalidated count - - XCTAssertTrue(finished); - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual(self.adapter.objects.count, 3); - - // STATE - // DataSource: 3 sections - // Adapter: 3 sections - // CollectionView: 3 sections - - [expectation2 fulfill]; - }]; - - // Force dataSource <> adapater sync by changing the dataSource - self.dataSource = [IGTestDelegateDataSource new]; - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar"), - genTestObject(@2, @"Baz") - ]; - self.adapter.dataSource = self.dataSource; - - // Although all the syncs should have been checked by now, lets still make - // sure the counts are right. - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual(self.adapter.objects.count, 3); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -#pragma mark - Changing the collectionView/dataSource with pending ITEM updates - -- (void)test_whenSchedulingItemUpdate_thenChangeCollectionView_thatLastestDataIsApplied { - [self setupWithObjects:@[ - genTestObject(@0, @1) - ]]; - - // STATE - // Section Controller: 1 cell - // CollectionView: 1 cell - - IGTestDelegateController *contoller = (IGTestDelegateController *)[self.adapter sectionControllerForSection:0]; - XCTAssertNotNil(contoller); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - - XCTestExpectation *expectation1 = genExpectation; - [contoller.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { - // Just change the item count for section 0 - contoller.item = genTestObject(@0, @2); - [batchContext insertInSectionController:contoller atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished) { - XCTAssertTrue(finished); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - [expectation1 fulfill]; - }]; - - // Force dataSource <> adapater sync by changing the collection view - self.layout = [UICollectionViewFlowLayout new]; - self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame - collectionViewLayout:self.layout]; - self.adapter.collectionView = self.collectionView; - - // STATE - // Section Controller: 2 cells - // CollectionView: Invalidated count - - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - - // STATE - // Section Controller: 2 cells - // CollectionView: 2 cells - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSchedulingItemUpdate_thenChangeDataSource_thatLastestDataIsApplied { - [self setupWithObjects:@[ - genTestObject(@0, @1) - ]]; - - // STATE - // Section Controller: 1 cell - // CollectionView: 1 cell - - IGTestDelegateController *contoller = (IGTestDelegateController *)[self.adapter sectionControllerForSection:0]; - XCTAssertNotNil(contoller); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - - XCTestExpectation *expectation1 = genExpectation; - [contoller.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { - // Just change the item count for section 0 - contoller.item = genTestObject(@0, @2); - [batchContext insertInSectionController:contoller atIndexes:[NSIndexSet indexSetWithIndex:0]]; - } completion:^(BOOL finished) { - XCTAssertTrue(finished); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - [expectation1 fulfill]; - }]; - - // Force dataSource <> adapater sync by changing the dataSource. - // Note that we keep the old object here, but that should not matter since - // it didn't change, it won't call -didUpdateToObject on that section-controller. - IGTestDelegateDataSource *oldDataSource = self.dataSource; - self.dataSource = [IGTestDelegateDataSource new]; - self.dataSource.objects = oldDataSource.objects; - self.adapter.dataSource = self.dataSource; - - // STATE - // Section Controller: 2 cells - // CollectionView: Invalidated count - - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - - // STATE - // Section Controller: 2 cells - // CollectionView: 2 cells - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -#pragma mark - Changing the collectionView/dataSource in middle of diffing - -- (void)test_whenSchedulingSectionUpdate_thenBeginDiffing_thenChangeCollectionView_thatLastestDataIsApplied { - IGListExperimentalAdapterUpdater *updater = (IGListExperimentalAdapterUpdater *)self.updater; - updater.experiments |= IGListExperimentBackgroundDiffing; - - [self setupWithObjects:@[ - genTestObject(@0, @"Foo") - ]]; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: 1 section - - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]; - - // STATE - // DataSource: 2 sections - // Adapter: 1 section - // CollectionView: 1 section - - // Schedule update - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - // STATE - // DataSource: 2 sections - // Adapter: 2 sections - // CollectionView: Invalidated count - - XCTAssertTrue(finished); - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(self.adapter.objects.count, 2); - - // STATE - // DataSource: 2 sections - // Adapter: 2 sections - // CollectionView: 2 sections - - [expectation fulfill]; - }]; - - // Force the update to happen right way, so that the diffing starts - [updater update]; - - // Force dataSource <> adapater sync by changing the collection view - self.layout = [UICollectionViewFlowLayout new]; - self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame - collectionViewLayout:self.layout]; - self.adapter.collectionView = self.collectionView; - - // Although all the syncs should have been checked by now, lets still make - // sure the counts are right. - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(self.adapter.objects.count, 2); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenSchedulingSectionUpdate_thenBeginDiffing_thenChangeTheDataSource_thatLastestDataIsApplied { - IGListExperimentalAdapterUpdater *updater = (IGListExperimentalAdapterUpdater *)self.updater; - updater.experiments |= IGListExperimentBackgroundDiffing; - - [self setupWithObjects:@[ - genTestObject(@0, @"Foo") - ]]; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: 1 section - - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar") - ]; - - // STATE - // DataSource: 2 sections - // Adapter: 1 section - // CollectionView: 1 section - - // Schedule update - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - // STATE - // DataSource: 3 sections - // Adapter: 3 sections - // CollectionView: Invalidated count - - XCTAssertTrue(finished); - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual(self.adapter.objects.count, 3); - - // STATE - // DataSource: 3 sections - // Adapter: 3 sections - // CollectionView: 3 sections - - [expectation fulfill]; - }]; - - // Force the update to happen right way, so that the diffing starts - [updater update]; - - // Force dataSource <> adapater sync by changing the dataSource - self.dataSource = [IGTestDelegateDataSource new]; - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar"), - genTestObject(@2, @"Baz") - ]; - self.adapter.dataSource = self.dataSource; - - // Although all the syncs should have been checked by now, lets still make - // sure the counts are right. - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual(self.adapter.objects.count, 3); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -#pragma mark - Sync the collectionView before setting a adapter.dataSource - -- (void)test_whenCollectionViewSyncsBeforeTheAdapterDataSourceIsSet_thatLastestDataIsApplied { - self.adapter.collectionView = self.collectionView; - - // Force the adapter <> collectionView to sync - XCTAssertEqual([self.collectionView numberOfSections], 0); - XCTAssertEqual([self.adapter objects].count, 0); - - // STATE - // DataSource: Nil - // Adapter: 0 sections - // CollectionView: 0 sections - - // Changing the `adapter.dataSource` will sync the adapter <> dataSource, and - // invalidate the collectionView's internal section/item counts. - self.dataSource.objects = @[genTestObject(@1, @"Foo")]; - self.adapter.dataSource = self.dataSource; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: Invalidated counts (UICollectionView will ask for counts on next layout) - - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.adapter objects].count, 1); - - // Test that collectionView syncs with the adapter - [self.collectionView layoutIfNeeded]; - XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: 1 section -} - -- (void)test_whenCollectionViewSyncsBeforeTheAdapterDataSourceIsSet_thenSchedulingSectionUpdate_thatLastestDataIsApplied { - self.adapter.collectionView = self.collectionView; - - // Force the adapter <> collectionView to sync - XCTAssertEqual([self.collectionView numberOfSections], 0); - XCTAssertEqual([self.adapter objects].count, 0); - - // STATE - // DataSource: Nil - // Adapter: 0 sections - // CollectionView: 0 sections - - // Changing the `adapter.dataSource` will sync the adapter <> dataSource, and - // invalidate the collectionView's internal section/item counts. - self.dataSource.objects = @[genTestObject(@0, @"Foo")]; - self.adapter.dataSource = self.dataSource; - - // STATE - // DataSource: 1 section - // Adapter: 1 section - // CollectionView: Invalidated counts (UICollectionView will ask for counts on next layout) - - XCTAssertEqual([self.adapter objects].count, 1); - - // Adding an object - self.dataSource.objects = @[ - genTestObject(@0, @"Foo"), - genTestObject(@1, @"Bar"), - ]; - - // STATE - // DataSource: 2 sections - // Adapter: 1 section - // CollectionView: Invalidated counts (Still) - - // Test that a batchUpdate from 1 -> 2 objects works, even though - // the collectionView has not synced yet. - XCTestExpectation *expectation = genExpectation; - [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { - // Checked that the update worked - XCTAssertTrue(finished); - // Check that the we have the correct counts - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(self.adapter.objects.count, 2); - [expectation fulfill]; - - // STATE - // DataSource: 2 sections - // Adapter: 2 section - // CollectionView: 2 sections - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -@end diff --git a/Tests/IGListExperimentalAdapterUpdaterTests.m b/Tests/IGListExperimentalAdapterUpdaterTests.m deleted file mode 100644 index a67e13ae3..000000000 --- a/Tests/IGListExperimentalAdapterUpdaterTests.m +++ /dev/null @@ -1,1284 +0,0 @@ -/* - * 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 - -#import - -#import "IGListAdapterUpdaterHelpers.h" -#import "IGListExperimentalAdapterUpdaterInternal.h" -#import "IGListMoveIndexInternal.h" -#import "IGListTestUICollectionViewDataSource.h" -#import "IGListTransitionData.h" -#import "IGTestObject.h" - -#define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] -#define waitExpectation [self waitForExpectationsWithTimeout:30 handler:nil] -#define genToBlock ^NSArray *{ return to; } - -@interface IGListExperimentalAdapterUpdaterTests : XCTestCase - -@property (nonatomic, strong) UIWindow *window; -@property (nonatomic, strong) UICollectionView *collectionView; -@property (nonatomic, strong) IGListTestUICollectionViewDataSource *dataSource; -@property (nonatomic, strong) IGListExperimentalAdapterUpdater *updater; -@property (nonatomic, strong) IGListTransitionDataApplyBlock applyDataBlock; - -@end - -@implementation IGListExperimentalAdapterUpdaterTests - -- (IGListCollectionViewBlock)collectionViewBlock { - return ^UICollectionView *{ return self.collectionView; }; -} - -- (IGListTransitionDataBlock)dataBlockFromObjects:(NSArray *)fromObjects toObjects:(NSArray *)toObjects { - return ^IGListTransitionData *{ - return [[IGListTransitionData alloc] initFromObjects:fromObjects toObjects:toObjects toSectionControllers:@[]]; - }; -} - -- (void)setUp { - [super setUp]; - - self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; - - UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:layout]; - - [self.window addSubview:self.collectionView]; - - self.dataSource = [[IGListTestUICollectionViewDataSource alloc] initWithCollectionView:self.collectionView]; - self.updater = [IGListExperimentalAdapterUpdater new]; - __weak __typeof__(self) weakSelf = self; - self.applyDataBlock = ^(IGListTransitionData *data) { - weakSelf.dataSource.sections = data.toObjects; - }; -} - -- (void)tearDown { - [super tearDown]; - - self.collectionView = nil; - self.dataSource = nil; - self.updater = nil; -} - -- (void)test_whenUpdatingtoObjects_thatUpdaterHasChanges { - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:@[] toObjects:@[@0]] - applyDataBlock:self.applyDataBlock - completion:nil]; - XCTAssertTrue([self.updater hasChanges]); -} - -- (void)test_whenUpdatingfromObjects_thatUpdaterHasChanges { - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:@[@0] toObjects:@[]] - applyDataBlock:self.applyDataBlock - completion:nil]; - XCTAssertTrue([self.updater hasChanges]); -} - -- (void)test_whenUpdatingtoObjects_withfromObjects_thatUpdaterHasChanges { - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:@[@0] toObjects:@[@1]] - applyDataBlock:self.applyDataBlock - completion:nil]; - XCTAssertTrue([self.updater hasChanges]); -} - -- (void)test_whenReloadingData_thatCollectionViewUpdates { - self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[]]]; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 1); - self.dataSource.sections = @[]; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 0); -} - -- (void)test_whenReloadingDataWithNilDataSourceBefore_thatCollectionViewNotCrash { - self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[@1]], [IGSectionObject sectionWithObjects:@[@2]]]; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 2); - - self.collectionView.dataSource = nil; - self.dataSource.sections = @[]; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 1); // Setting collectionView's dataSource to nil would yield a single section by default. -} - -- (void)test_whenInsertingSection_thatCollectionViewUpdates { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 1); - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenDeletingSection_thatCollectionViewUpdates { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 2); - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 1); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenInsertingSection_withItemChanges_thatCollectionViewUpdates { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[@0]] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[@0, @1]], - [IGSectionObject sectionWithObjects:@[@0, @1]] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenInsertingSection_withDeletedSection_thatCollectionViewUpdates { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[@0, @1, @2]], - [IGSectionObject sectionWithObjects:@[]] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[@1, @1]], - [IGSectionObject sectionWithObjects:@[@0]], - [IGSectionObject sectionWithObjects:@[@0, @2, @3]] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenReloadingSections_thatCollectionViewUpdates { - self.dataSource.sections = @[ - [IGSectionObject sectionWithObjects:@[@0, @1]], - [IGSectionObject sectionWithObjects:@[@0, @1]] - ]; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - - self.dataSource.sections = @[ - [IGSectionObject sectionWithObjects:@[@0, @1, @2]], - [IGSectionObject sectionWithObjects:@[@0, @1]] - ]; - [self.updater reloadCollectionView:self.collectionView sections:[NSIndexSet indexSetWithIndex:0]]; - - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); -} - -- (void)test_whenCollectionViewNeedsLayout_thatPerformBatchUpdateWorks { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - - // the collection view has been setup with 1 section and now needs layout - // calling performBatchUpdates: on a collection view needing layout will force layout - // we need to ensure that our data source is not changed until the update block is executed - [self.collectionView setNeedsLayout]; - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 1); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenUpdatesAreReentrant_thatUpdatesExecuteSerially { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - - __block NSInteger completionCounter = 0; - - XCTestExpectation *expectation1 = genExpectation; - void (^preUpdateBlock)(void) = ^{ - NSArray *anotherTo = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:to toObjects:anotherTo] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - completionCounter++; - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual(completionCounter, 2); - [expectation1 fulfill]; - }]; - }; - - XCTestExpectation *expectation2 = genExpectation; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:^(IGListTransitionData *data) { - // 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 - preUpdateBlock(); - - self.dataSource.sections = data.toObjects; - } - completion:^(BOOL finished) { - completionCounter++; - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual(completionCounter, 1); - [expectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenQueuingItemUpdates_thatUpdaterHasChanges { - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{} completion:nil]; - XCTAssertTrue([self.updater hasChanges]); -} - -- (void)test_whenOnlyQueueingItemUpdates_thatUpdateBlockExecutes { - XCTestExpectation *expectation = genExpectation; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{ - // expectation should be triggered. test failure is a timeout - [expectation fulfill]; - } completion:nil]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenQueueingItemUpdates_withBatchUpdate_thatItemUpdateBlockExecutes { - __block BOOL itemUpdateBlockExecuted = NO; - __block BOOL sectionUpdateBlockExecuted = NO; - - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:@[] toObjects:@[[IGSectionObject sectionWithObjects:@[@1]]]] - applyDataBlock:^(IGListTransitionData * data) { - self.dataSource.sections = data.toObjects; - sectionUpdateBlockExecuted = YES; - } - completion:nil]; - - XCTestExpectation *expectation = genExpectation; - [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 - XCTAssertEqual([self.collectionView numberOfSections], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - XCTAssertTrue(itemUpdateBlockExecuted); - XCTAssertTrue(sectionUpdateBlockExecuted); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenItemsMoveAndUpdate_thatCollectionViewWorks { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - ]; - - // change the number of items in the section, which a move would be unable to handle and would throw - // keep the same pointers so that the objects are equal - [from[2] setObjects:@[@1]]; - [from[0] setObjects:@[@1, @1]]; - [from[1] setObjects:@[@1, @1, @1]]; - - NSArray *to = @[ - from[2], - from[0], - from[1] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - - // without moves as inserts, we would assert b/c the # of items in each section changes - self.updater.sectionMovesAsDeletesInserts = YES; - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenConvertingReloads_withoutChanges_thatOriginalIndexUsed { - NSArray *from = @[]; - NSArray *to = @[]; - IGListIndexSetResult *result = IGListDiff(from, to, IGListDiffEquality); - NSMutableIndexSet *reloads = [result.updates mutableCopy]; - [reloads addIndex:2]; - NSMutableIndexSet *deletes = [result.deletes mutableCopy]; - NSMutableIndexSet *inserts = [result.inserts mutableCopy]; - IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); - XCTAssertEqual(reloads.count, 0); - XCTAssertEqual(deletes.count, 1); - XCTAssertEqual(inserts.count, 1); - XCTAssertTrue([deletes containsIndex:2]); - XCTAssertTrue([inserts containsIndex:2]); -} - -- (void)test_whenConvertingReloads_withChanges_thatIndexMoves { - NSArray *from = @[@1, @2, @3]; - NSArray *to = @[@3, @2, @1]; - IGListIndexSetResult *result = IGListDiff(from, to, IGListDiffEquality); - NSMutableIndexSet *reloads = [result.updates mutableCopy]; - [reloads addIndex:2]; - NSMutableIndexSet *deletes = [result.deletes mutableCopy]; - NSMutableIndexSet *inserts = [result.inserts mutableCopy]; - IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); - XCTAssertEqual(reloads.count, 0); - XCTAssertEqual(deletes.count, 1); - XCTAssertEqual(inserts.count, 1); - XCTAssertTrue([deletes containsIndex:2]); - XCTAssertTrue([inserts containsIndex:0]); -} - -- (void)test_whenReloadingSection_whenSectionRemoved_thatConvertMethodCorrects { - NSArray *from = @[@"a", @"b", @"c"]; - NSArray *to = @[@"a", @"c"]; - IGListIndexSetResult *result = IGListDiff(from, to, IGListDiffEquality); - NSMutableIndexSet *reloads = [NSMutableIndexSet indexSetWithIndex:1]; - NSMutableIndexSet *deletes = [NSMutableIndexSet new]; - NSMutableIndexSet *inserts = [NSMutableIndexSet new]; - IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); - XCTAssertEqual(reloads.count, 0); - XCTAssertEqual(deletes.count, 0); - XCTAssertEqual(inserts.count, 0); -} - -- (void)test_whenReloadingData_withNilCollectionView_thatDelegateFinishesWithoutUpdates { - id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - id compilerFriendlyNil = nil; - [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:nil]; - [self.updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; } reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - [mockDelegate verify]; -} - -- (void)test_whenPerformingUpdates_withNilCollectionView_thatDelegateFinishesWithoutUpdates { - id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - id compilerFriendlyNil = nil; - [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:nil]; - [self.updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; } reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - [mockDelegate verify]; -} - -- (void)test_whenCallingReloadData_withUICollectionViewFlowLayout_withEstimatedSize_thatSectionItemCountsCorrect { - UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; - // setting the estimated size of a layout causes UICollectionView to requery layout attributes during reloadData - // this becomes out of sync with the data source if the section/item count changes - layout.estimatedItemSize = CGSizeMake(100, 10); - - UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; - IGListTestUICollectionViewDataSource *dataSource = [[IGListTestUICollectionViewDataSource alloc] initWithCollectionView:collectionView]; - - // 2 sections, 1 item in 1st, 4 items in 2nd - dataSource.sections = @[ - [IGSectionObject sectionWithObjects:@[@1]], - [IGSectionObject sectionWithObjects:@[@1, @2, @3, @4]] - ]; - - // assert the initial state of the collection view WITHOUT any layoutSubviews or anything - XCTAssertEqual([collectionView numberOfSections], 2); - XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); - XCTAssertEqual([collectionView numberOfItemsInSection:1], 4); - - dataSource.sections = @[ - [IGSectionObject sectionWithObjects:@[@1]], - ]; - - IGListExperimentalAdapterUpdater *updater = [IGListExperimentalAdapterUpdater new]; - [updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; } reloadUpdateBlock:^{} completion:nil]; - [updater update]; - - XCTAssertEqual([collectionView numberOfSections], 1); - XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); - - dataSource.sections = @[ - [IGSectionObject sectionWithObjects:@[@1]], - [IGSectionObject sectionWithObjects:@[@1, @2, @3, @4]] - ]; - [updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; } reloadUpdateBlock:^{} completion:nil]; - [updater update]; - - XCTAssertEqual([collectionView numberOfSections], 2); - XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); - XCTAssertEqual([collectionView numberOfItemsInSection:1], 4); -} - -- (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isSetNO_diffHappens { - self.updater.allowsBackgroundReloading = NO; - [self.collectionView removeFromSuperview]; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - NSArray *to = @[[IGSectionObject sectionWithObjects:@[]]]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:self.dataSource.sections toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - return result.inserts.firstIndex == 0 && result.moves.count == 0 && result.updates.count == 0 && result.deletes.count == 0; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:OCMOCK_ANY collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isDefaultYES_fallbackToReload { - [self.collectionView removeFromSuperview]; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater didReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; - - XCTestExpectation *expectation = genExpectation; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isDefaultYES_andDataSourceWasSetToNilBefore_fallbackToReload { - [self.collectionView removeFromSuperview]; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater didReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; - - XCTestExpectation *expectation = genExpectation; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - self.collectionView.dataSource = nil; - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenReloadBatchedWithUpdate_thatCompletionBlockStillExecuted { - IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; - self.dataSource.sections = @[object]; - - __block BOOL reloadDataCompletionExecuted = NO; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:^(BOOL finished) { - reloadDataCompletionExecuted = YES; - }]; - - XCTestExpectation *expectation = genExpectation; - [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], - [NSIndexPath indexPathForItem:3 inSection:0], - ]]; - [self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[ - [NSIndexPath indexPathForItem:0 inSection:0], - ]]; - [self.updater moveItemInCollectionView:self.collectionView - fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0] - toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - } completion:^(BOOL finished) { - XCTAssertTrue(reloadDataCompletionExecuted); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingItemUpdateInMiddleOfReload_thatCompletionBlockStillExecuted { - IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; - self.dataSource.sections = @[object]; - - XCTestExpectation *expectation = genExpectation; - - // Section-controllers can schedule item updates in -didUpdateToObject, so lets make sure the completion block works. - IGListReloadUpdateBlock reloadUpdateBlock = ^{ - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] - animated:YES - itemUpdates:^{} - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - }; - - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] - reloadUpdateBlock:reloadUpdateBlock - completion:nil]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingItemUpdateInMiddleOfUpdate_thatCompletionBlockStillExecuted { - IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; - self.dataSource.sections = @[object]; - - XCTestExpectation *expectation = genExpectation; - - // Section-controllers can schedule item updates in -didUpdateToObject, so lets make sure the completion block works. - void (^updateItemBlock)(void) = ^{ - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] - animated:YES - itemUpdates:^{} - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - }; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:self.dataSource.sections] - applyDataBlock:^(IGListTransitionData * _Nonnull data) { - updateItemBlock(); - } - completion:nil]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenNotInViewHierarchy_thatUpdatesStillExecuteBlocks { - [self.collectionView removeFromSuperview]; - - IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; - self.dataSource.sections = @[object]; - - __block BOOL objectTransitionBlockExecuted = NO; - __block BOOL completionBlockExecuted = NO; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:self.dataSource.sections] - applyDataBlock:^(IGListTransitionData *data) { - objectTransitionBlockExecuted = YES; - } - completion:^(BOOL finished) { - completionBlockExecuted = YES; - }]; - - XCTestExpectation *expectation = genExpectation; - [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], - [NSIndexPath indexPathForItem:3 inSection:0], - ]]; - [self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[ - [NSIndexPath indexPathForItem:0 inSection:0], - ]]; - [self.updater moveItemInCollectionView:self.collectionView - fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0] - toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - } completion:^(BOOL finished) { - XCTAssertTrue(objectTransitionBlockExecuted); - XCTAssertTrue(completionBlockExecuted); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenNotBatchUpdate_thatDelegateEventsSent { - IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; - self.dataSource.sections = @[object]; - [self.collectionView reloadData]; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willDeleteIndexPaths:OCMOCK_ANY collectionView:self.collectionView]; - [[mockDelegate expect] listAdapterUpdater:self.updater willInsertIndexPaths:OCMOCK_ANY collectionView:self.collectionView]; - [[mockDelegate expect] listAdapterUpdater:self.updater - willMoveFromIndexPath:OCMOCK_ANY - toIndexPath:OCMOCK_ANY - collectionView:self.collectionView]; - [[mockDelegate expect] listAdapterUpdater:self.updater willReloadIndexPaths:OCMOCK_ANY collectionView:self.collectionView]; - - // This code is of no use, but it will let UICollectionView synchronize number of items, - // so it will not crash in following updates. https://stackoverflow.com/a/46751421/2977647 - [self.collectionView numberOfItemsInSection:0]; - - object.objects = @[@1, @2]; - [self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[ - [NSIndexPath indexPathForItem:0 inSection:0], - ]]; - object.objects = @[@1, @2, @4, @5]; - [self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[ - [NSIndexPath indexPathForItem:2 inSection:0], - [NSIndexPath indexPathForItem:3 inSection:0], - ]]; - object.objects = @[@2, @1, @4, @5]; - [self.updater moveItemInCollectionView:self.collectionView - fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0] - toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - - [self.updater reloadItemInCollectionView:self.collectionView - fromIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] - toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - [mockDelegate verify]; -} - -- (void)test_whenObjectIdentifiersCollide_withDifferentTypes_thatLookupReturnsNil { - id testObject = [[IGTestObject alloc] initWithKey:@"foo" value:@"bar"]; - id collision = @"foo"; - XCTAssertEqual(collision, [testObject diffIdentifier]); - - IGListExperimentalAdapterUpdater *updater = [IGListExperimentalAdapterUpdater new]; - - // mimic internal map setup in IGListAdapter - NSPointerFunctions *keyFunctions = [updater objectLookupPointerFunctions]; - NSPointerFunctions *valueFunctions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; - NSMapTable *table = [[NSMapTable alloc] initWithKeyPointerFunctions:keyFunctions valuePointerFunctions:valueFunctions capacity:0]; - - [table setObject:@1 forKey:testObject]; - XCTAssertNotNil([table objectForKey:testObject]); - XCTAssertNil([table objectForKey:collision]); -} - -- (void)test_whenReloadIsCalledWithSameItemCount_deleteInsertSectionHappen { - IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] - deleteSections:[NSIndexSet indexSetWithIndex:0] - moveSections:[NSSet new] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[] - moveIndexPaths:@[]]; - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id"]]; - self.dataSource.sections = from; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - return result.inserts.count == 0 && result.deletes.count == 0 && result.moves.count == 0 && result.updates.firstIndex == 0; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenPerformUpdates_dataSourceWasSetToNil_shouldNotCrash { - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"]]; - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id1"], - [IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"]]; - self.dataSource.sections = from; - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - // Manually set the data source to be nil. - self->_collectionView.dataSource = nil; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:^(IGListTransitionData *data) { - } - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenPerformIndexPathUpdates_reloadingTheSameIndexPathMultipleTimes_shouldNotCrash { - // Set up data - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; - self.dataSource.sections = from; - - // Mock delegate to confirm update did work - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:OCMOCK_ANY collectionView:self.collectionView]; - - // Expectation to wait for performUpdate to finish - XCTestExpectation *expectation = genExpectation; - - NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; - [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] - animated:NO - itemUpdates:^{ - [self.updater reloadItemInCollectionView:self.collectionView fromIndexPath:indexPath toIndexPath:indexPath]; - [self.updater reloadItemInCollectionView:self.collectionView fromIndexPath:indexPath toIndexPath:indexPath]; - } - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - - waitExpectation; - - [mockDelegate verify]; -} - -- (void)test_whenPerformingUpdatesMultipleTimesInARow_thenUpdateWorks { - NSArray *objects1 = @[ - [IGSectionObject sectionWithObjects:@[@0]] - ]; - NSArray *objects2 = @[ - [IGSectionObject sectionWithObjects:@[@0, @1]], - [IGSectionObject sectionWithObjects:@[@0, @1]] - ]; - NSArray *objects3 = @[ - [IGSectionObject sectionWithObjects:@[@0, @1]], - [IGSectionObject sectionWithObjects:@[@0, @1]], - [IGSectionObject sectionWithObjects:@[@0, @1]] - ]; - - self.dataSource.sections = objects1; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:objects1 toObjects:objects2] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:objects2 toObjects:objects3] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished2) { - XCTAssertEqual([self.collectionView numberOfSections], 3); - XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); - XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2); - [expectation fulfill]; - }]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenPerformingUpdate_thatCallsDiffingDelegate { - self.updater.experiments |= IGListExperimentBackgroundDiffing; - - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[] identifier:@"0"] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[] identifier:@"0"], - [IGSectionObject sectionWithObjects:@[] identifier:@"1"] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willDiffFromObjects:from toObjects:to]; - [[mockDelegate expect] listAdapterUpdater:self.updater didDiffWithResults:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - return [result.inserts isEqualToIndexSet:[NSIndexSet indexSetWithIndex:1]] - && result.deletes.count == 0 - && result.updates.count == 0 - && result.moves.count == 0; - }] onBackgroundThread:YES]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenCollectionViewSectionCountIsIncorrect_thatDoesNotCrash { - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[]] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - XCTAssertEqual([self.collectionView numberOfSections], 1); - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertEqual([self.collectionView numberOfSections], 2); - [expectation fulfill]; - }]; - - // Lets say we change the dataSource without the updater on accident. - self.dataSource.sections = @[ - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]], - [IGSectionObject sectionWithObjects:@[]] - ]; - - // Lets force the collectionView to sync - [self.collectionView reloadData]; - [self.collectionView layoutIfNeeded]; - XCTAssertEqual([self.collectionView numberOfSections], 3); - - // No we wait for the update, which should fallback to a reload. - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)test_whenNoChanges_thatPerformUpdateExitsEarly { - self.updater.experiments |= IGListExperimentSkipPerformUpdateIfPossible; - - NSArray *from = @[ - [IGSectionObject sectionWithObjects:@[] identifier:@"Foo"] - ]; - NSArray *to = @[ - [IGSectionObject sectionWithObjects:@[] identifier:@"Foo"] - ]; - - self.dataSource.sections = from; - [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; - [self.updater update]; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - - [[mockDelegate expect] listAdapterUpdater:self.updater - willPerformBatchUpdatesWithCollectionView:self.collectionView - fromObjects:from - toObjects:to - listIndexSetResult:OCMOCK_ANY - animated:NO]; - - [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - XCTAssertTrue(finished); - XCTAssertEqual([self.collectionView numberOfSections], 1); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; - [mockDelegate verify]; -} - -# pragma mark - preferItemReloadsFroSectionReloads - -- (void)test_whenReloadIsCalledWithSameItemCount_andPreferItemReload_updateIndexPathsHappen { - self.updater.preferItemReloadsForSectionReloads = YES; - - IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet new] - deleteSections:[NSIndexSet new] - moveSections:[NSSet new] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:0]] - moveIndexPaths:@[]]; - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; - // Update the items - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id"]]; - self.dataSource.sections = from; - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - return result.inserts.count == 0 && result.deletes.count == 0 && result.moves.count == 0 && result.updates.firstIndex == 0; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenReloadIsCalledWithDifferentItemCount_andPreferItemReload_deleteInsertSectionHappen { - self.updater.preferItemReloadsForSectionReloads = YES; - - IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] - deleteSections:[NSIndexSet indexSetWithIndex:0] - moveSections:[NSSet new] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[] - moveIndexPaths:@[]]; - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; - // more items in the section - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@1, @2] identifier:@"id"]]; - self.dataSource.sections = from; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - return result.inserts.count == 0 && result.deletes.count == 0 && result.moves.count == 0 && result.updates.firstIndex == 0; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_andPreferItemReload_deleteInsertMoveHappens { - self.updater.preferItemReloadsForSectionReloads = YES; - - IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] - deleteSections:[NSIndexSet indexSetWithIndex:1] - moveSections:[NSSet setWithArray:@[[[IGListMoveIndex alloc] initWithFrom:0 to:1]]] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[] - moveIndexPaths:@[]]; - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"], - [IGSectionObject sectionWithObjects:@[@2] identifier:@"id2"]]; - // move section, and also update the item for "id2" - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"], - [IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"]]; - self.dataSource.sections = from; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - if (result.inserts.count != 0 || result.deletes.count != 0) { - return NO; - } - // Make sure we note that index 1 is updated (id2 from @[@2] -> @[@22], "id1" moved from section 0 -> 1, and "id2" moved from section 1 -> 0 - return result.updates.firstIndex == 1 && result.moves.count == 2 && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:0 to:1]] && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:1 to:0]]; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_withDifferentSectionLength_andPreferItemReload_deleteInsertMoveHappens { - self.updater.preferItemReloadsForSectionReloads = YES; - - IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] - deleteSections:[NSIndexSet indexSetWithIndex:1] - moveSections:[NSSet setWithArray:@[[[IGListMoveIndex alloc] initWithFrom:0 to:1]]] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[] - moveIndexPaths:@[]]; - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1, @2, @3] identifier:@"id1"], - [IGSectionObject sectionWithObjects:@[@2] identifier:@"id2"]]; - // move section, and also update the item for "id2" - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"], - [IGSectionObject sectionWithObjects:@[@1, @2, @3] identifier:@"id1"]]; - self.dataSource.sections = from; - - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - if (result.inserts.count != 0 || result.deletes.count != 0) { - return NO; - } - // Make sure we note that index 1 is updated (id2 from @[@2] -> @[@22], "id1" moved from section 0 -> 1, and "id2" moved from section 1 -> 0 - return result.updates.firstIndex == 1 && result.moves.count == 2 && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:0 to:1]] && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:1 to:0]]; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - - -- (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_withThreeSections_deleteInsertMoveHappens { - self.updater.preferItemReloadsForSectionReloads = YES; - - IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] - deleteSections:[NSIndexSet indexSetWithIndex:1] - moveSections:[NSSet setWithArray:@[[[IGListMoveIndex alloc] initWithFrom:0 to:1]]] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[] - moveIndexPaths:@[]]; - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"], - [IGSectionObject sectionWithObjects:@[@2] identifier:@"id2"], - [IGSectionObject sectionWithObjects:@[@3] identifier:@"id3"]]; - // move section, and also update the items for "id2" - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@22, @23] identifier:@"id2"], - [IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"], - [IGSectionObject sectionWithObjects:@[@3] identifier:@"id3"]]; - self.dataSource.sections = from; - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - if (result.inserts.count != 0 || result.deletes.count != 0) { - return NO; - } - // Make sure we note that index 1 is updated (id2 from @[@2] -> @[@22], "id1" moved from section 0 -> 1, "id2" moved from section 1 -> 0 - return result.updates.firstIndex == 1 && result.moves.count == 2 && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:0 to:1]] && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:1 to:0]]; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -- (void)test_whenReloadIsCalledWithSectionInsertAndUpdate_andPreferItemReload_noItemReloads { - self.updater.preferItemReloadsForSectionReloads = YES; - - IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)] - deleteSections:[NSIndexSet indexSetWithIndex:0] - moveSections:[NSSet new] - insertIndexPaths:@[] - deleteIndexPaths:@[] - updateIndexPaths:@[] - moveIndexPaths:@[]]; - NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"]]; - NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id1"], - [IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"]]; - self.dataSource.sections = from; - id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; - self.updater.delegate = mockDelegate; - [mockDelegate setExpectationOrderMatters:YES]; - [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { - if (result.deletes.count != 0 || result.moves.count != 0) { - return NO; - } - // Make sure we note that index 1 is updated (id1 from @[@1] -> @[@2]), and "id2" was inserted at index 1 - return result.updates.firstIndex == 0 && result.inserts.firstIndex == 1; - }] animated:NO]; - [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; - - XCTestExpectation *expectation = genExpectation; - - [self.updater performExperimentalUpdateAnimated:NO - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:from toObjects:to] - applyDataBlock:self.applyDataBlock - completion:^(BOOL finished) { - [expectation fulfill]; - }]; - waitExpectation; - [mockDelegate verify]; -} - -@end