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:
Maxime Ollivier 2020-09-08 09:06:16 -07:00 committed by Facebook GitHub Bot
parent bba6b252a4
commit 13ad185227
10 changed files with 738 additions and 433 deletions

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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;