[ASWrapperCellNode] Introduce a new class allowing more control of UIKit passthrough cells. (#797)

* - [ASWrapperCellNode] Introduce a new class allowing more control of UIKit passthrough cells.

A few minor fixes to Collections behavior as well, including a new isSynchronized
API. The difference from processingUpdates is that after Synchronized, all animations
have also completed (or runloop turn if animations disabled, so .collectionViewLayout
can be relied on being fully in sync).

More upstreaming to come after this can land...

* Fix -[ASDataController clearData] to take no action before initial data loading.

* Empty commit to kick CI

* Spacing change to kick CI (since an empty commit doesn't work...)

* Tweak ASDataController changes to handle an edge case in _editingTransactionQueueCount management.

* Avoid excess cyclic calls to onDidFinishProcessingUpdates: by avoiding ASMainSerialQueue.

* Reverting my initial change as it wasn't the right approach, following the real fix before this.
This commit is contained in:
appleguy
2018-03-13 01:03:18 -07:00
committed by GitHub
parent 5cafdb9062
commit a41cbb48b3
15 changed files with 415 additions and 105 deletions

View File

@@ -374,7 +374,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
return [_dataController isProcessingUpdates];
}
- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion
- (void)onDidFinishProcessingUpdates:(void (^)())completion
{
[_dataController onDidFinishProcessingUpdates:completion];
}
@@ -391,6 +391,16 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
[_dataController waitUntilAllUpdatesAreProcessed];
}
- (BOOL)isSynchronized
{
return [_dataController isSynchronized];
}
- (void)onDidFinishSynchronizing:(void (^)())completion
{
[_dataController onDidFinishSynchronizing:completion];
}
- (void)setDataSource:(id<UICollectionViewDataSource>)dataSource
{
// UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. We also allow this when we're doing interop.
@@ -477,6 +487,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
if (_layoutInspectorFlags.didChangeCollectionViewDataSource) {
[layoutInspector didChangeCollectionViewDataSource:asyncDataSource];
}
[self _asyncDelegateOrDataSourceDidChange];
}
- (id<ASCollectionDelegate>)asyncDelegate
@@ -558,6 +569,15 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
if (_layoutInspectorFlags.didChangeCollectionViewDelegate) {
[layoutInspector didChangeCollectionViewDelegate:asyncDelegate];
}
[self _asyncDelegateOrDataSourceDidChange];
}
- (void)_asyncDelegateOrDataSourceDidChange
{
ASDisplayNodeAssertMainThread();
if (_asyncDataSource == nil && _asyncDelegate == nil) {
[_dataController clearData];
}
}
- (void)setCollectionViewLayout:(nonnull UICollectionViewLayout *)collectionViewLayout
@@ -644,18 +664,21 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
- (CGSize)sizeForElement:(ASCollectionElement *)element
{
ASDisplayNodeAssertMainThread();
if (element == nil) {
ASCellNode *node = element.node;
if (element == nil || node == nil) {
return CGSizeZero;
}
ASCellNode *node = element.node;
BOOL useUIKitCell = node.shouldUseUIKitCell;
if (useUIKitCell) {
// In this case, we should use the exact value that was stashed earlier by calling sizeForItem:, referenceSizeFor*, etc.
// Although the node would use the preferredSize in layoutThatFits, we can skip this because there's no constrainedSize.
ASDisplayNodeAssert([node.superclass isSubclassOfClass:[ASCellNode class]] == NO,
@"Placeholder cells for UIKit passthrough should be generic ASCellNodes: %@", node);
return node.style.preferredSize;
ASWrapperCellNode *wrapperNode = (ASWrapperCellNode *)node;
if (wrapperNode.sizeForItemBlock) {
return wrapperNode.sizeForItemBlock(wrapperNode, element.constrainedSize.max);
} else {
// In this case, we should use the exact value that was stashed earlier by calling sizeForItem:, referenceSizeFor*, etc.
// Although the node would use the preferredSize in layoutThatFits, we can skip this because there's no constrainedSize.
return wrapperNode.style.preferredSize;
}
} else {
return [node layoutThatFits:element.constrainedSize].size;
}
@@ -781,7 +804,11 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
// For UIKit passthrough cells of either type, re-fetch their sizes from the standard UIKit delegate methods.
ASCellNode *node = element.node;
if (node.shouldUseUIKitCell) {
NSIndexPath *indexPath = [self indexPathForNode:node];
ASWrapperCellNode *wrapperNode = (ASWrapperCellNode *)node;
if (wrapperNode.sizeForItemBlock) {
continue;
}
NSIndexPath *indexPath = [_dataController.pendingMap indexPathForElement:element];
NSString *kind = [element supplementaryElementKind];
CGSize previousSize = node.style.preferredSize;
CGSize size = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath];
@@ -818,7 +845,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
if (kind == nil) {
ASDisplayNodeAssert(_asyncDataSourceFlags.interop, @"This code should not be called except for UIKit passthrough compatibility");
SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:);
if ([_asyncDelegate respondsToSelector:sizeForItem]) {
if (indexPath && [_asyncDelegate respondsToSelector:sizeForItem]) {
size = [(id)_asyncDelegate collectionView:self layout:l sizeForItemAtIndexPath:indexPath];
} else {
size = ASFlowLayoutDefault(l, itemSize, CGSizeZero);
@@ -826,7 +853,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
} else if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility");
SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:);
if ([_asyncDelegate respondsToSelector:sizeForHeader]) {
if (indexPath && [_asyncDelegate respondsToSelector:sizeForHeader]) {
size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForHeaderInSection:indexPath.section];
} else {
size = ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero);
@@ -834,7 +861,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
} else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility");
SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:);
if ([_asyncDelegate respondsToSelector:sizeForFooter]) {
if (indexPath && [_asyncDelegate respondsToSelector:sizeForFooter]) {
size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForFooterInSection:indexPath.section];
} else {
size = ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero);
@@ -1105,9 +1132,12 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
UICollectionReusableView *view = nil;
ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:kind atIndexPath:indexPath];
ASCellNode *node = element.node;
ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil);
BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interopViewForSupplementaryElement && wrapperNode);
BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopViewForSupplementaryElement && (_asyncDataSourceFlags.interopAlwaysDequeue || node.shouldUseUIKitCell);
if (shouldDequeueExternally) {
if (wrapperNode.viewForSupplementaryBlock) {
view = wrapperNode.viewForSupplementaryBlock(wrapperNode);
} else if (shouldDequeueExternally) {
// This codepath is used for both IGListKit mode, and app-level UICollectionView interop.
view = [(id<ASCollectionDataSourceInterop>)_asyncDataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath];
} else {
@@ -1131,15 +1161,19 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
UICollectionViewCell *cell = nil;
ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath];
ASCellNode *node = element.node;
ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil);
BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interop && wrapperNode);
BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interop && node.shouldUseUIKitCell);
if (shouldDequeueExternally) {
if (wrapperNode.cellForItemBlock) {
cell = wrapperNode.cellForItemBlock(wrapperNode);
} else if (shouldDequeueExternally) {
cell = [(id<ASCollectionDataSourceInterop>)_asyncDataSource collectionView:collectionView cellForItemAtIndexPath:indexPath];
} else {
cell = [self dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath];
}
ASDisplayNodeAssert(element != nil, @"Element should exist. indexPath = %@, collectionDataSource = %@", indexPath, self);
ASDisplayNodeAssert(cell != nil, @"UICollectionViewCell must not be nil. indexPath = %@, collectionDataSource = %@", indexPath, self);
if (_ASCollectionViewCell *asCell = ASDynamicCastStrict(cell, _ASCollectionViewCell)) {
asCell.element = element;
@@ -1828,39 +1862,32 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
if (_asyncDataSourceFlags.collectionNodeNodeBlockForItem) {
GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; });
block = [_asyncDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath];
} else if (_asyncDataSourceFlags.collectionNodeNodeForItem) {
}
if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForItem) {
GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; });
cell = [_asyncDataSource collectionNode:collectionNode nodeForItemAtIndexPath:indexPath];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
} else if (_asyncDataSourceFlags.collectionViewNodeBlockForItem) {
if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeBlockForItem) {
block = [_asyncDataSource collectionView:self nodeBlockForItemAtIndexPath:indexPath];
} else if (_asyncDataSourceFlags.collectionViewNodeForItem) {
}
if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForItem) {
cell = [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath];
}
#pragma clang diagnostic pop
// Handle nil node block or cell
if (cell && [cell isKindOfClass:[ASCellNode class]]) {
block = ^{
return cell;
};
}
if (block == nil) {
if (_asyncDataSourceFlags.interop) {
CGSize preferredSize = [self _sizeForUIKitCellWithKind:nil atIndexPath:indexPath];
block = ^{
ASCellNode *node = [[ASCellNode alloc] init];
node.shouldUseUIKitCell = YES;
node.style.preferredSize = preferredSize;
return node;
};
} else {
ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the <ASCollectionDataSourceInterop> protocol!", indexPath, cell, block);
block = ^{
return [[ASCellNode alloc] init];
};
if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) {
// In this case, either the client is expecting a UIKit passthrough cell to be created automatically,
// or it is an error.
if (_asyncDataSourceFlags.interop) {
cell = [[ASWrapperCellNode alloc] init];
cell.style.preferredSize = [self _sizeForUIKitCellWithKind:nil atIndexPath:indexPath];
} else {
ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the <ASCollectionDataSourceInterop> protocol!", indexPath, cell, block);
cell = [[ASCellNode alloc] init];
}
}
}
@@ -1868,17 +1895,18 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
__weak __typeof__(self) weakSelf = self;
return ^{
__typeof__(self) strongSelf = weakSelf;
ASCellNode *node = (block != nil ? block() : [[ASCellNode alloc] init]);
ASCellNode *node = (block ? block() : cell);
ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"ASCollectionNode provided a non-ASCellNode! %@, %@", node, strongSelf);
[node enterHierarchyState:ASHierarchyStateRangeManaged];
if (node.interactionDelegate == nil) {
node.interactionDelegate = strongSelf;
}
if (strongSelf.inverted) {
node.transform = CATransform3DMakeScale(1, -1, 1) ;
node.transform = CATransform3DMakeScale(1, -1, 1);
}
return node;
};
return block;
}
- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section
@@ -1923,45 +1951,50 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
#pragma mark - ASDataControllerSource optional methods
- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout
{
ASDisplayNodeAssertMainThread();
ASCellNodeBlock nodeBlock = nil;
ASCellNode *node = nil;
ASCellNodeBlock block = nil;
ASCellNode *cell = nil;
if (_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement) {
GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; });
nodeBlock = [_asyncDataSource collectionNode:collectionNode nodeBlockForSupplementaryElementOfKind:kind atIndexPath:indexPath];
} else if (_asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) {
block = [_asyncDataSource collectionNode:collectionNode nodeBlockForSupplementaryElementOfKind:kind atIndexPath:indexPath];
}
if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) {
GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; });
node = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath];
} else if (_asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) {
cell = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath];
}
if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
node = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath];
cell = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath];
#pragma clang diagnostic pop
}
if (nodeBlock == nil) {
if (node) {
nodeBlock = ^{ return node; };
} else {
if (block == nil) {
if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) {
// In this case, the app code returned nil for the node and the nodeBlock.
// If the UIKit method is implemented, then we should use it. Otherwise the CGSizeZero default will cause UIKit to not show it.
CGSize preferredSize = CGSizeZero;
// If the UIKit method is implemented, then we should use a passthrough cell.
// Otherwise the CGSizeZero default will cause UIKit to not show it (so this isn't an error like the cellForItem case).
BOOL useUIKitCell = _asyncDataSourceFlags.interopViewForSupplementaryElement;
if (useUIKitCell) {
preferredSize = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath];
cell = [[ASWrapperCellNode alloc] init];
cell.style.preferredSize = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath];
} else {
cell = [[ASCellNode alloc] init];
}
nodeBlock = ^{
ASCellNode *node = [[ASCellNode alloc] init];
node.shouldUseUIKitCell = useUIKitCell;
node.style.preferredSize = preferredSize;
return node;
};
}
// This condition is intended to run for either cells received from the datasource, or created just above.
if (cell.shouldUseUIKitCell) {
*shouldAsyncLayout = NO;
}
block = ^{ return cell; };
}
return nodeBlock;
return block;
}
- (NSArray<NSString *> *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections
@@ -2097,6 +2130,15 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
[_layoutFacilitator collectionViewWillPerformBatchUpdates];
__block NSUInteger numberOfUpdates = 0;
id completion = ^(BOOL finished){
as_activity_scope(as_activity_create("Handle collection update completion", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT));
as_log_verbose(ASCollectionLog(), "Update animation finished %{public}@", self.collectionNode);
// Flush any range changes that happened as part of the update animations ending.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates];
[changeSet executeCompletionHandlerWithFinished:finished];
};
[self _superPerformBatchUpdates:^{
updates();
@@ -2129,14 +2171,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
[super insertItemsAtIndexPaths:change.indexPaths];
numberOfUpdates++;
}
} completion:^(BOOL finished){
as_activity_scope(as_activity_create("Handle collection update completion", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT));
as_log_verbose(ASCollectionLog(), "Update animation finished %{public}@", self.collectionNode);
// Flush any range changes that happened as part of the update animations ending.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates];
[changeSet executeCompletionHandlerWithFinished:finished];
}];
} completion:completion];
as_log_debug(ASCollectionLog(), "Completed batch update %{public}@", self.collectionNode);
// Flush any range changes that happened as part of submitting the update.