[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 415 additions and 105 deletions

View File

@ -2,6 +2,7 @@
* Add your own contributions to the next release on the line below this with your name.
- [tvOS] Fixes errors when building against tvOS SDK [Alex Hill](https://github.com/alexhillc) [#728](https://github.com/TextureGroup/Texture/pull/728)
- [ASDisplayNode] Add unit tests for layout z-order changes (with an open issue to fix).
- [ASWrapperCellNode] Introduce a new class allowing more control of UIKit passthrough cells.
- [ASDisplayNode] Consolidate main thread initialization and allow apps to invoke it manually instead of +load.
- [ASRunloopQueue] Introduce new runloop queue(ASCATransactionQueue) to coalesce Interface state update calls for view controller transitions.
- [ASRangeController] Fix stability of "minimum" rangeMode if the app has more than one layout before scrolling.

View File

@ -221,7 +221,7 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) {
- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock __unavailable;
- (void)setLayerBacked:(BOOL)layerBacked AS_UNAVAILABLE("ASCellNode does not support layer-backing");
- (void)setLayerBacked:(BOOL)layerBacked AS_UNAVAILABLE("ASCellNode does not support layer-backing, although subnodes may be layer-backed.");
@end

View File

@ -352,6 +352,25 @@
return NO;
}
- (BOOL)shouldUseUIKitCell
{
return NO;
}
@end
#pragma mark -
#pragma mark ASWrapperCellNode
// TODO: Consider if other calls, such as willDisplayCell, should be bridged to this class.
@implementation ASWrapperCellNode : ASCellNode
- (BOOL)shouldUseUIKitCell
{
return YES;
}
@end

View File

@ -57,6 +57,19 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, assign) BOOL usesSynchronousDataLoading;
/**
* Returns YES if the ASCollectionNode contents are completely synchronized with the underlying collection-view layout.
*/
@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized;
/**
* Schedules a block to be performed (on the main thread) as soon as the completion block is called
* on performBatchUpdates:.
*
* When isSynchronized == YES, the block is run block immediately (before the method returns).
*/
- (void)onDidFinishSynchronizing:(void (^)(void))didFinishSynchronizing;
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(nullable id<ASCollectionViewLayoutFacilitatorProtocol>)layoutFacilitator;
- (instancetype)initWithLayoutDelegate:(id<ASCollectionLayoutDelegate>)layoutDelegate layoutFacilitator:(nullable id<ASCollectionViewLayoutFacilitatorProtocol>)layoutFacilitator;

View File

@ -109,6 +109,30 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, assign) BOOL allowsMultipleSelection;
/**
* A Boolean value that determines whether bouncing always occurs when vertical scrolling reaches the end of the content.
* The default value of this property is NO.
*/
@property (nonatomic, assign) BOOL alwaysBounceVertical;
/**
* A Boolean value that determines whether bouncing always occurs when horizontal scrolling reaches the end of the content view.
* The default value of this property is NO.
*/
@property (nonatomic, assign) BOOL alwaysBounceHorizontal;
/**
* A Boolean value that controls whether the vertical scroll indicator is visible.
* The default value of this property is YES.
*/
@property (nonatomic, assign) BOOL showsVerticalScrollIndicator;
/**
* A Boolean value that controls whether the horizontal scroll indicator is visible.
* The default value of this property is NO.
*/
@property (nonatomic, assign) BOOL showsHorizontalScrollIndicator;
/**
* The layout used to organize the node's items.
*
@ -284,7 +308,7 @@ NS_ASSUME_NONNULL_BEGIN
*
* Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks.
*/
- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))didFinishProcessingUpdates;
- (void)onDidFinishProcessingUpdates:(void (^)(void))didFinishProcessingUpdates;
/**
* Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread.

View File

@ -49,9 +49,13 @@
@property (nonatomic, assign) BOOL usesSynchronousDataLoading;
@property (nonatomic, assign) CGFloat leadingScreensForBatching;
@property (weak, nonatomic) id <ASCollectionViewLayoutInspecting> layoutInspector;
@property (nonatomic, assign) BOOL alwaysBounceVertical;
@property (nonatomic, assign) BOOL alwaysBounceHorizontal;
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) CGPoint contentOffset;
@property (nonatomic, assign) BOOL animatesContentOffset;
@property (nonatomic, assign) BOOL showsVerticalScrollIndicator;
@property (nonatomic, assign) BOOL showsHorizontalScrollIndicator;
@end
@implementation _ASCollectionPendingState
@ -203,14 +207,29 @@
view.allowsMultipleSelection = pendingState.allowsMultipleSelection;
view.usesSynchronousDataLoading = pendingState.usesSynchronousDataLoading;
view.layoutInspector = pendingState.layoutInspector;
view.contentInset = pendingState.contentInset;
// Only apply these flags if they're enabled; the view might come with them turned on.
if (pendingState.alwaysBounceVertical) {
view.alwaysBounceVertical = YES;
}
if (pendingState.alwaysBounceHorizontal) {
view.alwaysBounceHorizontal = YES;
}
UIEdgeInsets contentInset = pendingState.contentInset;
if (!UIEdgeInsetsEqualToEdgeInsets(contentInset, UIEdgeInsetsZero)) {
view.contentInset = contentInset;
}
CGPoint contentOffset = pendingState.contentOffset;
if (!CGPointEqualToPoint(contentOffset, CGPointZero)) {
[view setContentOffset:contentOffset animated:pendingState.animatesContentOffset];
}
if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) {
[view.rangeController updateCurrentRangeWithMode:pendingState.rangeMode];
}
[view setContentOffset:pendingState.contentOffset animated:pendingState.animatesContentOffset];
// Don't need to set collectionViewLayout to the view as the layout was already used to init the view in view block.
}
}
@ -235,10 +254,11 @@
- (void)didEnterPreloadState
{
[super didEnterPreloadState];
// ASCollectionNode is often nested inside of other collections. In this case, ASHierarchyState's RangeManaged bit will be set.
// Intentionally allocate the view here and trigger a layout pass on it, which in turn will trigger the intial data load.
// We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view.
// TODO (ASCL) If this node supports async layout, kick off the initial data load without allocating the view
if (CGRectEqualToRect(self.bounds, CGRectZero) == NO) {
if (ASHierarchyStateIncludesRangeManaged(self.hierarchyState) && CGRectEqualToRect(self.bounds, CGRectZero) == NO) {
[[self view] layoutIfNeeded];
}
}
@ -435,6 +455,82 @@
}
}
- (void)setAlwaysBounceVertical:(BOOL)alwaysBounceVertical
{
if ([self pendingState]) {
_pendingState.alwaysBounceVertical = alwaysBounceVertical;
} else {
ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist");
self.view.alwaysBounceVertical = alwaysBounceVertical;
}
}
- (BOOL)alwaysBounceVertical
{
if ([self pendingState]) {
return _pendingState.alwaysBounceVertical;
} else {
return self.view.alwaysBounceVertical;
}
}
- (void)setAlwaysBounceHorizontal:(BOOL)alwaysBounceHorizontal
{
if ([self pendingState]) {
_pendingState.alwaysBounceHorizontal = alwaysBounceHorizontal;
} else {
ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist");
self.view.alwaysBounceHorizontal = alwaysBounceHorizontal;
}
}
- (BOOL)alwaysBounceHorizontal
{
if ([self pendingState]) {
return _pendingState.alwaysBounceHorizontal;
} else {
return self.view.alwaysBounceHorizontal;
}
}
- (void)setShowsVerticalScrollIndicator:(BOOL)showsVerticalScrollIndicator
{
if ([self pendingState]) {
_pendingState.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
} else {
ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist");
self.view.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
}
}
- (BOOL)showsVerticalScrollIndicator
{
if ([self pendingState]) {
return _pendingState.showsVerticalScrollIndicator;
} else {
return self.view.showsVerticalScrollIndicator;
}
}
- (void)setShowsHorizontalScrollIndicator:(BOOL)showsHorizontalScrollIndicator
{
if ([self pendingState]) {
_pendingState.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
} else {
ASDisplayNodeAssert([self isNodeLoaded], @"ASCollectionNode should be loaded if pendingState doesn't exist");
self.view.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
}
}
- (BOOL)showsHorizontalScrollIndicator
{
if ([self pendingState]) {
return _pendingState.showsHorizontalScrollIndicator;
} else {
return self.view.showsHorizontalScrollIndicator;
}
}
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout
{
if ([self pendingState]) {
@ -745,8 +841,11 @@
return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO);
}
- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion
- (void)onDidFinishProcessingUpdates:(void (^)())completion
{
if (!completion) {
return;
}
if (!self.nodeLoaded) {
completion();
} else {
@ -754,6 +853,23 @@
}
}
- (BOOL)isSynchronized
{
return (self.nodeLoaded ? [self.view isSynchronized] : YES);
}
- (void)onDidFinishSynchronizing:(void (^)())completion
{
if (!completion) {
return;
}
if (!self.nodeLoaded) {
completion();
} else {
[self.view onDidFinishSynchronizing:completion];
}
}
- (void)waitUntilAllUpdatesAreProcessed
{
ASDisplayNodeAssertMainThread();

View File

@ -296,9 +296,15 @@ NS_ASSUME_NONNULL_BEGIN
* See ASCollectionNode.h for full documentation of these methods.
*/
@property (nonatomic, readonly) BOOL isProcessingUpdates;
- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion;
- (void)onDidFinishProcessingUpdates:(void (^)(void))completion;
- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASCollectionNode waitUntilAllUpdatesAreProcessed] instead.");
/**
* See ASCollectionNode.h for full documentation of these methods.
*/
@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized;
- (void)onDidFinishSynchronizing:(void (^)(void))completion;
/**
* Registers the given kind of supplementary node for use in creating node-backed supplementary views.
*

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) {
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.
ASDisplayNodeAssert([node.superclass isSubclassOfClass:[ASCellNode class]] == NO,
@"Placeholder cells for UIKit passthrough should be generic ASCellNodes: %@", node);
return node.style.preferredSize;
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 (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) {
CGSize preferredSize = [self _sizeForUIKitCellWithKind:nil atIndexPath:indexPath];
block = ^{
ASCellNode *node = [[ASCellNode alloc] init];
node.shouldUseUIKitCell = YES;
node.style.preferredSize = preferredSize;
return node;
};
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);
block = ^{
return [[ASCellNode alloc] init];
};
cell = [[ASCellNode alloc] init];
}
}
}
@ -1868,8 +1895,10 @@ 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;
}
@ -1878,7 +1907,6 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
}
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];
}
nodeBlock = ^{
ASCellNode *node = [[ASCellNode alloc] init];
node.shouldUseUIKitCell = useUIKitCell;
node.style.preferredSize = preferredSize;
return node;
};
cell = [[ASWrapperCellNode alloc] init];
cell.style.preferredSize = [self _sizeForUIKitCellWithKind:kind atIndexPath:indexPath];
} else {
cell = [[ASCellNode alloc] init];
}
}
return nodeBlock;
// 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 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.

View File

@ -238,7 +238,7 @@ NS_ASSUME_NONNULL_BEGIN
*
* Calling -waitUntilAllUpdatesAreProcessed is one way to flush any pending update completion blocks.
*/
- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))didFinishProcessingUpdates;
- (void)onDidFinishProcessingUpdates:(void (^)(void))didFinishProcessingUpdates;
/**
* Blocks execution of the main thread until all section and item updates are committed to the view. This method must be called from the main thread.

View File

@ -776,8 +776,11 @@ ASLayoutElementCollectionTableSetTraitCollection(_environmentStateLock)
return (self.nodeLoaded ? [self.view isProcessingUpdates] : NO);
}
- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion
- (void)onDidFinishProcessingUpdates:(void (^)())completion
{
if (!completion) {
return;
}
if (!self.nodeLoaded) {
completion();
} else {

View File

@ -219,7 +219,7 @@ NS_ASSUME_NONNULL_BEGIN
* See ASTableNode.h for full documentation of these methods.
*/
@property (nonatomic, readonly) BOOL isProcessingUpdates;
- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion;
- (void)onDidFinishProcessingUpdates:(void (^)(void))completion;
- (void)waitUntilAllUpdatesAreCommitted ASDISPLAYNODE_DEPRECATED_MSG("Use -[ASTableNode waitUntilAllUpdatesAreProcessed] instead.");
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation ASDISPLAYNODE_DEPRECATED_MSG("Use ASTableNode method instead.");

View File

@ -725,7 +725,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
return [_dataController isProcessingUpdates];
}
- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion
- (void)onDidFinishProcessingUpdates:(void (^)())completion
{
[_dataController onDidFinishProcessingUpdates:completion];
}

View File

@ -91,7 +91,7 @@ extern NSString * const ASCollectionInvalidUpdateException;
- (NSUInteger)dataController:(ASDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section;
- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
- (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout;
/**
The constrained size range for layout. Called only if no data controller layout delegate is provided.
@ -261,9 +261,15 @@ extern NSString * const ASCollectionInvalidUpdateException;
* See ASCollectionNode.h for full documentation of these methods.
*/
@property (nonatomic, readonly) BOOL isProcessingUpdates;
- (void)onDidFinishProcessingUpdates:(nullable void (^)(void))completion;
- (void)onDidFinishProcessingUpdates:(void (^)(void))completion;
- (void)waitUntilAllUpdatesAreProcessed;
/**
* See ASCollectionNode.h for full documentation of these methods.
*/
@property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized;
- (void)onDidFinishSynchronizing:(void (^)(void))completion;
/**
* Notifies the data controller object that its environment has changed. The object will request its environment delegate for new information
* and propagate the information to all visible elements, including ones that are being prepared in background.
@ -274,6 +280,11 @@ extern NSString * const ASCollectionInvalidUpdateException;
*/
- (void)environmentDidChange;
/**
* Reset visibleMap and pendingMap when asyncDataSource and asyncDelegate of collection view become nil.
*/
- (void)clearData;
@end
NS_ASSUME_NONNULL_END

View File

@ -17,6 +17,8 @@
#import <AsyncDisplayKit/ASDataController.h>
#include <atomic>
#import <AsyncDisplayKit/_ASHierarchyChangeSet.h>
#import <AsyncDisplayKit/_ASScopeTimer.h>
#import <AsyncDisplayKit/ASAssert.h>
@ -54,6 +56,8 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat
typedef dispatch_block_t ASDataControllerCompletionBlock;
typedef void (^ASDataControllerSynchronizationBlock)();
@interface ASDataController () {
id<ASDataControllerLayoutDelegate> _layoutDelegate;
@ -66,9 +70,13 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
dispatch_queue_t _editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes.
dispatch_group_t _editingTransactionGroup; // Group of all edit transaction blocks. Useful for waiting.
std::atomic<int> _editingTransactionGroupCount;
BOOL _initialReloadDataHasBeenCalled;
BOOL _synchronized;
NSMutableSet<ASDataControllerSynchronizationBlock> *_onDidFinishSynchronizingBlocks;
struct {
unsigned int supplementaryNodeKindsInSections:1;
unsigned int supplementaryNodesOfKindInSection:1;
@ -98,7 +106,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
_dataSourceFlags.supplementaryNodeKindsInSections = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeKindsInSections:)];
_dataSourceFlags.supplementaryNodesOfKindInSection = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodesOfKind:inSection:)];
_dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:)];
_dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:shouldAsyncLayout:)];
_dataSourceFlags.constrainedSizeForNodeAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForNodeAtIndexPath:)];
_dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)];
_dataSourceFlags.contextForSection = [_dataSource respondsToSelector:@selector(dataController:contextForSection:)];
@ -113,6 +121,9 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
_mainSerialQueue = [[ASMainSerialQueue alloc] init];
_synchronized = YES;
_onDidFinishSynchronizingBlocks = [NSMutableSet set];
const char *queueName = [[NSString stringWithFormat:@"org.AsyncDisplayKit.ASDataController.editingTransactionQueue:%p", self] cStringUsingEncoding:NSASCIIStringEncoding];
_editingTransactionQueue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL);
dispatch_queue_set_specific(_editingTransactionQueue, &kASDataControllerEditingQueueKey, &kASDataControllerEditingQueueContext, NULL);
@ -352,7 +363,8 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
nodeBlock = [dataSource dataController:self nodeBlockAtIndexPath:indexPath];
}
} else {
nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath];
BOOL shouldAsyncLayout = YES;
nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath shouldAsyncLayout:&shouldAsyncLayout];
}
ASSizeRange constrainedSize = ASSizeRangeUnconstrained;
@ -440,29 +452,51 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
- (BOOL)isProcessingUpdates
{
ASDisplayNodeAssertMainThread();
if (_mainSerialQueue.numberOfScheduledBlocks > 0) {
return YES;
} else if (dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_NOW) != 0) {
// After waiting for zero duration, a nonzero value is returned if blocks are still running.
return YES;
}
// Both the _mainSerialQueue and _editingTransactionQueue are drained; we are fully quiesced.
return NO;
#if ASDISPLAYNODE_ASSERTIONS_ENABLED
// Using dispatch_group_wait is much more expensive than our manually managed count, but it's crucial they always match.
BOOL editingTransactionQueueBusy = dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_NOW) != 0;
ASDisplayNodeAssert(editingTransactionQueueBusy == (_editingTransactionGroupCount > 0),
@"editingTransactionQueueBusy = %@, but _editingTransactionGroupCount = %d !",
editingTransactionQueueBusy ? @"YES" : @"NO", (int)_editingTransactionGroupCount);
#endif
return _mainSerialQueue.numberOfScheduledBlocks > 0 || _editingTransactionGroupCount > 0;
}
- (void)onDidFinishProcessingUpdates:(nullable void (^)())completion
- (void)onDidFinishProcessingUpdates:(void (^)())completion
{
ASDisplayNodeAssertMainThread();
if (!completion) {
return;
}
if ([self isProcessingUpdates] == NO) {
ASPerformBlockOnMainThread(completion);
} else {
dispatch_async(_editingTransactionQueue, ^{
// Retry the block. If we're done processing updates, it'll run immediately, otherwise
// wait again for updates to quiesce completely.
[_mainSerialQueue performBlockOnMainThread:^{
// Don't use _mainSerialQueue so that we don't affect -isProcessingUpdates.
dispatch_async(dispatch_get_main_queue(), ^{
[self onDidFinishProcessingUpdates:completion];
}];
});
});
}
}
- (BOOL)isSynchronized {
return _synchronized;
}
- (void)onDidFinishSynchronizing:(void (^)())completion {
ASDisplayNodeAssertMainThread();
if (!completion) {
return;
}
if ([self isSynchronized]) {
ASPerformBlockOnMainThread(completion);
} else {
// Hang on to the completion block so that it gets called the next time view is synchronized to data.
[_onDidFinishSynchronizingBlocks addObject:[completion copy]];
}
}
@ -470,6 +504,20 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
{
ASDisplayNodeAssertMainThread();
_synchronized = NO;
[changeSet addCompletionHandler:^(BOOL finished) {
_synchronized = YES;
[self onDidFinishProcessingUpdates:^{
if (_synchronized) {
for (ASDataControllerSynchronizationBlock block in _onDidFinishSynchronizingBlocks) {
block();
}
[_onDidFinishSynchronizingBlocks removeAllObjects];
}
}];
}];
if (changeSet.includesReloadData) {
if (_initialReloadDataHasBeenCalled) {
as_log_debug(ASCollectionLog(), "reloadData %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView)));
@ -558,6 +606,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
as_log_debug(ASCollectionLog(), "New content: %@", newMap.smallDescription);
Class<ASDataControllerLayoutDelegate> layoutDelegateClass = [self.layoutDelegate class];
++_editingTransactionGroupCount;
dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{
__block __unused os_activity_scope_state_s preparationScope = {}; // unused if deployment target < iOS10
as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope);
@ -577,6 +626,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
self.visibleMap = newMap;
}];
}];
--_editingTransactionGroupCount;
};
// Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements
@ -584,9 +634,17 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
[layoutDelegateClass calculateLayoutWithContext:layoutContext];
completion();
} else {
NSArray<ASCollectionElement *> *elementsToProcess = ASArrayByFlatMapping(newMap,
ASCollectionElement *element,
(element.nodeIfAllocated.calculatedLayout == nil ? element : nil));
NSMutableArray<ASCollectionElement *> *elementsToProcess = [NSMutableArray array];
for (ASCollectionElement *element in newMap) {
ASCellNode *nodeIfAllocated = element.nodeIfAllocated;
if (nodeIfAllocated.shouldUseUIKitCell) {
// If the node exists and we know it is a passthrough cell, we know it will never have a .calculatedLayout.
continue;
} else if (nodeIfAllocated.calculatedLayout == nil) {
// If the node hasn't been allocated, or it doesn't have a valid layout, let's process it.
[elementsToProcess addObject:element];
}
}
[self _allocateNodesFromElements:elementsToProcess completion:completion];
}
});
@ -822,6 +880,15 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
});
}
- (void)clearData
{
ASDisplayNodeAssertMainThread();
if (_initialReloadDataHasBeenCalled) {
[self waitUntilAllUpdatesAreProcessed];
self.visibleMap = self.pendingMap = [[ASElementMap alloc] init];
}
}
# pragma mark - Helper methods
- (void)_scheduleBlockOnMainSerialQueue:(dispatch_block_t)block

View File

@ -63,7 +63,21 @@ NS_ASSUME_NONNULL_BEGIN
@property (atomic, weak, nullable) id<ASRangeManagingNode> owningNode;
@property (nonatomic, assign) BOOL shouldUseUIKitCell;
@property (nonatomic, readonly) BOOL shouldUseUIKitCell;
@end
@class ASWrapperCellNode;
typedef CGSize (^ASSizeForItemBlock)(ASWrapperCellNode *node, CGSize collectionSize);
typedef UICollectionViewCell * _Nonnull(^ASCellForItemBlock)(ASWrapperCellNode *node);
typedef UICollectionReusableView * _Nonnull(^ASViewForSupplementaryBlock)(ASWrapperCellNode *node);
@interface ASWrapperCellNode : ASCellNode
@property (nonatomic, copy, readonly) ASSizeForItemBlock sizeForItemBlock;
@property (nonatomic, copy, readonly) ASCellForItemBlock cellForItemBlock;
@property (nonatomic, copy, readonly) ASViewForSupplementaryBlock viewForSupplementaryBlock;
@end