Enable collection node interactive moves (#735)

* Add support for interactive moves

* Enable drag & drop in collection view example

* Update changelog

* Change the gating logic to match UIKit

* Add a warning when we prevent interactive movement due to async layout
This commit is contained in:
Adlai Holler
2018-01-09 14:34:32 -08:00
committed by GitHub
parent 131619de96
commit 5e73396cde
7 changed files with 195 additions and 61 deletions

View File

@@ -101,6 +101,10 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
NSHashTable<ASCellNode *> *_cellsForLayoutUpdates;
id<ASCollectionViewLayoutFacilitatorProtocol> _layoutFacilitator;
CGFloat _leadingScreensForBatching;
// When we update our data controller in response to an interactive move,
// we don't want to tell the collection view about the change (it knows!)
BOOL _updatingInResponseToInteractiveMove;
BOOL _inverted;
NSUInteger _superBatchUpdateCount;
@@ -218,6 +222,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
unsigned int numberOfSectionsInCollectionNode:1;
unsigned int collectionNodeNumberOfItemsInSection:1;
unsigned int collectionNodeContextForSection:1;
unsigned int collectionNodeCanMoveItem:1;
unsigned int collectionNodeMoveItem:1;
// Whether this data source conforms to ASCollectionDataSourceInterop
unsigned int interop:1;
@@ -454,6 +460,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForSupplementaryElementOfKind:atIndexPath:)];
_asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:supplementaryElementKindsInSection:)];
_asyncDataSourceFlags.nodeModelForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeModelForItemAtIndexPath:)];
_asyncDataSourceFlags.collectionNodeCanMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:canMoveItemWithNode:)];
_asyncDataSourceFlags.collectionNodeMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:moveItemAtIndexPath:toIndexPath:)];
_asyncDataSourceFlags.interop = [_asyncDataSource conformsToProtocol:@protocol(ASCollectionDataSourceInterop)];
if (_asyncDataSourceFlags.interop) {
@@ -1492,6 +1500,66 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath
{
// Mimic UIKit's gating logic.
// If the data source doesn't support moving, then all bets are off.
if (!_asyncDataSourceFlags.collectionNodeMoveItem) {
return NO;
}
// Currently we do not support interactive moves when using async layout. The reason is, we do not have a mechanism
// to propagate the "presentation data" element map (containing the speculative in-progress moves) to the layout delegate,
// and this can cause exceptions to be thrown from UICV. For example, if you drag an item out of a section,
// the element map will still contain N items in that section, even though there's only N-1 shown, and UICV will
// throw an exception that you specified an element that doesn't exist.
//
// In iOS >= 11, this is made much easier by the UIDataSourceTranslating API. In previous versions of iOS our best bet
// would be to capture the invalidation contexts that are sent during interactive moves and make our own data source translator.
if ([self.collectionViewLayout isKindOfClass:[ASCollectionLayout class]]) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
as_log_debug(ASCollectionLog(), "Collection node item interactive movement is not supported when using a layout delegate. This message will only be logged once. Node: %@", ASObjectDescriptionMakeTiny(self));
});
return NO;
}
// If the data source implements canMoveItem, let them decide.
if (_asyncDataSourceFlags.collectionNodeCanMoveItem) {
if (auto cellNode = [self nodeForItemAtIndexPath:indexPath]) {
GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO);
return [_asyncDataSource collectionNode:collectionNode canMoveItemWithNode:cellNode];
}
}
// Otherwise allow the move for all items.
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeMoveItem, @"Should not allow interactive collection item movement if data source does not support it.");
// Inform the data source first, in case they call nodeForItemAtIndexPath:.
// We want to make sure we return them the node for the item they have in mind.
if (auto collectionNode = self.collectionNode) {
[_asyncDataSource collectionNode:collectionNode moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
}
// Now we update our data controller's store.
// Get up to date
[self waitUntilAllUpdatesAreCommitted];
// Set our flag to suppress informing super about the change.
ASDisplayNodeAssertFalse(_updatingInResponseToInteractiveMove);
_updatingInResponseToInteractiveMove = YES;
// Submit the move
[self moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
// Wait for it to finish should be fast!
[self waitUntilAllUpdatesAreCommitted];
// Clear the flag
_updatingInResponseToInteractiveMove = NO;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// If a scroll happenes the current range mode needs to go to full
@@ -2023,7 +2091,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
- (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates
{
ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource || _superIsPendingDataLoad) {
if (!self.asyncDataSource || _superIsPendingDataLoad || _updatingInResponseToInteractiveMove) {
updates();
[changeSet executeCompletionHandlerWithFinished:NO];
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
@@ -2278,21 +2346,4 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
return;
}
#if ASDISPLAYNODE_ASSERTIONS_ENABLED // Remove implementations entirely for efficiency if not asserting.
// intercepted due to not being supported by ASCollectionView (prevent bugs caused by usage)
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0)
{
ASDisplayNodeAssert(![self.asyncDataSource respondsToSelector:_cmd], @"%@ is not supported by ASCollectionView - please remove or disable this data source method.", NSStringFromSelector(_cmd));
return NO;
}
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath NS_AVAILABLE_IOS(9_0)
{
ASDisplayNodeAssert(![self.asyncDataSource respondsToSelector:_cmd], @"%@ is not supported by ASCollectionView - please remove or disable this data source method.", NSStringFromSelector(_cmd));
}
#endif
@end