Skip to content
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

Merged
merged 10 commits into from
Jul 6, 2017
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Overhaul logging and add activity tracing support. [Adlai Holler](https://github.com/Adlai-Holler)
- Fix a crash where scrolling a table view after entering editing mode could lead to bad internal states in the table. [Huy Nguyen](https://github.com/nguyenhuy) [#416](https://github.com/TextureGroup/Texture/pull/416/)
- Fix a crash in collection view that occurs if batch updates are performed while scrolling [Huy Nguyen](https://github.com/nguyenhuy) [#378](https://github.com/TextureGroup/Texture/issues/378)
- Some improvements in ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#407](https://github.com/TextureGroup/Texture/pull/407)

##2.3.4
- [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration [Scott Goodson](https://github.com/appleguy)
Expand Down
121 changes: 67 additions & 54 deletions Source/ASCollectionView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

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.

atIndexPath:indexPath];
if (element == nil) {
return CGSizeZero;
Copy link
Member Author

Choose a reason for hiding this comment

The 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 nil, element.node.shouldUseUIKitCell must be NO here, so this is not a breaking change.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the answer will come from...in what case can we get here but have a nil element?

I was assuming that clients that use interop can return an item/supplementary view that is not of type _ASCollectionViewCell/_ASCollectionReusableView, and so the element can be nil in these cases.

It turns out that, there is a race condition in ASDataController that can cause an element to be nil. We also got crashes reported at Pinterest with similar symptoms. More details here: #420. Now that we've fixed the root cause, I think the nil checks in this PR is less of a concern, although I still want to keep them should code correctness.

}

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
Expand All @@ -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;
}

Expand All @@ -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];
}
Expand All @@ -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);
Expand Down Expand Up @@ -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]) {
Copy link
Member

Choose a reason for hiding this comment

The 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];
}
}
Expand All @@ -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];
Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand All @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 _ASCollectionViewCell are added to this collection. We'll update it to id when we support non-_AS cells.

[cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventDidEndDragging inScrollView:scrollView];
}
if (_asyncDelegateFlags.scrollViewDidEndDragging) {
[_asyncDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
}
}

#pragma mark - Scroll Direction.
Expand Down
6 changes: 6 additions & 0 deletions Source/Base/ASBaseDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@
((c *) ([__val isKindOfClass:[c class]] ? __val : nil));\
})

/// Ensure that class is of certain kind, assuming it is subclass restricted
#define ASDynamicCastStrict(x, c) ({ \
id __val = x;\
((c *) ([__val class] == [c class] ? __val : nil));\
})

/**
* Create a new set by mapping `collection` over `work`, ignoring nil.
*/
Expand Down
4 changes: 3 additions & 1 deletion Source/Details/_ASCollectionReusableView.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@

NS_ASSUME_NONNULL_BEGIN

AS_SUBCLASSING_RESTRICTED
AS_SUBCLASSING_RESTRICTED // Note: ASDynamicCastStrict is used on instances of this class based on this restriction.
@interface _ASCollectionReusableView : UICollectionReusableView

@property (nonatomic, strong, readonly, nullable) ASCellNode *node;
@property (nonatomic, strong, nullable) ASCollectionElement *element;
@property (nonatomic, strong, nullable) UICollectionViewLayoutAttributes *layoutAttributes;

@end

NS_ASSUME_NONNULL_END
3 changes: 1 addition & 2 deletions Source/Details/_ASCollectionReusableView.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
//

#import "_ASCollectionReusableView.h"
#import "ASCellNode+Internal.h"
#import <AsyncDisplayKit/ASCellNode+Internal.h>
#import <AsyncDisplayKit/ASCollectionElement.h>
#import <AsyncDisplayKit/AsyncDisplayKit.h>

@implementation _ASCollectionReusableView

Expand Down
15 changes: 13 additions & 2 deletions Source/Details/_ASCollectionViewCell.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The 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;
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this has never supported supplementary views. This API is in _ASCollectionViewCell so I think it's pretty clear that only _AS item cells are considered. We can of course extend it to work on other types of cells or supplementary views, but it is not in the scope of this PR.


- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView;

@end

NS_ASSUME_NONNULL_END
18 changes: 16 additions & 2 deletions Source/Details/_ASCollectionViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
//

#import "_ASCollectionViewCell.h"
#import "ASCellNode+Internal.h"
#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import <AsyncDisplayKit/ASCellNode+Internal.h>
#import <AsyncDisplayKit/ASCollectionElement.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>

@implementation _ASCollectionViewCell

Expand All @@ -38,6 +38,20 @@ - (void)setElement:(ASCollectionElement *)element
[node __setHighlightedFromUIKit:self.highlighted];
}

- (BOOL)consumesCellNodeVisibilityEvents
{
ASCellNode *node = self.node;
if (node == nil) {
return NO;
}
return ASSubclassOverridesSelector([ASCellNode class], [node class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:));
}

- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView
{
[self.node cellNodeVisibilityEvent:event inScrollView:scrollView withCellFrame:self.frame];
}

- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
Expand Down