-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ASCollectionView] Small improvements #407
Changes from all commits
fe9e626
e32ad6d
33a9c4d
bd31b05
2bcf410
8b0dfe0
e384428
4b9f906
c5f8754
8726c98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -952,46 +952,60 @@ - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSe | |
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath | ||
{ | ||
ASDisplayNodeAssertMainThread(); | ||
ASCellNode *cell = [self nodeForItemAtIndexPath:indexPath]; | ||
ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; | ||
if (element == nil) { | ||
ASDisplayNodeAssert(NO, @"Unexpected nil element for collectionView:layout:sizeForItemAtIndexPath: %@, %@, %@", self, collectionViewLayout, indexPath); | ||
return CGSizeZero; | ||
} | ||
|
||
ASCellNode *cell = element.node; | ||
if (cell.shouldUseUIKitCell) { | ||
if ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:sizeForItemAtIndexPath:)]) { | ||
CGSize size = [(id)_asyncDelegate collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:indexPath]; | ||
cell.style.preferredSize = size; | ||
return size; | ||
} | ||
} | ||
ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; | ||
return [self sizeForElement:e]; | ||
|
||
return [self sizeForElement:element]; | ||
} | ||
|
||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout referenceSizeForHeaderInSection:(NSInteger)section | ||
{ | ||
ASDisplayNodeAssertMainThread(); | ||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; | ||
ASCellNode *cell = [self supplementaryNodeForElementKind:UICollectionElementKindSectionHeader | ||
atIndexPath:indexPath]; | ||
if (cell.shouldUseUIKitCell && _asyncDelegateFlags.interop) { | ||
ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:UICollectionElementKindSectionHeader | ||
atIndexPath:indexPath]; | ||
if (element == nil) { | ||
return CGSizeZero; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @appleguy In case of interop, should we reach out to the delegate here? Note that since element is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nguyenhuy this is a very good and probably important question to resolve some of the crashes I've been trying to get past. I think the answer will come from...in what case can we get here but have a nil element? If that is expected to be possible / valid, then we probably do need to call the delegate. If it is believed to be invalid, then we should probably add an assertion in the return CGSizeZero case, and not worry about calling the delegate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was assuming that clients that use interop can return an item/supplementary view that is not of type It turns out that, there is a race condition in |
||
} | ||
|
||
if (element.node.shouldUseUIKitCell && _asyncDelegateFlags.interop) { | ||
if ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)]) { | ||
return [(id)_asyncDelegate collectionView:collectionView layout:layout referenceSizeForHeaderInSection:section]; | ||
} | ||
} | ||
ASCollectionElement *e = [_dataController.visibleMap supplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; | ||
return [self sizeForElement:e]; | ||
|
||
return [self sizeForElement:element]; | ||
} | ||
|
||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout referenceSizeForFooterInSection:(NSInteger)section | ||
{ | ||
ASDisplayNodeAssertMainThread(); | ||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; | ||
ASCellNode *cell = [self supplementaryNodeForElementKind:UICollectionElementKindSectionFooter | ||
atIndexPath:indexPath]; | ||
if (cell.shouldUseUIKitCell && _asyncDelegateFlags.interop) { | ||
ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:UICollectionElementKindSectionFooter | ||
atIndexPath:indexPath]; | ||
if (element == nil) { | ||
return CGSizeZero; | ||
} | ||
|
||
if (element.node.shouldUseUIKitCell && _asyncDelegateFlags.interop) { | ||
if ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)]) { | ||
return [(id)_asyncDelegate collectionView:collectionView layout:layout referenceSizeForFooterInSection:section]; | ||
} | ||
} | ||
ASCollectionElement *e = [_dataController.visibleMap supplementaryElementOfKind:UICollectionElementKindSectionFooter atIndexPath:indexPath]; | ||
return [self sizeForElement:e]; | ||
|
||
return [self sizeForElement:element]; | ||
} | ||
|
||
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath | ||
|
@@ -1013,7 +1027,7 @@ - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView | |
view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; | ||
} | ||
|
||
if (_ASCollectionReusableView *reusableView = ASDynamicCast(view, _ASCollectionReusableView)) { | ||
if (_ASCollectionReusableView *reusableView = ASDynamicCastStrict(view, _ASCollectionReusableView)) { | ||
reusableView.element = element; | ||
} | ||
|
||
|
@@ -1039,7 +1053,7 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell | |
|
||
ASDisplayNodeAssert(element != nil, @"Element should exist. indexPath = %@, collectionDataSource = %@", indexPath, self); | ||
|
||
if (_ASCollectionViewCell *asCell = ASDynamicCast(cell, _ASCollectionViewCell)) { | ||
if (_ASCollectionViewCell *asCell = ASDynamicCastStrict(cell, _ASCollectionViewCell)) { | ||
asCell.element = element; | ||
[_rangeController configureContentView:cell.contentView forCellNode:node]; | ||
} | ||
|
@@ -1053,16 +1067,15 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICol | |
[(id <ASCollectionDelegateInterop>)_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; | ||
} | ||
|
||
// Since _ASCollectionViewCell is not available for subclassing, this is faster than isKindOfClass: | ||
// We must exit early here, because only _ASCollectionViewCell implements the -node accessor method. | ||
if ([rawCell class] != [_ASCollectionViewCell class]) { | ||
_ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); | ||
if (cell == nil) { | ||
[_rangeController setNeedsUpdate]; | ||
return; | ||
} | ||
auto cell = (_ASCollectionViewCell *)rawCell; | ||
|
||
|
||
ASCollectionElement *element = cell.element; | ||
if (element) { | ||
ASDisplayNodeAssertTrue([_dataController.visibleMap elementForItemAtIndexPath:indexPath] == element); | ||
[_visibleElements addObject:element]; | ||
} else { | ||
ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplayCell: %@, %@, %@", rawCell, self, indexPath); | ||
|
@@ -1100,7 +1113,7 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICol | |
|
||
[_rangeController setNeedsUpdate]; | ||
|
||
if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { | ||
if ([cell consumesCellNodeVisibilityEvents]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is potentially valuable if made into an API that can be implemented optionally on UIKit cells too. |
||
[_cellsForVisibilityUpdates addObject:cell]; | ||
} | ||
} | ||
|
@@ -1111,14 +1124,13 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( | |
[(id <ASCollectionDelegateInterop>)_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; | ||
} | ||
|
||
// Since _ASCollectionViewCell is not available for subclassing, this is faster than isKindOfClass: | ||
// We must exit early here, because only _ASCollectionViewCell implements the -node accessor method. | ||
if ([rawCell class] != [_ASCollectionViewCell class]) { | ||
_ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); | ||
if (cell == nil) { | ||
[_rangeController setNeedsUpdate]; | ||
return; | ||
} | ||
auto cell = (_ASCollectionViewCell *)rawCell; | ||
|
||
|
||
// Retrieve the element from cell instead of visible map because at this point visible map could have been updated and no longer holds the element. | ||
ASCollectionElement *element = cell.element; | ||
if (element) { | ||
[_visibleElements removeObject:element]; | ||
|
@@ -1150,49 +1162,56 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( | |
|
||
- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)rawView forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath | ||
{ | ||
if (rawView.class != [_ASCollectionReusableView class]) { | ||
_ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); | ||
if (view == nil) { | ||
return; | ||
} | ||
auto view = (_ASCollectionReusableView *)rawView; | ||
|
||
if (view.element) { | ||
[_visibleElements addObject:view.element]; | ||
ASCollectionElement *element = view.element; | ||
if (element) { | ||
ASDisplayNodeAssertTrue([_dataController.visibleMap supplementaryElementOfKind:elementKind atIndexPath:indexPath] == view.element); | ||
[_visibleElements addObject:element]; | ||
} else { | ||
ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplaySupplementaryView: %@, %@, %@", rawView, self, indexPath); | ||
return; | ||
} | ||
|
||
// This is a safeguard similar to the behavior for cells in -[ASCollectionView collectionView:willDisplayCell:forItemAtIndexPath:] | ||
// It ensures _ASCollectionReusableView receives layoutAttributes and calls applyLayoutAttributes. | ||
// Under iOS 10+, cells may be removed/re-added to the collection view without | ||
// receiving prepareForReuse/applyLayoutAttributes, as an optimization for e.g. | ||
// if the user is scrolling back and forth across a small set of items. | ||
// In this case, we have to fetch the layout attributes manually. | ||
// This may be possible under iOS < 10 but it has not been observed yet. | ||
if (view.layoutAttributes == nil) { | ||
view.layoutAttributes = [collectionView layoutAttributesForSupplementaryElementOfKind:elementKind atIndexPath:indexPath]; | ||
} | ||
|
||
if (_asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement) { | ||
GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); | ||
ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; | ||
ASCellNode *node = element.node; | ||
ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); | ||
[_asyncDelegate collectionNode:collectionNode willDisplaySupplementaryElementWithNode:node]; | ||
} | ||
} | ||
|
||
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)rawView forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath | ||
{ | ||
if (rawView.class != [_ASCollectionReusableView class]) { | ||
_ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); | ||
if (view == nil) { | ||
return; | ||
} | ||
auto view = (_ASCollectionReusableView *)rawView; | ||
|
||
if (view.element) { | ||
[_visibleElements removeObject:view.element]; | ||
// Retrieve the element from cell instead of visible map because at this point visible map could have been updated and no longer holds the element. | ||
ASCollectionElement *element = view.element; | ||
if (element) { | ||
[_visibleElements removeObject:element]; | ||
} else { | ||
ASDisplayNodeAssert(NO, @"Unexpected nil element for didEndDisplayingSupplementaryView: %@, %@, %@", rawView, self, indexPath); | ||
return; | ||
} | ||
|
||
if (_asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement) { | ||
GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); | ||
ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; | ||
ASCellNode *node = element.node; | ||
ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); | ||
[_asyncDelegate collectionNode:collectionNode didEndDisplayingSupplementaryElementWithNode:node]; | ||
} | ||
|
@@ -1374,11 +1393,9 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView | |
[self _checkForBatchFetching]; | ||
} | ||
|
||
for (_ASCollectionViewCell *collectionCell in _cellsForVisibilityUpdates) { | ||
for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { | ||
// Only nodes that respond to the selector are added to _cellsForVisibilityUpdates | ||
[[collectionCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged | ||
inScrollView:scrollView | ||
withCellFrame:collectionCell.frame]; | ||
[cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView]; | ||
} | ||
if (_asyncDelegateFlags.scrollViewDidScroll) { | ||
[_asyncDelegate scrollViewDidScroll:scrollView]; | ||
|
@@ -1414,10 +1431,8 @@ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView | |
|
||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView | ||
{ | ||
for (_ASCollectionViewCell *collectionCell in _cellsForVisibilityUpdates) { | ||
[[collectionCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging | ||
inScrollView:scrollView | ||
withCellFrame:collectionCell.frame]; | ||
for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { | ||
[cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging inScrollView:scrollView]; | ||
} | ||
if (_asyncDelegateFlags.scrollViewWillBeginDragging) { | ||
[_asyncDelegate scrollViewWillBeginDragging:scrollView]; | ||
|
@@ -1426,14 +1441,12 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView | |
|
||
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate | ||
{ | ||
for (_ASCollectionViewCell *collectionCell in _cellsForVisibilityUpdates) { | ||
[[collectionCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventDidEndDragging | ||
inScrollView:scrollView | ||
withCellFrame:collectionCell.frame]; | ||
} | ||
if (_asyncDelegateFlags.scrollViewDidEndDragging) { | ||
[_asyncDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; | ||
} | ||
for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it valid to use this type when iterating over _cellsForVisibilityUpdates? The gating factor to add things to this array is whether the cell consumes visibility events, but this might theoretically happen for non-_AS cells too. Maybe we should make the _cellsForVisibilityUpdates collection typed to <id > or something. If this already works correctly because of the early-returns checking for the _AS classes that occur before the cells are added, then that's OK for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm gonna keep this as is as we do make sure only |
||
[cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventDidEndDragging inScrollView:scrollView]; | ||
} | ||
if (_asyncDelegateFlags.scrollViewDidEndDragging) { | ||
[_asyncDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; | ||
} | ||
} | ||
|
||
#pragma mark - Scroll Direction. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,16 +17,27 @@ | |
|
||
#import <UIKit/UIKit.h> | ||
#import <AsyncDisplayKit/ASBaseDefines.h> | ||
#import <AsyncDisplayKit/ASCellNode.h> | ||
|
||
@class ASCellNode, ASCollectionElement; | ||
@class ASCollectionElement; | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
AS_SUBCLASSING_RESTRICTED | ||
AS_SUBCLASSING_RESTRICTED // Note: ASDynamicCastStrict is used on instances of this class based on this restriction. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
@interface _ASCollectionViewCell : UICollectionViewCell | ||
|
||
@property (nonatomic, strong, nullable) ASCollectionElement *element; | ||
@property (nonatomic, strong, readonly, nullable) ASCellNode *node; | ||
@property (nonatomic, strong, nullable) UICollectionViewLayoutAttributes *layoutAttributes; | ||
|
||
/** | ||
* Whether or not this cell is interested in cell node visibility events. | ||
* -cellNodeVisibilityEvent:inScrollView: should be called only if this property is YES. | ||
*/ | ||
@property (nonatomic, readonly) BOOL consumesCellNodeVisibilityEvents; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did these events ever work for Supplementary views? It would be nice if they did, particularly because they use the same ASCellNode and the API is exposed there, so it would be unclear at a user level if they did not get called. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, this has never supported supplementary views. This API is in |
||
|
||
- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because
-supplementaryNodeForElementKind:atIndexPath
internally calls[_dataController.visibleMap supplementaryElementOfKind:atIndexPath].node
, we're literally asking for the element twice in this method. Let's directly reach out to the visible map.