diff --git a/Source/IGListKit/IGListExperimentalAdapterUpdater.m b/Source/IGListKit/IGListExperimentalAdapterUpdater.m index 7682ad6a..ee31467c 100644 --- a/Source/IGListKit/IGListExperimentalAdapterUpdater.m +++ b/Source/IGListKit/IGListExperimentalAdapterUpdater.m @@ -6,7 +6,6 @@ */ #import "IGListExperimentalAdapterUpdater.h" -#import "IGListExperimentalAdapterUpdaterInternal.h" #import @@ -15,11 +14,14 @@ #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 IGListExperimentalAdapterUpdater () +@property (nonatomic, strong) IGListUpdateTransactionBuilder *transactionBuilder; +@property (nonatomic, strong, nullable) id transaction; +@end @implementation IGListExperimentalAdapterUpdater @@ -42,290 +44,13 @@ typedef void (^IGListAdapterUpdaterCompletionBlock)(BOOL); return self; } -#pragma mark - Private API +#pragma mark - Update - (BOOL)hasChanges { return [self.transactionBuilder hasChanges]; } -- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { - IGAssertMainThread(); - - id delegate = self.delegate; - void (^reloadUpdates)(void) = [self.transactionBuilder reloadBlock]; - NSArray *itemUpdateBlocks = [self.transactionBuilder itemUpdateBlocks]; - NSArray *completionBlocks = [self.transactionBuilder completionBlocks]; - - [self cleanStateBeforeUpdates]; - - void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { - for (IGListUpdatingCompletion block in completionBlocks) { - block(finished); - } - - // Execute any completion blocks from item updates. Added after item blocks are executed in order to capture any - // re-entrant updates. - NSArray *inUpdateCompletionBlocks = [self.inUpdateCompletionBlocks copy]; - for (IGListUpdatingCompletion block in inUpdateCompletionBlocks) { - block(finished); - } - - [self _cleanStateAfterUpdates]; - 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) { - executeCompletionBlocks(NO); - [_delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView]; - 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 itemUpdateBlocks) { - itemUpdateBlock(); - } - - self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; - - [delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView isFallbackReload:NO]; - [collectionView reloadData]; - [collectionView.collectionViewLayout invalidateLayout]; - [collectionView layoutIfNeeded]; - [delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView isFallbackReload:NO]; - - executeCompletionBlocks(YES); -} - -- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { - 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; - IGListTransitionDataBlock dataBlock = [self.transactionBuilder dataBlock]; - IGListTransitionDataApplyBlock applyDataBlock = [self.transactionBuilder applyDataBlock]; - NSArray *completionBlocks = [self.transactionBuilder completionBlocks]; - const BOOL animated = [self.transactionBuilder animated]; - const BOOL allowsReloadingOnTooManyUpdates = self.allowsReloadingOnTooManyUpdates; - const IGListExperiment experiments = self.experiments; - NSArray *itemUpdateBlocks = [self.transactionBuilder itemUpdateBlocks]; - - // 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) { - for (IGListUpdatingCompletion block in completionBlocks) { - block(finished); - } - - // Execute any completion blocks from item updates. Added after item blocks are executed in order to capture any - // re-entrant updates. - NSArray *inUpdateCompletionBlocks = [self.inUpdateCompletionBlocks copy]; - for (IGListUpdatingCompletion block in inUpdateCompletionBlocks) { - block(finished); - } - - [self _cleanStateAfterUpdates]; - self.applyingUpdateData = nil; - 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) { - executeCompletionBlocks(NO); - [_delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView]; - return; - } - - - IGListTransitionData *data = nil; - if (dataBlock != nil) { - data = dataBlock(); - } - - NSArray *toObjects = data.toObjects; - NSArray *fromObjects = data.fromObjects; - -#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); - } -#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 (applyDataBlock != nil && data != nil) { - applyDataBlock(data); - } - - // 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 itemUpdateBlocks) { - itemUpdateBlock(); - } - - self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; - }; - - void (^reloadDataFallback)(void) = ^{ - [delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView isFallbackReload:YES]; - executeUpdateBlocks(); - [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]; - }; - - // disables multiple performBatchUpdates: from happening at the same time - self.state = IGListBatchUpdateStateQueuedBatchUpdate; - - // 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(); - 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.inUpdateItemCollector.sectionReloads, - self.inUpdateItemCollector.itemInserts, - self.inUpdateItemCollector.itemDeletes, - self.inUpdateItemCollector.itemReloads, - self.inUpdateItemCollector.itemMoves, - fromObjects, - self.sectionMovesAsDeletesInserts, - self.preferItemReloadsForSectionReloads); - } - }; - - // 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)cleanStateBeforeUpdates { - _transactionBuilder = [IGListUpdateTransactionBuilder new]; - - self.inUpdateCompletionBlocks = [NSMutableArray new]; - self.inUpdateItemCollector = [IGListItemUpdatesCollector new]; -} - -- (void)_cleanStateAfterUpdates { - self.inUpdateCompletionBlocks = nil; - self.inUpdateItemCollector = nil; -} - -- (void)_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { +- (void)_queueUpdate { IGAssertMainThread(); __weak __typeof__(self) weakSelf = self; @@ -335,19 +60,52 @@ willPerformBatchUpdatesWithCollectionView:collectionView // 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]) { - return; - } - - if (weakSelf.transactionBuilder.hasReloadData) { - [weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock]; - } else { - [weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock]; - } + [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, + .allowBackgroundDiffing = IGListExperimentEnabled(_experiments, IGListExperimentBackgroundDiffing), + }; + + id transaction = [self.transactionBuilder buildWithConfig:config delegate:_delegate updater:self]; + self.transaction = transaction; + 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) { + if (weakSelf.transaction == weakTransaction) { + weakSelf.transaction = nil; + // queue another update in case something changed during batch updates. this method will bail next runloop if + // there are no changes + [weakSelf _queueUpdate]; + } + }]; + [transaction begin]; +} + #pragma mark - IGListUpdatingDelegate static BOOL IGListIsEqual(const void *a, const void *b, NSUInteger (*size)(const void *item)) { @@ -371,20 +129,20 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons } - (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 { + collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + dataBlock:(IGListTransitionDataBlock)dataBlock + applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock + completion:(IGListUpdatingCompletion)completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil); IGParameterAssert(dataBlock != nil); @@ -396,23 +154,23 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons applyDataBlock:applyDataBlock completion:completion]; - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; + [self _queueUpdate]; } - (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); // 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.inUpdateCompletionBlocks addObject:completion]; + [self.transaction addCompletionBlock:completion]; } itemUpdates(); } else { @@ -421,16 +179,30 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons itemUpdates:itemUpdates completion:completion]; - [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; + [self _queueUpdate]; } } +- (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 _queueUpdate]; +} + - (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { IGAssertMainThread(); IGParameterAssert(collectionView != nil); IGParameterAssert(indexPaths != nil); - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.inUpdateItemCollector.itemInserts addObjectsFromArray:indexPaths]; + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.transaction insertItemsAtIndexPaths:indexPaths]; } else { [self.delegate listAdapterUpdater:self willInsertIndexPaths:indexPaths collectionView:collectionView]; [collectionView insertItemsAtIndexPaths:indexPaths]; @@ -441,8 +213,8 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons IGAssertMainThread(); IGParameterAssert(collectionView != nil); IGParameterAssert(indexPaths != nil); - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.inUpdateItemCollector.itemDeletes addObjectsFromArray:indexPaths]; + if (self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.transaction deleteItemsAtIndexPaths:indexPaths]; } else { [self.delegate listAdapterUpdater:self willDeleteIndexPaths:indexPaths collectionView:collectionView]; [collectionView deleteItemsAtIndexPaths:indexPaths]; @@ -452,9 +224,8 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons - (void)moveItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:fromIndexPath to:toIndexPath]; - [self.inUpdateItemCollector.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]; @@ -464,15 +235,26 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons - (void)reloadItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - IGListReloadIndexPath *reload = [[IGListReloadIndexPath alloc] initWithFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; - [self.inUpdateItemCollector.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 { @@ -513,60 +295,4 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons }]; } -- (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 _queueUpdateWithCollectionViewBlock:collectionViewBlock]; -} - -- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { - IGAssertMainThread(); - IGParameterAssert(collectionView != nil); - IGParameterAssert(sections != nil); - if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { - [self.inUpdateItemCollector.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/Internal/IGListBatchUpdateTransaction.h b/Source/IGListKit/Internal/IGListBatchUpdateTransaction.h new file mode 100644 index 00000000..72449e67 --- /dev/null +++ b/Source/IGListKit/Internal/IGListBatchUpdateTransaction.h @@ -0,0 +1,40 @@ +/* +* 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 + +#import "IGListUpdateTransactable.h" + +@protocol IGListAdapterUpdaterCompatible; +@protocol IGListAdapterUpdaterDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/// Handles a batch update transaction +IGLK_SUBCLASSING_RESTRICTED +@interface IGListBatchUpdateTransaction : NSObject + +- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + updater:(id)updater + delegate:(nullable id)delegate + config:(IGListUpdateTransactationConfig)config + animated:(BOOL)animated + dataBlock:(nullable IGListTransitionDataBlock)dataBlock + applyDataBlock:(nullable IGListTransitionDataApplyBlock)applyDataBlock + itemUpdateBlocks:(NSArray *)itemUpdateBlocks + completionBlocks:(NSArray *)completionBlocks NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/Internal/IGListBatchUpdateTransaction.m b/Source/IGListKit/Internal/IGListBatchUpdateTransaction.m new file mode 100644 index 00000000..087f602e --- /dev/null +++ b/Source/IGListKit/Internal/IGListBatchUpdateTransaction.m @@ -0,0 +1,300 @@ +/* +* 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 "IGListBatchUpdateTransaction.h" + +#import +#import +#import +#import + +#import "IGListAdapterUpdaterHelpers.h" +#import "IGListIndexSetResultInternal.h" +#import "IGListItemUpdatesCollector.h" +#import "IGListMoveIndexPathInternal.h" +#import "IGListReloadIndexPath.h" +#import "IGListTransitionData.h" +#import "UICollectionView+IGListBatchUpdateData.h" + +@interface IGListBatchUpdateTransaction () +// Given +@property (nonatomic, copy, readonly) IGListCollectionViewBlock collectionViewBlock; +@property (nonatomic, weak, readonly, nullable) id updater; +@property (nonatomic, weak, readonly, nullable) id delegate; +@property (nonatomic, assign, readonly) IGListUpdateTransactationConfig config; +@property (nonatomic, assign, readonly) BOOL animated; +@property (nonatomic, copy, readonly, nullable) IGListTransitionDataBlock dataBlock; +@property (nonatomic, copy, readonly, nullable) IGListTransitionDataApplyBlock applyDataBlock; +@property (nonatomic, copy, readonly) NSArray *itemUpdateBlocks; +@property (nonatomic, copy, readonly) NSArray *completionBlocks; +// Internal +@property (nonatomic, strong, readonly) IGListItemUpdatesCollector *inUpdateItemCollector; +@property (nonatomic, copy, readonly) NSMutableArray *inUpdateCompletionBlocks; +@property (nonatomic, assign, readwrite) IGListBatchUpdateState state; +@property (nonatomic, strong, readwrite, nullable) IGListBatchUpdateData *actualCollectionViewUpdates; +@end + +@implementation IGListBatchUpdateTransaction + +- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + updater:(id)updater + delegate:(nullable id)delegate + config:(IGListUpdateTransactationConfig)config + animated:(BOOL)animated + dataBlock:(nullable IGListTransitionDataBlock)dataBlock + applyDataBlock:(nullable IGListTransitionDataApplyBlock)applyDataBlock + itemUpdateBlocks:(NSArray *)itemUpdateBlocks + completionBlocks:(NSArray *)completionBlocks { + if (self = [super init]) { + _collectionViewBlock = [collectionViewBlock copy]; + _updater = updater; + _delegate = delegate; + _config = config; + _animated = animated; + _dataBlock = [dataBlock copy]; + _applyDataBlock = [applyDataBlock copy]; + _itemUpdateBlocks = [itemUpdateBlocks copy]; + _completionBlocks = [completionBlocks copy]; + + _inUpdateItemCollector = [IGListItemUpdatesCollector new]; + _state = IGListBatchUpdateStateIdle; + } + return self; +} + +#pragma mark - IGListUpdateTransactable + +- (void)begin { + IGListCollectionViewBlock collectionViewBlock = self.collectionViewBlock; + id updater = self.updater; + id delegate = self.delegate; + IGListTransitionDataBlock dataBlock = self.dataBlock; + IGListTransitionDataApplyBlock applyDataBlock = self.applyDataBlock; + NSArray *itemUpdateBlocks = self.itemUpdateBlocks; + NSArray *completionBlocks = self.completionBlocks; + const BOOL animated = self.animated; + const IGListUpdateTransactationConfig config = self.config; + + void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { + for (IGListUpdatingCompletion block in completionBlocks) { + block(finished); + } + + // Execute any completion blocks from item updates. Added after item blocks are executed in order to capture any + // re-entrant updates. + NSArray *inUpdateCompletionBlocks = [self.inUpdateCompletionBlocks copy]; + for (IGListUpdatingCompletion block in inUpdateCompletionBlocks) { + block(finished); + } + + self.actualCollectionViewUpdates = nil; + 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) { + [delegate listAdapterUpdater:updater didFinishWithoutUpdatesWithCollectionView:collectionView]; + executeCompletionBlocks(NO); + return; + } + + + IGListTransitionData *data = nil; + if (dataBlock != nil) { + data = dataBlock(); + } + + NSArray *toObjects = data.toObjects; + NSArray *fromObjects = data.fromObjects; + +#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); + } +#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 (applyDataBlock != nil && data != nil) { + applyDataBlock(data); + } + + // 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 itemUpdateBlocks) { + itemUpdateBlock(); + } + + self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; + }; + + void (^reloadDataFallback)(void) = ^{ + [delegate listAdapterUpdater:updater willReloadDataWithCollectionView:collectionView isFallbackReload:YES]; + executeUpdateBlocks(); + [collectionView reloadData]; + [collectionView layoutIfNeeded]; + [delegate listAdapterUpdater:updater didReloadDataWithCollectionView:collectionView isFallbackReload:YES]; + executeCompletionBlocks(YES); + }; + + // disables multiple performBatchUpdates: from happening at the same time + self.state = IGListBatchUpdateStateQueuedBatchUpdate; + + // 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 (config.allowsBackgroundReloading && collectionView.window == nil) { + reloadDataFallback(); + return; + } + + // block executed in the first param block of -[UICollectionView performBatchUpdates:completion:] + void (^batchUpdatesBlock)(IGListIndexSetResult *result) = ^(IGListIndexSetResult *result){ + executeUpdateBlocks(); + if (config.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.actualCollectionViewUpdates = [[IGListBatchUpdateData alloc] + initWithInsertSections:result.inserts + deleteSections:result.deletes + moveSections:[NSSet setWithArray:result.moves] + insertIndexPaths:@[] + deleteIndexPaths:@[] + updateIndexPaths:@[] + moveIndexPaths:@[]]; + } else { + self.actualCollectionViewUpdates = IGListApplyUpdatesToCollectionView(collectionView, + result, + self.inUpdateItemCollector.sectionReloads, + self.inUpdateItemCollector.itemInserts, + self.inUpdateItemCollector.itemDeletes, + self.inUpdateItemCollector.itemReloads, + self.inUpdateItemCollector.itemMoves, + fromObjects, + config.sectionMovesAsDeletesInserts, + config.preferItemReloadsForSectionReloads); + } + }; + + // block used as the second param of -[UICollectionView performBatchUpdates:completion:] + void (^fallbackWithoutUpdates)(void) = ^(void) { + [delegate listAdapterUpdater:updater didFinishWithoutUpdatesWithCollectionView:collectionView]; + executeCompletionBlocks(NO); + }; + + // block used as the second param of -[UICollectionView performBatchUpdates:completion:] + void (^batchUpdatesCompletionBlock)(BOOL) = ^(BOOL finished) { + IGListBatchUpdateData *oldApplyingUpdateData = self.actualCollectionViewUpdates; + [delegate listAdapterUpdater:updater didPerformBatchUpdates:oldApplyingUpdateData collectionView:collectionView]; + executeCompletionBlocks(finished); + }; + + void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){ + [delegate listAdapterUpdater:updater +willPerformBatchUpdatesWithCollectionView:collectionView + fromObjects:fromObjects + toObjects:toObjects + listIndexSetResult:result + animated:animated]; + + if (animated) { + [collectionView performBatchUpdates:^{ + batchUpdatesBlock(result); + } completion:batchUpdatesCompletionBlock]; + } else { + [UIView performWithoutAnimation:^{ + [collectionView performBatchUpdates:^{ + batchUpdatesBlock(result); + } completion:batchUpdatesCompletionBlock]; + }]; + } + }; + + // block that executes the batch update and exception handling + void (^tryToPerformUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){ + [delegate listAdapterUpdater:updater didDiffWithResults:result onBackgroundThread:config.allowBackgroundDiffing]; + + @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 && config.allowsReloadingOnTooManyUpdates) { + reloadDataFallback(); + } else { + performUpdate(result); + } + } @catch (NSException *exception) { + [delegate listAdapterUpdater:updater + collectionView:collectionView + willCrashWithException:exception + fromObjects:fromObjects + toObjects:toObjects + diffResult:result + updates:(id)self.actualCollectionViewUpdates]; + @throw exception; + } + }; + + [delegate listAdapterUpdater:updater willDiffFromObjects:fromObjects toObjects:toObjects]; + if (config.allowBackgroundDiffing) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + IGListIndexSetResult *result = IGListDiff(fromObjects, toObjects, IGListDiffEquality); + dispatch_async(dispatch_get_main_queue(), ^{ + tryToPerformUpdate(result); + }); + }); + } else { + IGListIndexSetResult *result = IGListDiff(fromObjects, toObjects, IGListDiffEquality); + tryToPerformUpdate(result); + } +} + +- (void)addCompletionBlock:(IGListUpdatingCompletion)completion { + if (!self.inUpdateCompletionBlocks) { + _inUpdateCompletionBlocks = [NSMutableArray new]; + } + [self.inUpdateCompletionBlocks addObject:completion]; +} + +#pragma mark - Item updates + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { + [self.inUpdateItemCollector.itemInserts addObjectsFromArray:indexPaths]; +} + +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { + [self.inUpdateItemCollector.itemDeletes addObjectsFromArray:indexPaths]; +} + +- (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { + IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:fromIndexPath to:toIndexPath]; + [self.inUpdateItemCollector.itemMoves addObject:move]; +} + +- (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { + IGListReloadIndexPath *reload = [[IGListReloadIndexPath alloc] initWithFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; + [self.inUpdateItemCollector.itemReloads addObject:reload]; +} + +- (void)reloadSections:(NSIndexSet *)sections { + [self.inUpdateItemCollector.sectionReloads addIndexes:sections]; +} + +@end diff --git a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdaterInternal.h b/Source/IGListKit/Internal/IGListExperimentalAdapterUpdaterInternal.h index 962d9427..9a72773e 100644 --- a/Source/IGListKit/Internal/IGListExperimentalAdapterUpdaterInternal.h +++ b/Source/IGListKit/Internal/IGListExperimentalAdapterUpdaterInternal.h @@ -8,31 +8,18 @@ #import #import -#import -#import - #import "IGListExperimentalAdapterUpdater.h" #import "IGListBatchUpdateState.h" -#import "IGListItemUpdatesCollector.h" -#import "IGListUpdateTransactionBuilder.h" NS_ASSUME_NONNULL_BEGIN @interface IGListExperimentalAdapterUpdater () -@property (nonatomic, strong, readonly) IGListUpdateTransactionBuilder *transactionBuilder; - -@property (nonatomic, strong) NSMutableArray *inUpdateCompletionBlocks; -@property (nonatomic, strong) IGListItemUpdatesCollector *inUpdateItemCollector; - -@property (nonatomic, assign) IGListBatchUpdateState state; -@property (nonatomic, strong, nullable) IGListBatchUpdateData *applyingUpdateData; - -- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock; -- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock; -- (void)cleanStateBeforeUpdates; - (BOOL)hasChanges; +/// Force an update to start +- (void)update; + @end NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/Internal/IGListReloadTransaction.h b/Source/IGListKit/Internal/IGListReloadTransaction.h new file mode 100644 index 00000000..182e3ab5 --- /dev/null +++ b/Source/IGListKit/Internal/IGListReloadTransaction.h @@ -0,0 +1,36 @@ +/* +* 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 "IGListUpdateTransactable.h" + +@protocol IGListAdapterUpdaterCompatible; +@protocol IGListAdapterUpdaterDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/// Handles a full reload transaction +IGLK_SUBCLASSING_RESTRICTED +@interface IGListReloadTransaction : NSObject + +- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + updater:(id)updater + delegate:(nullable id)delegate + reloadBlock:(IGListReloadUpdateBlock)reloadBlock + itemUpdateBlocks:(NSArray *)itemUpdateBlocks + completionBlocks:(NSArray *)completionBlocks NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/Internal/IGListReloadTransaction.m b/Source/IGListKit/Internal/IGListReloadTransaction.m new file mode 100644 index 00000000..8618292e --- /dev/null +++ b/Source/IGListKit/Internal/IGListReloadTransaction.m @@ -0,0 +1,133 @@ +/* +* 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 "IGListReloadTransaction.h" + +#import + +@interface IGListReloadTransaction () +// Given +@property (nonatomic, copy, readonly) IGListCollectionViewBlock collectionViewBlock; +@property (nonatomic, weak, readonly, nullable) id updater; +@property (nonatomic, weak, readonly, nullable) id delegate; +@property (nonatomic, copy, readonly) IGListReloadUpdateBlock reloadBlock; +@property (nonatomic, copy, readonly) NSArray *itemUpdateBlocks; +@property (nonatomic, copy, readonly) NSArray *completionBlocks; +// Internal +@property (nonatomic, assign, readwrite) IGListBatchUpdateState state; +@property (nonatomic, copy, readonly) NSMutableArray *inUpdateCompletionBlocks; +@end + +@implementation IGListReloadTransaction + +- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + updater:(id)updater + delegate:(nullable id)delegate + reloadBlock:(IGListReloadUpdateBlock)reloadBlock + itemUpdateBlocks:(NSArray *)itemUpdateBlocks + completionBlocks:(NSArray *)completionBlocks { + if (self = [super init]) { + _collectionViewBlock = [collectionViewBlock copy]; + _updater = updater; + _delegate = delegate; + _reloadBlock = [reloadBlock copy]; + _itemUpdateBlocks = [itemUpdateBlocks copy]; + _completionBlocks = [completionBlocks copy]; + + _state = IGListBatchUpdateStateIdle; + } + return self; +} + +#pragma mark - IGListUpdateTransactable + +- (void)begin { + IGListCollectionViewBlock collectionViewBlock = self.collectionViewBlock; + id updater = self.updater; + id delegate = self.delegate; + void (^reloadUpdates)(void) = self.reloadBlock; + NSArray *itemUpdateBlocks = self.itemUpdateBlocks; + NSArray *completionBlocks = self.completionBlocks; + + void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { + for (IGListUpdatingCompletion block in completionBlocks) { + block(finished); + } + + // Execute any completion blocks from item updates. Added after item blocks are executed in order to capture any + // re-entrant updates. + NSArray *inUpdateCompletionBlocks = [self.inUpdateCompletionBlocks copy]; + for (IGListUpdatingCompletion block in inUpdateCompletionBlocks) { + 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) { + executeCompletionBlocks(NO); + [delegate listAdapterUpdater:updater didFinishWithoutUpdatesWithCollectionView:collectionView]; + 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 itemUpdateBlocks) { + itemUpdateBlock(); + } + + self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; + + [delegate listAdapterUpdater:updater willReloadDataWithCollectionView:collectionView isFallbackReload:NO]; + [collectionView reloadData]; + [collectionView.collectionViewLayout invalidateLayout]; + [collectionView layoutIfNeeded]; + [delegate listAdapterUpdater:updater didReloadDataWithCollectionView:collectionView isFallbackReload:NO]; + + executeCompletionBlocks(YES); +} + +- (void)addCompletionBlock:(IGListUpdatingCompletion)completion { + if (!self.inUpdateCompletionBlocks) { + _inUpdateCompletionBlocks = [NSMutableArray new]; + } + [self.inUpdateCompletionBlocks addObject:completion]; +} + +#pragma mark - Item updates + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { + // no-op. Reloading all cells. +} + +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { + // no-op. Reloading all cells. +} + +- (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { + // no-op. Reloading all cells. +} + +- (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { + // no-op. Reloading all cells. +} + +- (void)reloadSections:(NSIndexSet *)sections { + // no-op. Reloading all cells. +} + +@end diff --git a/Source/IGListKit/Internal/IGListUpdateTransactable.h b/Source/IGListKit/Internal/IGListUpdateTransactable.h new file mode 100644 index 00000000..10c08ff7 --- /dev/null +++ b/Source/IGListKit/Internal/IGListUpdateTransactable.h @@ -0,0 +1,45 @@ +/* +* 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 "IGListBatchUpdateState.h" +#import "IGListUpdatingDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Config to customize how the transition works. +typedef struct { + BOOL sectionMovesAsDeletesInserts; + BOOL singleItemSectionUpdates; + BOOL preferItemReloadsForSectionReloads; + BOOL allowsBackgroundReloading; + BOOL allowsReloadingOnTooManyUpdates; + BOOL allowBackgroundDiffing; +} IGListUpdateTransactationConfig; + +/// Conform to this protocol to handle an update transaction. +@protocol IGListUpdateTransactable + +/// Begin the transaction. We expect all completion blocks to be called once finished. +- (void)begin; + +/// Current state of the transaction +- (IGListBatchUpdateState)state; + +/// Add a completion block to complete once the transaction ends +- (void)addCompletionBlock:(IGListUpdatingCompletion)completion; + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; +- (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; +- (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; +- (void)reloadSections:(NSIndexSet *)sections; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h b/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h index 95f992db..254b4c3e 100644 --- a/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h +++ b/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h @@ -11,8 +11,10 @@ #import #import +#import "IGListUpdateTransactable.h" + +@protocol IGListAdapterUpdaterCompatible; @protocol IGListAdapterUpdaterDelegate; -@protocol IGListAdapterUpdating; NS_ASSUME_NONNULL_BEGIN @@ -59,19 +61,13 @@ Completely reload data in the collection. reloadBlock:(IGListReloadUpdateBlock)reloadBlock completion:(nullable IGListUpdatingCompletion)completion; -// Batch updates -- (nullable IGListTransitionDataBlock)dataBlock; -- (nullable IGListTransitionDataApplyBlock)applyDataBlock; -- (NSMutableArray *)itemUpdateBlocks; -- (BOOL)animated; +/** + Build a transaction based on the changes addded. + */ +- (nullable id)buildWithConfig:(IGListUpdateTransactationConfig)config + delegate:(nullable id)delegate + updater:(id)updater; -// Reload -- (BOOL)hasReloadData; -- (nullable IGListReloadUpdateBlock)reloadBlock; - -// Both -- (nullable IGListCollectionViewBlock)collectionViewBlock; -- (NSMutableArray *)completionBlocks; - (BOOL)hasChanges; @end diff --git a/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.m b/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.m index fdab2026..c63a0531 100644 --- a/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.m +++ b/Source/IGListKit/Internal/IGListUpdateTransactionBuilder.m @@ -7,6 +7,9 @@ #import "IGListUpdateTransactionBuilder.h" +#import "IGListBatchUpdateTransaction.h" +#import "IGListReloadTransaction.h" + @interface IGListUpdateTransactionBuilder () // Batch updates @property (nonatomic, copy, readwrite, nullable) IGListTransitionDataBlock dataBlock; @@ -92,4 +95,36 @@ || self.dataBlock != nil; } +- (nullable id)buildWithConfig:(IGListUpdateTransactationConfig)config + delegate:(nullable id)delegate + updater:(id)updater { + IGListCollectionViewBlock collectionViewBlock = _collectionViewBlock; + if (!collectionViewBlock) { + return nil; + } + + if (_hasReloadData) { + IGListReloadUpdateBlock reloadBlock = self.reloadBlock; + if (!reloadBlock) { + return nil; + } + return [[IGListReloadTransaction alloc] initWithCollectionViewBlock:collectionViewBlock + updater:updater + delegate:delegate + reloadBlock:reloadBlock + itemUpdateBlocks:self.itemUpdateBlocks + completionBlocks:self.completionBlocks]; + } else { + return [[IGListBatchUpdateTransaction alloc] initWithCollectionViewBlock:collectionViewBlock + updater:updater + delegate:delegate + config:config + animated:self.animated + dataBlock:self.dataBlock + applyDataBlock:self.applyDataBlock + itemUpdateBlocks:self.itemUpdateBlocks + completionBlocks:self.completionBlocks]; + } +} + @end diff --git a/Tests/IGListExperimentalAdapterUpdaterTests.m b/Tests/IGListExperimentalAdapterUpdaterTests.m index 8aa77548..43cba30e 100644 --- a/Tests/IGListExperimentalAdapterUpdaterTests.m +++ b/Tests/IGListExperimentalAdapterUpdaterTests.m @@ -97,34 +97,27 @@ XCTAssertTrue([self.updater hasChanges]); } -- (void)test_whenCleaningUpState_withChanges_thatUpdaterHasNoChanges { - [self.updater performExperimentalUpdateAnimated:YES - collectionViewBlock:[self collectionViewBlock] - dataBlock:[self dataBlockFromObjects:@[] toObjects:@[@0]] - applyDataBlock:self.applyDataBlock - 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. } @@ -138,7 +131,8 @@ ]; 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; @@ -163,7 +157,8 @@ ]; 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; @@ -188,7 +183,8 @@ ]; 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); @@ -218,7 +214,8 @@ ]; 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); @@ -243,7 +240,8 @@ [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); @@ -269,7 +267,8 @@ ]; 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 @@ -298,7 +297,8 @@ ]; self.dataSource.sections = from; - [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; + [self.updater update]; __block NSInteger completionCounter = 0; @@ -403,7 +403,8 @@ ]; 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; @@ -473,7 +474,8 @@ 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]; } @@ -482,7 +484,8 @@ 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]; } @@ -511,7 +514,8 @@ ]; IGListExperimentalAdapterUpdater *updater = [IGListExperimentalAdapterUpdater 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); @@ -520,7 +524,8 @@ [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); @@ -886,7 +891,8 @@ ]; 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 performExperimentalUpdateAnimated:YES @@ -925,7 +931,8 @@ ]; 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;