mirror of
https://github.com/Instagram/IGListKit
synced 2026-05-22 16:58:23 +00:00
Summary: Adding an API to do item-level (cell) moves on the collection view. This complicates things a little bit because of all the issues that moving sections have while in batch updates (e.g. simultaneous animation UICV bugs). Thankfully we use pretty strict types so the compiler does most of the work for us. Closes #145 - [x] Tests build and pass - [x] Add `IGListBatchUpdateData` tests to check moves during - [x] ~~Moving within a reloaded section (no op)~~ can't reload sections - [x] Moving within a deleted section (no op) - [x] Moving within a moved section (convert section ops to delete+insert) - [x] Moving an index path that is also reloaded (convert to delete+insert path) - [x] Add move unit tests to `IGListAdapterUpdater` - [x] Add move unit tests to `IGListReloadDataUpdater` (mostly for code coverage...) - [x] Add move unit tests to `IGListStackedSectionController` - [x] Add `CHANGELOG.md` entry for 3.0.0 - [x] Test moving without batch Closes https://github.com/Instagram/IGListKit/pull/418 Reviewed By: jessesquires Differential Revision: D4521732 Pulled By: rnystrom fbshipit-source-id: 99a46d1cbb0cc1f857a62ff6ca257aff6e8b7f25
517 lines
21 KiB
Objective-C
517 lines
21 KiB
Objective-C
/**
|
|
* Copyright (c) 2016-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "IGListAdapterUpdater.h"
|
|
#import "IGListAdapterUpdaterInternal.h"
|
|
|
|
#import <IGListKit/IGListAssert.h>
|
|
#import <IGListKit/IGListBatchUpdateData.h>
|
|
#import <IGListKit/IGListDiff.h>
|
|
|
|
#import "UICollectionView+IGListBatchUpdateData.h"
|
|
#import "IGListMoveIndexPathInternal.h"
|
|
|
|
@implementation IGListAdapterUpdater
|
|
|
|
- (instancetype)init {
|
|
IGAssertMainThread();
|
|
|
|
if (self = [super init]) {
|
|
// the default is to use animations unless NO is passed
|
|
_queuedUpdateIsAnimated = YES;
|
|
|
|
_completionBlocks = [NSMutableArray new];
|
|
_itemUpdateBlocks = [NSMutableArray new];
|
|
|
|
_reloadSections = [NSMutableIndexSet new];
|
|
|
|
_deleteIndexPaths = [NSMutableSet new];
|
|
_insertIndexPaths = [NSMutableSet new];
|
|
_moveIndexPaths = [NSMutableSet new];
|
|
_reloadIndexPaths = [NSMutableSet new];
|
|
|
|
_allowsBackgroundReloading = YES;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
|
|
#pragma mark - Private API
|
|
|
|
- (BOOL)hasChanges {
|
|
return self.hasQueuedReloadData
|
|
|| self.itemUpdateBlocks.count > 0
|
|
|| self.fromObjects != nil
|
|
|| self.toObjects != nil;
|
|
}
|
|
|
|
- (void)performReloadDataWithCollectionView:(UICollectionView *)collectionView {
|
|
IGAssertMainThread();
|
|
|
|
// bail early if the collection view has been deallocated in the time since the update was queued
|
|
if (collectionView == nil) {
|
|
return;
|
|
}
|
|
|
|
id<IGListAdapterUpdaterDelegate> delegate = self.delegate;
|
|
void (^reloadUpdates)() = self.reloadUpdates;
|
|
NSArray *completionBlocks = [self.completionBlocks copy];
|
|
NSArray *itemUpdateBlocks = [self.itemUpdateBlocks copy];
|
|
|
|
// item updates must not send mutations to the collection view while we are reloading
|
|
self.batchUpdateOrReloadInProgress = YES;
|
|
|
|
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 actually /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();
|
|
}
|
|
|
|
// cleanup state before reloading and calling completion blocks
|
|
[self cleanupState];
|
|
[self cleanupUpdateBlockState];
|
|
|
|
[delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView];
|
|
[collectionView reloadData];
|
|
[collectionView.collectionViewLayout invalidateLayout];
|
|
[collectionView layoutIfNeeded];
|
|
[delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView];
|
|
|
|
self.batchUpdateOrReloadInProgress = NO;
|
|
|
|
for (IGListUpdatingCompletion block in completionBlocks) {
|
|
block(YES);
|
|
}
|
|
}
|
|
|
|
static NSArray *objectsWithDuplicateIdentifiersRemoved(NSArray<id<IGListDiffable>> *objects) {
|
|
NSMutableSet *identifiers = [NSMutableSet new];
|
|
NSMutableArray *uniqueObjects = [NSMutableArray new];
|
|
for (id<IGListDiffable> object in objects) {
|
|
id diffIdentifier = [object diffIdentifier];
|
|
if (![identifiers containsObject:diffIdentifier]) {
|
|
[identifiers addObject:diffIdentifier];
|
|
[uniqueObjects addObject:object];
|
|
} else {
|
|
IGLKLog(@"WARNING: Object %@ already appeared in objects array", object);
|
|
}
|
|
}
|
|
return uniqueObjects;
|
|
}
|
|
|
|
- (void)performBatchUpdatesWithCollectionView:(UICollectionView *)collectionView {
|
|
IGAssertMainThread();
|
|
IGAssert(!self.batchUpdateOrReloadInProgress, @"should not call this when updating");
|
|
|
|
// bail early if the collection view has been deallocated in the time since the update was queued
|
|
if (collectionView == nil) {
|
|
return;
|
|
}
|
|
|
|
// create local variables so we can immediately clean our state but pass these items into the batch update block
|
|
id<IGListAdapterUpdaterDelegate> delegate = self.delegate;
|
|
NSArray *fromObjects = [self.fromObjects copy];
|
|
NSArray *toObjects = objectsWithDuplicateIdentifiersRemoved(self.toObjects);
|
|
void (^objectTransitionBlock)(NSArray *) = [self.objectTransitionBlock copy];
|
|
NSArray *itemUpdateBlocks = [self.itemUpdateBlocks copy];
|
|
NSArray *completionBlocks = [self.completionBlocks copy];
|
|
const BOOL animated = self.queuedUpdateIsAnimated;
|
|
|
|
// clean up all state so that new updates can be coalesced while the current update is in flight
|
|
[self cleanupState];
|
|
|
|
void (^executeUpdateBlocks)() = ^{
|
|
// run the update block so that the adapter can set its items. this makes sure that just before the update is
|
|
// committed that the data source is updated to the /latest/ "toObjects". this makes the data source in sync
|
|
// with the items that the updater is transitioning to
|
|
if (objectTransitionBlock != nil) {
|
|
objectTransitionBlock(toObjects);
|
|
}
|
|
|
|
// execute each item update block which should make calls like insert, delete, and reload for index paths
|
|
// we collect all mutations in corresponding sets on self, then filter based on UICollectionView shortcomings
|
|
// call after the objectTransitionBlock so section level mutations happen before any items
|
|
for (IGListItemUpdateBlock itemUpdateBlock in itemUpdateBlocks) {
|
|
itemUpdateBlock();
|
|
}
|
|
};
|
|
|
|
void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) {
|
|
for (IGListUpdatingCompletion block in completionBlocks) {
|
|
block(finished);
|
|
}
|
|
};
|
|
|
|
// 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
|
|
const BOOL iOS83OrLater = (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_3);
|
|
if (iOS83OrLater && self.allowsBackgroundReloading && collectionView.window == nil) {
|
|
[self beginPerformBatchUpdatestoObjects:toObjects];
|
|
executeUpdateBlocks();
|
|
[self cleanupUpdateBlockState];
|
|
[self performBatchUpdatesItemBlockApplied];
|
|
[collectionView reloadData];
|
|
[self endPerformBatchUpdates];
|
|
executeCompletionBlocks(YES);
|
|
return;
|
|
}
|
|
|
|
IGListIndexSetResult *result = IGListDiffExperiment(fromObjects, toObjects, IGListDiffEquality, self.experiments);
|
|
|
|
// if the diff has no changes and there are no update blocks queued, dont batch update
|
|
if (!result.hasChanges && itemUpdateBlocks.count == 0) {
|
|
executeUpdateBlocks();
|
|
executeCompletionBlocks(YES);
|
|
return;
|
|
}
|
|
|
|
__block IGListBatchUpdateData *updateData = nil;
|
|
|
|
void (^updateBlock)() = ^{
|
|
executeUpdateBlocks();
|
|
|
|
updateData = [self flushCollectionView:collectionView
|
|
withDiffResult:result
|
|
reloadSections:[self.reloadSections copy]
|
|
deleteIndexPaths:[self.deleteIndexPaths copy]
|
|
insertIndexPaths:[self.insertIndexPaths copy]
|
|
moveIndexPaths:[self.moveIndexPaths copy]
|
|
reloadIndexPaths:[self.reloadIndexPaths copy]
|
|
fromObjects:fromObjects];
|
|
|
|
[self cleanupUpdateBlockState];
|
|
[self performBatchUpdatesItemBlockApplied];
|
|
};
|
|
|
|
void (^completionBlock)(BOOL) = ^(BOOL finished) {
|
|
[self endPerformBatchUpdates];
|
|
|
|
executeCompletionBlocks(finished);
|
|
|
|
[delegate listAdapterUpdater:self didPerformBatchUpdates:updateData withCollectionView:collectionView];
|
|
|
|
// queue another update in case something changed during batch updates. this method will bail next runloop if
|
|
// there are no changes
|
|
[self queueUpdateWithCollectionView:collectionView];
|
|
};
|
|
|
|
// disables multiple performBatchUpdates: from happening at the same time
|
|
[self beginPerformBatchUpdatestoObjects:toObjects];
|
|
|
|
@try {
|
|
[delegate listAdapterUpdater:self willPerformBatchUpdatesWithCollectionView:collectionView];
|
|
if (animated) {
|
|
[collectionView performBatchUpdates:updateBlock completion:completionBlock];
|
|
} else {
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
[collectionView performBatchUpdates:updateBlock completion:^(BOOL finished) {
|
|
completionBlock(finished);
|
|
[CATransaction commit];
|
|
}];
|
|
}
|
|
} @catch (NSException *exception) {
|
|
[delegate listAdapterUpdater:self willCrashWithException:exception fromObjects:fromObjects toObjects:toObjects updates:updateData];
|
|
@throw exception;
|
|
}
|
|
}
|
|
|
|
void convertReloadToDeleteInsert(NSMutableIndexSet *reloads,
|
|
NSMutableIndexSet *deletes,
|
|
NSMutableIndexSet *inserts,
|
|
IGListIndexSetResult *result,
|
|
NSArray<id<IGListDiffable>> *fromObjects) {
|
|
// reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts
|
|
const BOOL hasObjects = [fromObjects count] > 0;
|
|
[[reloads copy] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
|
|
// if a diff was not performed, there are no changes. instead use the same index that was originally queued
|
|
id<NSObject> diffIdentifier = hasObjects ? [fromObjects[idx] diffIdentifier] : nil;
|
|
const NSInteger from = hasObjects ? [result oldIndexForIdentifier:diffIdentifier] : idx;
|
|
const NSInteger to = hasObjects ? [result newIndexForIdentifier:diffIdentifier] : idx;
|
|
[reloads removeIndex:from];
|
|
|
|
// if a reload is queued outside the diff and the object was inserted or deleted it cannot be
|
|
if (from != NSNotFound && to != NSNotFound) {
|
|
[deletes addIndex:from];
|
|
[inserts addIndex:to];
|
|
} else {
|
|
IGAssert([result.deletes containsIndex:idx],
|
|
@"Reloaded section %zi was not found in deletes with from: %zi, to: %zi, deletes: %@",
|
|
idx, from, to, deletes);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (IGListBatchUpdateData *)flushCollectionView:(UICollectionView *)collectionView
|
|
withDiffResult:(IGListIndexSetResult *)diffResult
|
|
reloadSections:(NSIndexSet *)reloadSections
|
|
deleteIndexPaths:(NSSet<NSIndexPath *> *)deleteIndexPaths
|
|
insertIndexPaths:(NSSet<NSIndexPath *> *)insertIndexPaths
|
|
moveIndexPaths:(NSSet<IGListMoveIndexPath *> *)moveFromIndexPaths
|
|
reloadIndexPaths:(NSSet<NSIndexPath *> *)reloadIndexPaths
|
|
fromObjects:(NSArray <id<IGListDiffable>> *)fromObjects {
|
|
NSSet *moves = [[NSSet alloc] initWithArray:diffResult.moves];
|
|
|
|
// combine section reloads from the diff and manual reloads via reloadItems:
|
|
NSMutableIndexSet *reloads = [diffResult.updates mutableCopy];
|
|
[reloads addIndexes:reloadSections];
|
|
|
|
NSMutableIndexSet *inserts = [diffResult.inserts mutableCopy];
|
|
NSMutableIndexSet *deletes = [diffResult.deletes mutableCopy];
|
|
if (self.movesAsDeletesInserts) {
|
|
for (IGListMoveIndex *move in moves) {
|
|
[deletes addIndex:move.from];
|
|
[inserts addIndex:move.to];
|
|
}
|
|
// clear out all moves
|
|
moves = [NSSet new];
|
|
}
|
|
|
|
// reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts
|
|
convertReloadToDeleteInsert(reloads, deletes, inserts, diffResult, fromObjects);
|
|
|
|
IGListBatchUpdateData *updateData = [[IGListBatchUpdateData alloc] initWithInsertSections:inserts
|
|
deleteSections:deletes
|
|
moveSections:moves
|
|
insertIndexPaths:insertIndexPaths
|
|
deleteIndexPaths:deleteIndexPaths
|
|
moveIndexPaths:moveFromIndexPaths
|
|
reloadIndexPaths:reloadIndexPaths];
|
|
[collectionView ig_applyBatchUpdateData:updateData];
|
|
return updateData;
|
|
}
|
|
|
|
- (void)beginPerformBatchUpdatestoObjects:(NSArray *)toObjects {
|
|
self.batchUpdateOrReloadInProgress = YES;
|
|
self.pendingTransitionToObjects = toObjects;
|
|
}
|
|
|
|
- (void)performBatchUpdatesItemBlockApplied {
|
|
self.pendingTransitionToObjects = nil;
|
|
}
|
|
|
|
- (void)endPerformBatchUpdates {
|
|
self.batchUpdateOrReloadInProgress = NO;
|
|
}
|
|
|
|
- (void)cleanupState {
|
|
self.queuedUpdateIsAnimated = YES;
|
|
|
|
// destroy to/from transition items
|
|
self.fromObjects = nil;
|
|
self.toObjects = nil;
|
|
|
|
// destroy reloadData state
|
|
self.reloadUpdates = nil;
|
|
self.queuedReloadData = NO;
|
|
|
|
// remove indexpath/item changes
|
|
self.objectTransitionBlock = nil;
|
|
[self.itemUpdateBlocks removeAllObjects];
|
|
|
|
// remove completion blocks from item transitions or index path updates
|
|
[self.completionBlocks removeAllObjects];
|
|
}
|
|
|
|
- (void)cleanupUpdateBlockState {
|
|
[self.reloadSections removeAllIndexes];
|
|
[self.deleteIndexPaths removeAllObjects];
|
|
[self.insertIndexPaths removeAllObjects];
|
|
[self.moveIndexPaths removeAllObjects];
|
|
[self.reloadIndexPaths removeAllObjects];
|
|
}
|
|
|
|
- (void)queueUpdateWithCollectionView:(UICollectionView *)collectionView {
|
|
IGAssertMainThread();
|
|
|
|
// callers may hold weak refs and lose the collection view by the time we requeue, bail if that's the case
|
|
if (collectionView == nil) {
|
|
return;
|
|
}
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (weakSelf.batchUpdateOrReloadInProgress || ![weakSelf hasChanges]) {
|
|
return;
|
|
}
|
|
|
|
if (weakSelf.hasQueuedReloadData) {
|
|
[weakSelf performReloadDataWithCollectionView:collectionView];
|
|
} else {
|
|
[weakSelf performBatchUpdatesWithCollectionView:collectionView];
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
#pragma mark - IGListUpdatingDelegate
|
|
|
|
static BOOL IGListIsEqual(const void *a, const void *b, NSUInteger (*size)(const void *item)) {
|
|
const id<IGListDiffable> left = (__bridge id<IGListDiffable>)a;
|
|
const id<IGListDiffable> right = (__bridge id<IGListDiffable>)b;
|
|
return [[left diffIdentifier] isEqual:[right diffIdentifier]];
|
|
}
|
|
|
|
// since the diffing algo used in this updater keys items based on their -diffIdentifier, we must use a map table that
|
|
// precisely mimics this behavior
|
|
static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(const void *item)) {
|
|
return [[(__bridge id<IGListDiffable>)item diffIdentifier] hash];
|
|
}
|
|
|
|
- (NSPointerFunctions *)objectLookupPointerFunctions {
|
|
NSPointerFunctions *functions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory];
|
|
functions.hashFunction = IGListIdentifierHash;
|
|
functions.isEqualFunction = IGListIsEqual;
|
|
return functions;
|
|
}
|
|
|
|
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
|
|
fromObjects:(nullable NSArray *)fromObjects
|
|
toObjects:(nullable NSArray *)toObjects
|
|
animated:(BOOL)animated
|
|
objectTransitionBlock:(void (^)(NSArray *))objectTransitionBlock
|
|
completion:(nullable void (^)(BOOL))completion {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(collectionView != nil);
|
|
IGParameterAssert(objectTransitionBlock != nil);
|
|
|
|
// only update the items that we are coming from if it has not been set
|
|
// this allows multiple updates to be called while an update is already in progress, and the transition from > to
|
|
// will be done on the first "fromObjects" received and the last "toObjects"
|
|
// if performBatchUpdates: hasn't applied the update block, then data source hasn't transitioned its state. if an
|
|
// update is queued in between then we must use the pending toObjects
|
|
self.fromObjects = self.fromObjects ?: self.pendingTransitionToObjects ?: fromObjects;
|
|
self.toObjects = toObjects;
|
|
|
|
// disabled animations will always take priority
|
|
// reset to YES in -cleanupState
|
|
self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;
|
|
|
|
#ifdef DEBUG
|
|
for (id obj in toObjects) {
|
|
IGAssert([obj conformsToProtocol:@protocol(IGListDiffable)],
|
|
@"In order to use IGListAdapterUpdater, object %@ must conform to IGListDiffable", obj);
|
|
}
|
|
#endif
|
|
|
|
// always use the last update block, even though this should always do the exact same thing
|
|
self.objectTransitionBlock = objectTransitionBlock;
|
|
|
|
IGListUpdatingCompletion localCompletion = completion;
|
|
if (localCompletion) {
|
|
[self.completionBlocks addObject:localCompletion];
|
|
}
|
|
|
|
[self queueUpdateWithCollectionView:collectionView];
|
|
}
|
|
|
|
- (void)performUpdateWithCollectionView:(UICollectionView *)collectionView
|
|
animated:(BOOL)animated
|
|
itemUpdates:(void (^)())itemUpdates
|
|
completion:(void (^)(BOOL))completion {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(collectionView != nil);
|
|
IGParameterAssert(itemUpdates != nil);
|
|
|
|
// disabled animations will always take priority
|
|
// reset to YES in -cleanupState
|
|
self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;
|
|
|
|
[self.itemUpdateBlocks addObject:itemUpdates];
|
|
|
|
if (completion != nil) {
|
|
[self.completionBlocks addObject:completion];
|
|
}
|
|
|
|
[self queueUpdateWithCollectionView:collectionView];
|
|
}
|
|
|
|
- (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray <NSIndexPath *> *)indexPaths {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(collectionView != nil);
|
|
IGParameterAssert(indexPaths != nil);
|
|
if (self.batchUpdateOrReloadInProgress) {
|
|
[self.insertIndexPaths addObjectsFromArray:indexPaths];
|
|
} else {
|
|
[self.delegate listAdapterUpdater:self willInsertIndexPaths:indexPaths collectionView:collectionView];
|
|
[collectionView insertItemsAtIndexPaths:indexPaths];
|
|
}
|
|
}
|
|
|
|
- (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray <NSIndexPath *> *)indexPaths {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(collectionView != nil);
|
|
IGParameterAssert(indexPaths != nil);
|
|
if (self.batchUpdateOrReloadInProgress) {
|
|
[self.deleteIndexPaths addObjectsFromArray:indexPaths];
|
|
} else {
|
|
[self.delegate listAdapterUpdater:self willDeleteIndexPaths:indexPaths collectionView:collectionView];
|
|
[collectionView deleteItemsAtIndexPaths:indexPaths];
|
|
}
|
|
}
|
|
|
|
- (void)moveItemInCollectionView:(UICollectionView *)collectionView
|
|
fromIndexPath:(NSIndexPath *)fromIndexPath
|
|
toIndexPath:(NSIndexPath *)toIndexPath {
|
|
if (self.batchUpdateOrReloadInProgress) {
|
|
IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:fromIndexPath to:toIndexPath];
|
|
[self.moveIndexPaths addObject:move];
|
|
} else {
|
|
[self.delegate listAdapterUpdater:self willMoveFromIndexPath:fromIndexPath toIndexPath:toIndexPath collectionView:collectionView];
|
|
[collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
|
|
}
|
|
}
|
|
|
|
- (void)reloadItemsInCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray <NSIndexPath *> *)indexPaths {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(collectionView != nil);
|
|
IGParameterAssert(indexPaths != nil);
|
|
if (self.batchUpdateOrReloadInProgress) {
|
|
[self.reloadIndexPaths addObjectsFromArray:indexPaths];
|
|
} else {
|
|
[self.delegate listAdapterUpdater:self willReloadIndexPaths:indexPaths collectionView:collectionView];
|
|
[collectionView reloadItemsAtIndexPaths:indexPaths];
|
|
}
|
|
}
|
|
|
|
- (void)reloadDataWithCollectionView:(UICollectionView *)collectionView
|
|
reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
|
|
completion:(nullable IGListUpdatingCompletion)completion {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(collectionView != nil);
|
|
IGParameterAssert(reloadUpdateBlock != nil);
|
|
|
|
IGListUpdatingCompletion localCompletion = completion;
|
|
if (localCompletion) {
|
|
[self.completionBlocks addObject:localCompletion];
|
|
}
|
|
|
|
self.reloadUpdates = reloadUpdateBlock;
|
|
self.queuedReloadData = YES;
|
|
[self queueUpdateWithCollectionView:collectionView];
|
|
}
|
|
|
|
- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(collectionView != nil);
|
|
IGParameterAssert(sections != nil);
|
|
if (self.batchUpdateOrReloadInProgress) {
|
|
[self.reloadSections addIndexes:sections];
|
|
} else {
|
|
[self.delegate listAdapterUpdater:self willReloadSections:sections collectionView:collectionView];
|
|
[collectionView reloadSections:sections];
|
|
}
|
|
}
|
|
|
|
@end
|