mirror of
https://github.com/Instagram/IGListKit
synced 2026-05-23 09:18:29 +00:00
create update transactions
Summary: Lets move things to the right transaction: * `performBatchUpdate` path to `IGListBatchUpdateTransaction` * `reloadData` path to `IGListReloadTransaction` Reviewed By: patters Differential Revision: D23145770 fbshipit-source-id: e80fc05d2783e165354a147453083b449c92a61c
This commit is contained in:
parent
bba6b252a4
commit
13ad185227
10 changed files with 738 additions and 433 deletions
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
#import "IGListExperimentalAdapterUpdater.h"
|
||||
#import "IGListExperimentalAdapterUpdaterInternal.h"
|
||||
|
||||
#import <IGListDiffKit/IGListAssert.h>
|
||||
|
||||
|
|
@ -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<IGListUpdateTransactable> 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<IGListAdapterUpdaterDelegate> 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<IGListAdapterUpdaterDelegate> 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<IGListUpdateTransactable> 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 <NSIndexPath *> *)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<id<IGListDiffable>> *_Nullable oldArray,
|
||||
NSArray<id<IGListDiffable>> *_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
|
||||
|
|
|
|||
40
Source/IGListKit/Internal/IGListBatchUpdateTransaction.h
Normal file
40
Source/IGListKit/Internal/IGListBatchUpdateTransaction.h
Normal file
|
|
@ -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 <Foundation/Foundation.h>
|
||||
|
||||
#import <IGListDiffKit/IGListMacros.h>
|
||||
#import <IGListKit/IGListUpdatingDelegate.h>
|
||||
#import <IGListKit/IGListUpdatingDelegateExperimental.h>
|
||||
|
||||
#import "IGListUpdateTransactable.h"
|
||||
|
||||
@protocol IGListAdapterUpdaterCompatible;
|
||||
@protocol IGListAdapterUpdaterDelegate;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Handles a batch update transaction
|
||||
IGLK_SUBCLASSING_RESTRICTED
|
||||
@interface IGListBatchUpdateTransaction : NSObject <IGListUpdateTransactable>
|
||||
|
||||
- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
|
||||
updater:(id<IGListAdapterUpdaterCompatible>)updater
|
||||
delegate:(nullable id<IGListAdapterUpdaterDelegate>)delegate
|
||||
config:(IGListUpdateTransactationConfig)config
|
||||
animated:(BOOL)animated
|
||||
dataBlock:(nullable IGListTransitionDataBlock)dataBlock
|
||||
applyDataBlock:(nullable IGListTransitionDataApplyBlock)applyDataBlock
|
||||
itemUpdateBlocks:(NSArray<IGListItemUpdateBlock> *)itemUpdateBlocks
|
||||
completionBlocks:(NSArray<IGListUpdatingCompletion> *)completionBlocks NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
300
Source/IGListKit/Internal/IGListBatchUpdateTransaction.m
Normal file
300
Source/IGListKit/Internal/IGListBatchUpdateTransaction.m
Normal file
|
|
@ -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 <IGListDiffKit/IGListAssert.h>
|
||||
#import <IGListDiffKit/IGListDiffable.h>
|
||||
#import <IGListDiffKit/IGListDiff.h>
|
||||
#import <IGListKit/IGListAdapterUpdaterDelegate.h>
|
||||
|
||||
#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<IGListAdapterUpdaterCompatible> updater;
|
||||
@property (nonatomic, weak, readonly, nullable) id<IGListAdapterUpdaterDelegate> 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<IGListItemUpdateBlock> *itemUpdateBlocks;
|
||||
@property (nonatomic, copy, readonly) NSArray<IGListUpdatingCompletion> *completionBlocks;
|
||||
// Internal
|
||||
@property (nonatomic, strong, readonly) IGListItemUpdatesCollector *inUpdateItemCollector;
|
||||
@property (nonatomic, copy, readonly) NSMutableArray<IGListUpdatingCompletion> *inUpdateCompletionBlocks;
|
||||
@property (nonatomic, assign, readwrite) IGListBatchUpdateState state;
|
||||
@property (nonatomic, strong, readwrite, nullable) IGListBatchUpdateData *actualCollectionViewUpdates;
|
||||
@end
|
||||
|
||||
@implementation IGListBatchUpdateTransaction
|
||||
|
||||
- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
|
||||
updater:(id<IGListAdapterUpdaterCompatible>)updater
|
||||
delegate:(nullable id<IGListAdapterUpdaterDelegate>)delegate
|
||||
config:(IGListUpdateTransactationConfig)config
|
||||
animated:(BOOL)animated
|
||||
dataBlock:(nullable IGListTransitionDataBlock)dataBlock
|
||||
applyDataBlock:(nullable IGListTransitionDataApplyBlock)applyDataBlock
|
||||
itemUpdateBlocks:(NSArray<IGListItemUpdateBlock> *)itemUpdateBlocks
|
||||
completionBlocks:(NSArray<IGListUpdatingCompletion> *)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<IGListAdapterUpdaterCompatible> updater = self.updater;
|
||||
id<IGListAdapterUpdaterDelegate> delegate = self.delegate;
|
||||
IGListTransitionDataBlock dataBlock = self.dataBlock;
|
||||
IGListTransitionDataApplyBlock applyDataBlock = self.applyDataBlock;
|
||||
NSArray<IGListItemUpdateBlock> *itemUpdateBlocks = self.itemUpdateBlocks;
|
||||
NSArray<IGListUpdatingCompletion> *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 <NSIndexPath *> *)indexPaths {
|
||||
[self.inUpdateItemCollector.itemInserts addObjectsFromArray:indexPaths];
|
||||
}
|
||||
|
||||
- (void)deleteItemsAtIndexPaths:(NSArray <NSIndexPath *> *)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
|
||||
|
|
@ -8,31 +8,18 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
#import <IGListDiffKit/IGListMoveIndexPath.h>
|
||||
#import <IGListKit/IGListUpdatingDelegateExperimental.h>
|
||||
|
||||
#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<IGListUpdatingCompletion> *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
|
||||
|
|
|
|||
36
Source/IGListKit/Internal/IGListReloadTransaction.h
Normal file
36
Source/IGListKit/Internal/IGListReloadTransaction.h
Normal file
|
|
@ -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 <Foundation/Foundation.h>
|
||||
|
||||
#import <IGListDiffKit/IGListMacros.h>
|
||||
#import <IGListKit/IGListUpdatingDelegate.h>
|
||||
|
||||
#import "IGListUpdateTransactable.h"
|
||||
|
||||
@protocol IGListAdapterUpdaterCompatible;
|
||||
@protocol IGListAdapterUpdaterDelegate;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Handles a full reload transaction
|
||||
IGLK_SUBCLASSING_RESTRICTED
|
||||
@interface IGListReloadTransaction : NSObject <IGListUpdateTransactable>
|
||||
|
||||
- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
|
||||
updater:(id<IGListAdapterUpdaterCompatible>)updater
|
||||
delegate:(nullable id<IGListAdapterUpdaterDelegate>)delegate
|
||||
reloadBlock:(IGListReloadUpdateBlock)reloadBlock
|
||||
itemUpdateBlocks:(NSArray<IGListItemUpdateBlock> *)itemUpdateBlocks
|
||||
completionBlocks:(NSArray<IGListUpdatingCompletion> *)completionBlocks NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
133
Source/IGListKit/Internal/IGListReloadTransaction.m
Normal file
133
Source/IGListKit/Internal/IGListReloadTransaction.m
Normal file
|
|
@ -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 <IGListKit/IGListAdapterUpdaterDelegate.h>
|
||||
|
||||
@interface IGListReloadTransaction ()
|
||||
// Given
|
||||
@property (nonatomic, copy, readonly) IGListCollectionViewBlock collectionViewBlock;
|
||||
@property (nonatomic, weak, readonly, nullable) id<IGListAdapterUpdaterCompatible> updater;
|
||||
@property (nonatomic, weak, readonly, nullable) id<IGListAdapterUpdaterDelegate> delegate;
|
||||
@property (nonatomic, copy, readonly) IGListReloadUpdateBlock reloadBlock;
|
||||
@property (nonatomic, copy, readonly) NSArray<IGListItemUpdateBlock> *itemUpdateBlocks;
|
||||
@property (nonatomic, copy, readonly) NSArray<IGListUpdatingCompletion> *completionBlocks;
|
||||
// Internal
|
||||
@property (nonatomic, assign, readwrite) IGListBatchUpdateState state;
|
||||
@property (nonatomic, copy, readonly) NSMutableArray<IGListUpdatingCompletion> *inUpdateCompletionBlocks;
|
||||
@end
|
||||
|
||||
@implementation IGListReloadTransaction
|
||||
|
||||
- (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
|
||||
updater:(id<IGListAdapterUpdaterCompatible>)updater
|
||||
delegate:(nullable id<IGListAdapterUpdaterDelegate>)delegate
|
||||
reloadBlock:(IGListReloadUpdateBlock)reloadBlock
|
||||
itemUpdateBlocks:(NSArray<IGListItemUpdateBlock> *)itemUpdateBlocks
|
||||
completionBlocks:(NSArray<IGListUpdatingCompletion> *)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<IGListAdapterUpdaterCompatible> updater = self.updater;
|
||||
id<IGListAdapterUpdaterDelegate> 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 <NSIndexPath *> *)indexPaths {
|
||||
// no-op. Reloading all cells.
|
||||
}
|
||||
|
||||
- (void)deleteItemsAtIndexPaths:(NSArray <NSIndexPath *> *)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
|
||||
45
Source/IGListKit/Internal/IGListUpdateTransactable.h
Normal file
45
Source/IGListKit/Internal/IGListUpdateTransactable.h
Normal file
|
|
@ -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 <Foundation/Foundation.h>
|
||||
|
||||
#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 <NSObject>
|
||||
|
||||
/// 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 <NSIndexPath *> *)indexPaths;
|
||||
- (void)deleteItemsAtIndexPaths:(NSArray <NSIndexPath *> *)indexPaths;
|
||||
- (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath;
|
||||
- (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath;
|
||||
- (void)reloadSections:(NSIndexSet *)sections;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -11,8 +11,10 @@
|
|||
#import <IGListKit/IGListUpdatingDelegate.h>
|
||||
#import <IGListKit/IGListUpdatingDelegateExperimental.h>
|
||||
|
||||
#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<IGListItemUpdateBlock> *)itemUpdateBlocks;
|
||||
- (BOOL)animated;
|
||||
/**
|
||||
Build a transaction based on the changes addded.
|
||||
*/
|
||||
- (nullable id<IGListUpdateTransactable>)buildWithConfig:(IGListUpdateTransactationConfig)config
|
||||
delegate:(nullable id<IGListAdapterUpdaterDelegate>)delegate
|
||||
updater:(id<IGListAdapterUpdaterCompatible>)updater;
|
||||
|
||||
// Reload
|
||||
- (BOOL)hasReloadData;
|
||||
- (nullable IGListReloadUpdateBlock)reloadBlock;
|
||||
|
||||
// Both
|
||||
- (nullable IGListCollectionViewBlock)collectionViewBlock;
|
||||
- (NSMutableArray<IGListUpdatingCompletion> *)completionBlocks;
|
||||
- (BOOL)hasChanges;
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -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<IGListUpdateTransactable>)buildWithConfig:(IGListUpdateTransactationConfig)config
|
||||
delegate:(nullable id<IGListAdapterUpdaterDelegate>)delegate
|
||||
updater:(id<IGListAdapterUpdaterCompatible>)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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue