merge IGListUpdatingDelegateExperimental into IGListUpdatingDelegate

Summary:
Now that the new updater has shipped, lets update `IGListUpdatingDelegate` with the new methods:
* `-performExperimentalUpdateAnimated` is the new section update method (renaming coming in the next diff)
* `-performDataSourceChange` lets us safely update the `UICollectionView` dataSource

Also, something I've been wanting to do for a long time, lets group related methods in `IGListUpdatingDelegate.h`.

Reviewed By: Haud

Differential Revision: D25884780

fbshipit-source-id: 5d9201ace8bf6b281d71ff03463cb7c911e7f967
This commit is contained in:
Maxime Ollivier 2021-01-21 19:55:59 -08:00 committed by Facebook GitHub Bot
parent 247e7cac65
commit 43af8838df
9 changed files with 127 additions and 240 deletions

View file

@ -15,7 +15,7 @@
#import "IGListDebugger.h"
#import "IGListSectionControllerInternal.h"
#import "IGListTransitionData.h"
#import "IGListUpdatingDelegateExperimental.h"
#import "IGListUpdatingDelegate.h"
#import "UICollectionViewLayout+InteractiveReordering.h"
#import "UIScrollView+IGListKit.h"
@ -24,8 +24,6 @@
// An array of blocks to execute once batch updates are finished
NSMutableArray<void (^)(void)> *_queuedCompletionBlocks;
NSHashTable<id<IGListAdapterUpdateListener>> *_updateListeners;
// Temporary property while we experiment with a new updater.
id<IGListUpdatingDelegateExperimental> _experimentalUpdater;
}
- (void)dealloc {
@ -56,7 +54,6 @@
_updater = updater;
_viewController = viewController;
_experimentalUpdater = [updater conformsToProtocol:@protocol(IGListUpdatingDelegateExperimental)] ? (id<IGListUpdatingDelegateExperimental>)updater : nil;
[IGListDebugger trackAdapter:self];
}
@ -97,35 +94,26 @@
_registeredSupplementaryViewIdentifiers = [NSMutableSet new];
_registeredSupplementaryViewNibNames = [NSMutableSet new];
const BOOL settingFirstCollectionView = _collectionView == nil;
// We can't just swap out the collectionView, because we might have on-going or pending updates.
// `_updater` can take care of that by wrapping the change in `performDataSourceChange`.
[_updater performDataSourceChange:^{
if (self->_collectionView.dataSource == self) {
// Since we're not going to sync the previous collectionView anymore, lets not be its dataSource.
self->_collectionView.dataSource = nil;
}
self->_collectionView = collectionView;
self->_collectionView.dataSource = self;
if (_experimentalUpdater) {
// We can't just swap out the collectionView, because we might have on-going or pending updates.
// `_experimentalUpdater` can take care of that by wrapping the change in `performDataSourceChange`.
[_experimentalUpdater performDataSourceChange:^{
if (self->_collectionView.dataSource == self) {
// Since we're not going to sync the previous collectionView anymore, lets not be its dataSource.
self->_collectionView.dataSource = nil;
}
self->_collectionView = collectionView;
self->_collectionView.dataSource = self;
[self _updateCollectionViewDelegate];
// Sync the dataSource <> adapter for a couple of reasons:
// 1. We might not have synced on -setDataSource, so now is the time to try again.
// 2. Any in-flight `performUpdatesAnimated` were cancelled, so lets make sure we have the latest data.
[self _updateObjects];
// The sync between the collectionView <> adapter will happen automically, since
// we just changed the `collectionView.dataSource`.
}];
} else {
_collectionView = collectionView;
_collectionView.dataSource = self;
[self _updateCollectionViewDelegate];
}
// Sync the dataSource <> adapter for a couple of reasons:
// 1. We might not have synced on -setDataSource, so now is the time to try again.
// 2. Any in-flight `performUpdatesAnimated` were cancelled, so lets make sure we have the latest data.
[self _updateObjects];
// The sync between the collectionView <> adapter will happen automically, since
// we just changed the `collectionView.dataSource`.
}];
if (@available(iOS 10.0, tvOS 10, *)) {
_collectionView.prefetchingEnabled = NO;
@ -133,10 +121,6 @@
[_collectionView.collectionViewLayout ig_hijackLayoutInteractiveReorderingMethodForAdapter:self];
[_collectionView.collectionViewLayout invalidateLayout];
if (!_experimentalUpdater && settingFirstCollectionView) {
[self _updateObjectsIfHasDataSource];
}
}
}
@ -145,24 +129,19 @@
return;
}
if (_experimentalUpdater) {
[_experimentalUpdater performDataSourceChange:^{
self->_dataSource = dataSource;
[_updater performDataSourceChange:^{
self->_dataSource = dataSource;
// Invalidate the collectionView internal section & item counts, as if its dataSource changed.
self->_collectionView.dataSource = nil;
self->_collectionView.dataSource = self;
// Invalidate the collectionView internal section & item counts, as if its dataSource changed.
self->_collectionView.dataSource = nil;
self->_collectionView.dataSource = self;
// Sync the dataSource <> adapter
[self _updateObjects];
// Sync the dataSource <> adapter
[self _updateObjects];
// The sync between the collectionView <> adapter will happen automically, since
// we just changed the `collectionView.dataSource`.
}];
} else {
_dataSource = dataSource;
[self _updateObjectsIfHasDataSource];
}
// The sync between the collectionView <> adapter will happen automically, since
// we just changed the `collectionView.dataSource`.
}];
}
// reset and configure the delegate proxy whenever this property is set
@ -186,13 +165,6 @@
}
}
- (void)_updateObjectsIfHasDataSource {
// This is to keep the existing logic while testing `experimentalUpdater`
if (_dataSource != nil) {
[self _updateObjects];
}
}
- (void)_updateObjects {
if (_collectionView == nil) {
// If we don't have a collectionView, we can't do much.
@ -375,66 +347,8 @@
return;
}
__weak __typeof__(self) weakSelf = self;
IGListUpdaterCompletion outerCompletionBlock = ^(BOOL finished){
__typeof__(self) strongSelf = weakSelf;
if (strongSelf == nil) {
IGLK_BLOCK_CALL_SAFE(completion,finished);
return;
}
// release the previous items
strongSelf.previousSectionMap = nil;
[strongSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated];
IGLK_BLOCK_CALL_SAFE(completion,finished);
[strongSelf _exitBatchUpdates];
};
[self _enterBatchUpdates];
if (_experimentalUpdater) {
[self _performExperimentalUpdatesWithUpdater:_experimentalUpdater
dataSource:dataSource
animated:animated
completion:outerCompletionBlock];
} else {
[self _performRegularUpdatesWithUpdater:updater
dataSource:dataSource
animated:animated
completion:outerCompletionBlock];
}
}
- (void)_performRegularUpdatesWithUpdater:(id<IGListUpdatingDelegate>)updater
dataSource:(id<IGListAdapterDataSource>)dataSource
animated:(BOOL)animated
completion:(IGListUpdaterCompletion)completion {
NSArray *fromObjects = self.sectionMap.objects;
__weak __typeof__(self) weakSelf = self;
IGListToObjectBlock toObjectsBlock = ^NSArray *{
__typeof__(self) strongSelf = weakSelf;
if (strongSelf == nil) {
return nil;
}
return [dataSource objectsForListAdapter:strongSelf];
};
[updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock]
fromObjects:fromObjects
toObjectsBlock:toObjectsBlock
animated:animated
objectTransitionBlock:^(NSArray *toObjects) {
// temporarily capture the item map that we are transitioning from in case
// there are any item deletes at the same
weakSelf.previousSectionMap = [weakSelf.sectionMap copy];
[weakSelf _updateObjects:toObjects dataSource:dataSource];
} completion:completion];
}
- (void)_performExperimentalUpdatesWithUpdater:(id<IGListUpdatingDelegateExperimental>)updater
dataSource:(id<IGListAdapterDataSource>)dataSource
animated:(BOOL)animated
completion:(IGListUpdaterCompletion)completion {
__weak __typeof__(self) weakSelf = self;
IGListTransitionDataBlock dataBlock = ^IGListTransitionData *{
__typeof__(self) strongSelf = weakSelf;
@ -452,15 +366,29 @@
}
// temporarily capture the item map that we are transitioning from in case
// there are any item deletes at the same
strongSelf.previousSectionMap = [weakSelf.sectionMap copy];
strongSelf.previousSectionMap = [strongSelf.sectionMap copy];
[strongSelf _updateWithData:data];
};
IGListUpdaterCompletion outerCompletionBlock = ^(BOOL finished){
__typeof__(self) strongSelf = weakSelf;
if (strongSelf == nil) {
IGLK_BLOCK_CALL_SAFE(completion,finished);
return;
}
// release the previous items
strongSelf.previousSectionMap = nil;
[strongSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated];
IGLK_BLOCK_CALL_SAFE(completion,finished);
[strongSelf _exitBatchUpdates];
};
[updater performExperimentalUpdateAnimated:animated
collectionViewBlock:[self _collectionViewBlock]
dataBlock:dataBlock
applyDataBlock:applyDataBlock
completion:completion];
completion:outerCompletionBlock];
}
- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion {
@ -795,7 +723,7 @@
return; // will be called again when update block completes
}
if (!shouldHide || !_experimentalUpdater) {
if (!shouldHide) {
UIView *backgroundView = [self.dataSource emptyViewForListAdapter:self];
// don't do anything if the client is using the same view
if (backgroundView != _collectionView.backgroundView) {

View file

@ -9,7 +9,6 @@
#import <IGListDiffKit/IGListMacros.h>
#import <IGListKit/IGListAdapterUpdaterCompatible.h>
#import <IGListKit/IGListUpdatingDelegateExperimental.h>
NS_ASSUME_NONNULL_BEGIN
@ -23,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
*/
IGLK_SUBCLASSING_RESTRICTED
NS_SWIFT_NAME(ListAdapterUpdater)
@interface IGListAdapterUpdater : NSObject <IGListAdapterUpdaterCompatible, IGListUpdatingDelegateExperimental>
@interface IGListAdapterUpdater : NSObject <IGListAdapterUpdaterCompatible>
@end

View file

@ -147,16 +147,6 @@ static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(cons
return functions;
}
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
fromObjects:(NSArray *)fromObjects
toObjectsBlock:(IGListToObjectBlock)toObjectsBlock
animated:(BOOL)animated
objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
completion:(IGListUpdatingCompletion)completion {
IGFailAssert(@"IGListExperimentalAdapterUpdater works with IGListUpdatingDelegateExperimental and doesn't implement the regular -performUpdateWithCollectionViewBlock method");
completion(NO);
}
- (void)performExperimentalUpdateAnimated:(BOOL)animated
collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
dataBlock:(IGListTransitionDataBlock)dataBlock

View file

@ -15,15 +15,14 @@
return [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality];
}
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
fromObjects:(NSArray *)fromObjects
toObjectsBlock:(IGListToObjectBlock)toObjectsBlock
animated:(BOOL)animated
objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
completion:(IGListUpdatingCompletion)completion {
if (toObjectsBlock != nil) {
NSArray *toObjects = toObjectsBlock() ?: @[];
objectTransitionBlock(toObjects);
- (void)performExperimentalUpdateAnimated:(BOOL)animated
collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
dataBlock:(IGListTransitionDataBlock)dataBlock
applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock
completion:(nullable IGListUpdatingCompletion)completion {
IGListTransitionData *sectionData = dataBlock ? dataBlock() : nil;
if (sectionData != nil) {
applyDataBlock(sectionData);
}
[self _synchronousReloadDataWithCollectionView:collectionViewBlock()];
if (completion) {
@ -42,6 +41,20 @@
}
}
- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block {
// A `UICollectionView` dataSource change will automatically invalidate
// its data, so no need to do anything else.
block();
}
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion {
reloadUpdateBlock();
[self _synchronousReloadDataWithCollectionView:collectionViewBlock()];
if (completion) {
completion(YES);
}
}
- (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray<NSIndexPath *> *)indexPaths {
[self _synchronousReloadDataWithCollectionView:collectionView];
}
@ -62,14 +75,6 @@
[self _synchronousReloadDataWithCollectionView:collectionView];
}
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion {
reloadUpdateBlock();
[self _synchronousReloadDataWithCollectionView:collectionViewBlock()];
if (completion) {
completion(YES);
}
}
- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections {
[self _synchronousReloadDataWithCollectionView:collectionView];
}

View file

@ -7,6 +7,7 @@
#import <UIKit/UIKit.h>
@class IGListTransitionData;
@protocol IGListDiffable;
NS_ASSUME_NONNULL_BEGIN
@ -47,6 +48,18 @@ typedef UICollectionView * _Nullable (^IGListCollectionViewBlock)(void);
NS_SWIFT_NAME(ListDataSourceChangeBlock)
typedef void (^IGListDataSourceChangeBlock)(void);
/// A block that returns the `IGListTransitionData` needed before an update.
NS_SWIFT_NAME(ListTransitionDataBlock)
typedef IGListTransitionData * _Nullable (^IGListTransitionDataBlock)(void);
/**
A block to be called when the adapter applies changes to the collection view.
@param data The new data that contains the from/to objects.
*/
NS_SWIFT_NAME(ListTransitionDataApplyBlock)
typedef void (^IGListTransitionDataApplyBlock)(IGListTransitionData *data);
/**
Implement this protocol in order to handle both section and row based update events. Implementation should forward or
coalesce these events to a backing store or collection.
@ -69,29 +82,59 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
- (NSPointerFunctions *)objectLookupPointerFunctions;
/**
Tells the delegate to perform a section transition from an old array of objects to a new one.
Perform a **section** update from an old array of objects to a new one.
@param collectionViewBlock A block returning the collecion view to perform updates on.
@param fromObjects The previous objects in the collection view. Objects must conform to `IGListDiffable`.
@param toObjectsBlock A block returning the new objects in the collection view. Objects must conform to `IGListDiffable`.
@param animated A flag indicating if the transition should be animated.
@param objectTransitionBlock A block that must be called when the adapter applies changes to the collection view.
@param collectionViewBlock A block returning the collecion view to perform updates on.
@param dataBlock A block that returns the section information (ex: from and to objects)
@param applyDataBlock A block that must be called when the adapter applies changes to the collection view.
@param completion A completion block to execute when the update is finished.
@note Implementations determine how to transition between objects. You can perform a diff on the objects, reload
each section, or simply call `-reloadData` on the collection view. In the end, the collection view must be setup with a
section for each object in the `toObjects` array.
The `objectTransitionBlock` block should be called prior to making any `UICollectionView` updates, passing in the `toObjects`
The `applyDataBlock` block should be called prior to making any `UICollectionView` updates, passing in the `toObjects`
that the updater is applying.
*/
- (void)performExperimentalUpdateAnimated:(BOOL)animated
collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
dataBlock:(IGListTransitionDataBlock)dataBlock
applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock
completion:(nullable IGListUpdatingCompletion)completion;
/**
Perform an **item** update block in the collection view.
@param collectionViewBlock A block returning the collecion view to perform updates on.
@param animated A flag indicating if the transition should be animated.
@param itemUpdates A block containing all of the updates.
@param completion A completion block to execute when the update is finished.
*/
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
fromObjects:(nullable NSArray<id <IGListDiffable>> *)fromObjects
toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock
animated:(BOOL)animated
objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
itemUpdates:(IGListItemUpdateBlock)itemUpdates
completion:(nullable IGListUpdatingCompletion)completion;
/**
Perform a `[UICollectionView setDataSource:...]` swap within this block. It gives the updater the chance to cancel or
execute any on-going updates. The block should be executed synchronously.
@param block The block that will actuallty change the `dataSource`
*/
- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block;
/**
Completely reload data in the collection.
@param collectionViewBlock A block returning the collecion view to reload.
@param reloadUpdateBlock A block that must be called when the adapter reloads the collection view.
@param completion A completion block to execute when the reload is finished.
*/
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
completion:(nullable IGListUpdatingCompletion)completion;
/**
Tells the delegate to perform item inserts at the given index paths.
@ -145,17 +188,6 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
fromIndex:(NSInteger)fromIndex
toIndex:(NSInteger)toIndex;
/**
Completely reload data in the collection.
@param collectionViewBlock A block returning the collecion view to reload.
@param reloadUpdateBlock A block that must be called when the adapter reloads the collection view.
@param completion A completion block to execute when the reload is finished.
*/
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
completion:(nullable IGListUpdatingCompletion)completion;
/**
Completely reload each section in the collection view.
@ -164,19 +196,6 @@ NS_SWIFT_NAME(ListUpdatingDelegate)
*/
- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections;
/**
Perform an item update block in the collection view.
@param collectionViewBlock A block returning the collecion view to perform updates on.
@param animated A flag indicating if the transition should be animated.
@param itemUpdates A block containing all of the updates.
@param completion A completion block to execute when the update is finished.
*/
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
animated:(BOOL)animated
itemUpdates:(IGListItemUpdateBlock)itemUpdates
completion:(nullable IGListUpdatingCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View file

@ -1,54 +0,0 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import "IGListUpdatingDelegate.h"
@class IGListTransitionData;
NS_ASSUME_NONNULL_BEGIN
/// A block that returns the `IGListTransitionData` needed before an update.
NS_SWIFT_NAME(ListTransitionDataBlock)
typedef IGListTransitionData * _Nullable (^IGListTransitionDataBlock)(void);
/**
A block to be called when the adapter applies changes to the collection view.
@param data The new data that contains the from/to objects.
*/
NS_SWIFT_NAME(ListTransitionDataApplyBlock)
typedef void (^IGListTransitionDataApplyBlock)(IGListTransitionData *data);
/**
Temporary experimental version of `IGListUpdatingDelegate`
*/
NS_SWIFT_NAME(ListUpdatingDelegateExperimental)
@protocol IGListUpdatingDelegateExperimental <IGListUpdatingDelegate>
/**
Experimental version of `performUpdateWithCollectionViewBlock` that uses `IGListTransitionData` to make updates safer.
The adapter will use this method instead of the regular `performUpdateWithCollectionViewBlock` if implemented.
*/
- (void)performExperimentalUpdateAnimated:(BOOL)animated
collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
dataBlock:(IGListTransitionDataBlock)dataBlock
applyDataBlock:(IGListTransitionDataApplyBlock)applyDataBlock
completion:(nullable IGListUpdatingCompletion)completion;
/**
Perform a `[UICollectionView setDataSource:...]` swap within this block. It gives the updater the chance to cancel or
execute any on-going updates. The block will be executed synchronously.
@param block The block that will actuallty change the `dataSource`
*/
- (void)performDataSourceChange:(IGListDataSourceChangeBlock)block;
@end
NS_ASSUME_NONNULL_END

View file

@ -9,7 +9,6 @@
#import <IGListDiffKit/IGListMacros.h>
#import <IGListKit/IGListUpdatingDelegate.h>
#import <IGListKit/IGListUpdatingDelegateExperimental.h>
#import "IGListUpdateTransactable.h"

View file

@ -9,7 +9,6 @@
#import <IGListDiffKit/IGListMacros.h>
#import <IGListKit/IGListUpdatingDelegate.h>
#import <IGListKit/IGListUpdatingDelegateExperimental.h>
#import "IGListUpdateTransactable.h"

View file

@ -176,8 +176,8 @@
((IGListTestAdapterDataSource *)self.dataSource).backgroundView = background;
__block BOOL executed = NO;
[self.adapter reloadDataWithCompletion:^(BOOL finished) {
XCTAssertTrue(self.adapter.collectionView.backgroundView.hidden, @"Background view should be hidden");
XCTAssertEqualObjects(background, self.adapter.collectionView.backgroundView, @"Background view not correctly assigned");
UIView *backgroundViewAfterReload = self.adapter.collectionView.backgroundView;
XCTAssertTrue(!backgroundViewAfterReload || backgroundViewAfterReload.hidden, @"Background view should be hidden");
self.dataSource.objects = @[];
[self.adapter reloadDataWithCompletion:^(BOOL finished2) {
@ -358,7 +358,8 @@
self.dataSource.objects = @[@1];
((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new];
[self.adapter reloadDataWithCompletion:nil];
XCTAssertTrue(self.collectionView.backgroundView.hidden);
UIView *backgroundView = self.adapter.collectionView.backgroundView;
XCTAssertTrue(!backgroundView || backgroundView.hidden);
IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(1)];
sectionController.items = 0;
[self.adapter deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]];
@ -380,7 +381,8 @@
self.dataSource.objects = @[@1, @2];
((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new];
[self.adapter reloadDataWithCompletion:nil];
XCTAssertTrue(self.collectionView.backgroundView.hidden);
UIView *backgroundView = self.adapter.collectionView.backgroundView;
XCTAssertTrue(!backgroundView || backgroundView.hidden);
IGListTestSection *firstSectionController = [self.adapter sectionControllerForObject:@(1)];
IGListTestSection *secondSectionController = [self.adapter sectionControllerForObject:@(2)];
XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];