Swiftgram/AsyncDisplayKit/ASCollectionView.mm
Huy Nguyen fb6d1830a0 Beter table/collection update history (#2562)
- Introduce thread-safe ASEventLog
- ASCollectionNode and ASTableNode share their event log with their ASDataController. The controller uses it to log change set submitting and finishing events.
- ASCollectionNode and ASTableNode print their data source and delegate in their debug description.
2016-11-08 16:44:49 -08:00

1737 lines
73 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ASCollectionView.mm
// AsyncDisplayKit
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
//
#import "ASAssert.h"
#import "ASAvailability.h"
#import "ASBatchFetching.h"
#import "ASDelegateProxy.h"
#import "ASCellNode+Internal.h"
#import "ASCollectionDataController.h"
#import "ASCollectionViewLayoutController.h"
#import "ASCollectionViewFlowLayoutInspector.h"
#import "ASDisplayNodeExtras.h"
#import "ASDisplayNode+FrameworkPrivate.h"
#import "ASInternalHelpers.h"
#import "UICollectionViewLayout+ASConvenience.h"
#import "ASRangeController.h"
#import "ASCollectionNode.h"
#import "_ASDisplayLayer.h"
#import "ASCollectionViewLayoutFacilitatorProtocol.h"
#import "ASSectionContext.h"
#import "ASCollectionView+Undeprecated.h"
/// What, if any, invalidation should we perform during the next -layoutSubviews.
typedef NS_ENUM(NSUInteger, ASCollectionViewInvalidationStyle) {
/// Perform no invalidation.
ASCollectionViewInvalidationStyleNone,
/// Perform invalidation with animation (use an empty batch update).
ASCollectionViewInvalidationStyleWithoutAnimation,
/// Perform invalidation without animation (use -invalidateLayout).
ASCollectionViewInvalidationStyleWithAnimation,
};
static const NSUInteger kASCollectionViewAnimationNone = UITableViewRowAnimationNone;
static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
#pragma mark -
#pragma mark ASCellNode<->UICollectionViewCell bridging.
@class _ASCollectionViewCell;
@interface _ASCollectionViewCell : UICollectionViewCell
@property (nonatomic, weak) ASCellNode *node;
@property (nonatomic, strong) UICollectionViewLayoutAttributes *layoutAttributes;
@end
@implementation _ASCollectionViewCell
- (void)setNode:(ASCellNode *)node
{
ASDisplayNodeAssertMainThread();
node.layoutAttributes = _layoutAttributes;
_node = node;
[node __setSelectedFromUIKit:self.selected];
[node __setHighlightedFromUIKit:self.highlighted];
}
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
[_node __setSelectedFromUIKit:selected];
}
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
[_node __setHighlightedFromUIKit:highlighted];
}
- (void)setLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
_layoutAttributes = layoutAttributes;
_node.layoutAttributes = layoutAttributes;
}
- (void)prepareForReuse
{
self.layoutAttributes = nil;
// Need to clear node pointer before UIKit calls setSelected:NO / setHighlighted:NO on its cells
self.node = nil;
[super prepareForReuse];
}
/**
* In the initial case, this is called by UICollectionView during cell dequeueing, before
* we get a chance to assign a node to it, so we must be sure to set these layout attributes
* on our node when one is next assigned to us in @c setNode: . Since there may be cases when we _do_ already
* have our node assigned e.g. during a layout update for existing cells, we also attempt
* to update it now.
*/
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
self.layoutAttributes = layoutAttributes;
}
@end
#pragma mark -
#pragma mark ASCollectionView.
@interface ASCollectionView () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASCollectionDataControllerSource, ASCellNodeInteractionDelegate, ASDelegateProxyInterceptor, ASBatchFetchingScrollView, ASDataControllerEnvironmentDelegate, ASCALayerExtendedDelegate> {
ASCollectionViewProxy *_proxyDataSource;
ASCollectionViewProxy *_proxyDelegate;
ASCollectionDataController *_dataController;
ASRangeController *_rangeController;
ASCollectionViewLayoutController *_layoutController;
id<ASCollectionViewLayoutInspecting> _defaultLayoutInspector;
__weak id<ASCollectionViewLayoutInspecting> _layoutInspector;
NSMutableSet *_cellsForVisibilityUpdates;
id<ASCollectionViewLayoutFacilitatorProtocol> _layoutFacilitator;
BOOL _performingBatchUpdates;
NSUInteger _superBatchUpdateCount;
NSMutableArray *_batchUpdateBlocks;
BOOL _isDeallocating;
ASBatchContext *_batchContext;
CGSize _lastBoundsSizeUsedForMeasuringNodes;
BOOL _ignoreNextBoundsSizeChangeForMeasuringNodes;
NSMutableSet *_registeredSupplementaryKinds;
CGPoint _deceleratingVelocity;
ASCollectionViewInvalidationStyle _nextLayoutInvalidationStyle;
/**
* Our layer, retained. Under iOS < 9, when collection views are removed from the hierarchy,
* their layers may be deallocated and become dangling pointers. This puts the collection view
* into a very dangerous state where pretty much any call will crash it. So we manually retain our layer.
*
* You should never access this, and it will be nil under iOS >= 9.
*/
CALayer *_retainedLayer;
/**
* If YES, the `UICollectionView` will reload its data on next layout pass so we should not forward any updates to it.
* Rationale:
* In `reloadData`, a collection view invalidates its data and marks itself as needing reload, and waits until `layoutSubviews` to requery its data source.
* This can lead to data inconsistency problems.
* Say you have an empty collection view. You call `reloadData`, then immediately insert an item into your data source and call `insertItemsAtIndexPaths:[0,0]`.
* You will get an assertion failure saying `Invalid number of items in section 0.
* The number of items after the update (1) must be equal to the number of items before the update (1) plus or minus the items added and removed (1 added, 0 removed).`
* The collection view never queried your data source before the update to see that it actually had 0 items.
*/
BOOL _superIsPendingDataLoad;
struct {
unsigned int scrollViewDidScroll:1;
unsigned int scrollViewWillBeginDragging:1;
unsigned int scrollViewDidEndDragging:1;
unsigned int scrollViewWillEndDragging:1;
unsigned int collectionViewWillDisplayNodeForItem:1;
unsigned int collectionViewWillDisplayNodeForItemDeprecated:1;
unsigned int collectionViewDidEndDisplayingNodeForItem:1;
unsigned int collectionViewShouldSelectItem:1;
unsigned int collectionViewDidSelectItem:1;
unsigned int collectionViewShouldDeselectItem:1;
unsigned int collectionViewDidDeselectItem:1;
unsigned int collectionViewShouldHighlightItem:1;
unsigned int collectionViewDidHighlightItem:1;
unsigned int collectionViewDidUnhighlightItem:1;
unsigned int collectionViewShouldShowMenuForItem:1;
unsigned int collectionViewCanPerformActionForItem:1;
unsigned int collectionViewPerformActionForItem:1;
unsigned int collectionViewWillBeginBatchFetch:1;
unsigned int shouldBatchFetchForCollectionView:1;
unsigned int collectionNodeWillDisplayItem:1;
unsigned int collectionNodeDidEndDisplayingItem:1;
unsigned int collectionNodeShouldSelectItem:1;
unsigned int collectionNodeDidSelectItem:1;
unsigned int collectionNodeShouldDeselectItem:1;
unsigned int collectionNodeDidDeselectItem:1;
unsigned int collectionNodeShouldHighlightItem:1;
unsigned int collectionNodeDidHighlightItem:1;
unsigned int collectionNodeDidUnhighlightItem:1;
unsigned int collectionNodeShouldShowMenuForItem:1;
unsigned int collectionNodeCanPerformActionForItem:1;
unsigned int collectionNodePerformActionForItem:1;
unsigned int collectionNodeWillBeginBatchFetch:1;
unsigned int collectionNodeWillDisplaySupplementaryElement:1;
unsigned int collectionNodeDidEndDisplayingSupplementaryElement:1;
unsigned int shouldBatchFetchForCollectionNode:1;
} _asyncDelegateFlags;
struct {
unsigned int collectionViewNodeForItem:1;
unsigned int collectionViewNodeBlockForItem:1;
unsigned int collectionViewNodeForSupplementaryElement:1;
unsigned int numberOfSectionsInCollectionView:1;
unsigned int collectionViewNumberOfItemsInSection:1;
unsigned int collectionNodeNodeForItem:1;
unsigned int collectionNodeNodeBlockForItem:1;
unsigned int collectionNodeNodeForSupplementaryElement:1;
unsigned int numberOfSectionsInCollectionNode:1;
unsigned int collectionNodeNumberOfItemsInSection:1;
unsigned int collectionNodeContextForSection:1;
} _asyncDataSourceFlags;
struct {
unsigned int didChangeCollectionViewDataSource:1;
unsigned int didChangeCollectionViewDelegate:1;
unsigned int scrollableDirections:1;
} _layoutInspectorFlags;
}
@property (nonatomic, weak) ASCollectionNode *collectionNode;
@end
@interface ASCollectionNode ()
- (instancetype)_initWithCollectionView:(ASCollectionView *)collectionView;
@end
@implementation ASCollectionView
@synthesize asyncDelegate = _asyncDelegate;
@synthesize asyncDataSource = _asyncDataSource;
// Using _ASDisplayLayer ensures things like -layout are properly forwarded to ASCollectionNode.
+ (Class)layerClass
{
return [_ASDisplayLayer class];
}
#pragma mark -
#pragma mark Lifecycle.
- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout
{
return [self initWithFrame:CGRectZero collectionViewLayout:layout];
}
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
{
return [self _initWithFrame:frame collectionViewLayout:layout layoutFacilitator:nil eventLog:nil];
}
- (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(id<ASCollectionViewLayoutFacilitatorProtocol>)layoutFacilitator eventLog:(ASEventLog *)eventLog
{
if (!(self = [super initWithFrame:frame collectionViewLayout:layout]))
return nil;
_layoutController = [[ASCollectionViewLayoutController alloc] initWithCollectionView:self];
_rangeController = [[ASRangeController alloc] init];
_rangeController.dataSource = self;
_rangeController.delegate = self;
_rangeController.layoutController = _layoutController;
_dataController = [[ASCollectionDataController alloc] initWithDataSource:self eventLog:eventLog];
_dataController.delegate = _rangeController;
_dataController.environmentDelegate = self;
_batchContext = [[ASBatchContext alloc] init];
_leadingScreensForBatching = 2.0;
_performingBatchUpdates = NO;
_batchUpdateBlocks = [NSMutableArray array];
_superIsPendingDataLoad = YES;
_lastBoundsSizeUsedForMeasuringNodes = self.bounds.size;
// If the initial size is 0, expect a size change very soon which is part of the initial configuration
// and should not trigger a relayout.
_ignoreNextBoundsSizeChangeForMeasuringNodes = CGSizeEqualToSize(_lastBoundsSizeUsedForMeasuringNodes, CGSizeZero);
_layoutFacilitator = layoutFacilitator;
_proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self];
super.delegate = (id<UICollectionViewDelegate>)_proxyDelegate;
_proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self];
super.dataSource = (id<UICollectionViewDataSource>)_proxyDataSource;
_registeredSupplementaryKinds = [NSMutableSet set];
_cellsForVisibilityUpdates = [NSMutableSet set];
self.backgroundColor = [UIColor whiteColor];
[self registerClass:[_ASCollectionViewCell class] forCellWithReuseIdentifier:kCellReuseIdentifier];
if (!AS_AT_LEAST_IOS9) {
_retainedLayer = self.layer;
}
return self;
}
- (void)dealloc
{
// Sometimes the UIKit classes can call back to their delegate even during deallocation, due to animation completion blocks etc.
_isDeallocating = YES;
[self setAsyncDelegate:nil];
[self setAsyncDataSource:nil];
}
#pragma mark -
#pragma mark Overrides.
- (void)reloadDataWithCompletion:(void (^)())completion
{
ASPerformBlockOnMainThread(^{
_superIsPendingDataLoad = YES;
[super reloadData];
});
[_dataController reloadDataWithAnimationOptions:kASCollectionViewAnimationNone completion:completion];
}
- (void)reloadData
{
[self reloadDataWithCompletion:nil];
}
- (void)reloadDataImmediately
{
ASDisplayNodeAssertMainThread();
_superIsPendingDataLoad = YES;
[_dataController reloadDataImmediatelyWithAnimationOptions:kASCollectionViewAnimationNone];
[super reloadData];
}
- (void)relayoutItems
{
[_dataController relayoutAllNodes];
}
- (void)waitUntilAllUpdatesAreCommitted
{
ASDisplayNodeAssertMainThread();
[_dataController waitUntilAllUpdatesAreCommitted];
}
- (void)setDataSource:(id<UICollectionViewDataSource>)dataSource
{
// UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil.
ASDisplayNodeAssert(dataSource == nil, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property.");
}
- (void)setDelegate:(id<UICollectionViewDelegate>)delegate
{
// Our UIScrollView superclass sets its delegate to nil on dealloc. Only assert if we get a non-nil value here.
ASDisplayNodeAssert(delegate == nil, @"ASCollectionView uses asyncDelegate, not UICollectionView's delegate property.");
}
- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy
{
if (proxy == _proxyDelegate) {
[self setAsyncDelegate:nil];
} else if (proxy == _proxyDataSource) {
[self setAsyncDataSource:nil];
}
}
- (void)setAsyncDataSource:(id<ASCollectionDataSource>)asyncDataSource
{
// Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle
// the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource
// will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong
// reference to the old dataSource in this case because calls to ASCollectionViewProxy will start failing and cause crashes.
NS_VALID_UNTIL_END_OF_SCOPE id oldDataSource = super.dataSource;
if (asyncDataSource == nil) {
_asyncDataSource = nil;
_proxyDataSource = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self];
memset(&_asyncDataSourceFlags, 0, sizeof(_asyncDataSourceFlags));
} else {
_asyncDataSource = asyncDataSource;
_proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self];
_asyncDataSourceFlags.collectionViewNodeForItem = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeForItemAtIndexPath:)];
_asyncDataSourceFlags.collectionViewNodeBlockForItem = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeBlockForItemAtIndexPath:)];
_asyncDataSourceFlags.numberOfSectionsInCollectionView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)];
_asyncDataSourceFlags.collectionViewNumberOfItemsInSection = [_asyncDataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)];
_asyncDataSourceFlags.collectionViewNodeForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeForSupplementaryElementOfKind:atIndexPath:)];
_asyncDataSourceFlags.collectionNodeNodeForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeForItemAtIndexPath:)];
_asyncDataSourceFlags.collectionNodeNodeBlockForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForItemAtIndexPath:)];
_asyncDataSourceFlags.numberOfSectionsInCollectionNode = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionNode:)];
_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:numberOfItemsInSection:)];
_asyncDataSourceFlags.collectionNodeContextForSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:contextForSection:)];
_asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeForSupplementaryElementOfKind:atIndexPath:)];
ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection || _asyncDataSourceFlags.collectionViewNumberOfItemsInSection, @"Data source must implement collectionNode:numberOfItemsInSection:");
ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNodeBlockForItem
|| _asyncDataSourceFlags.collectionNodeNodeForItem
|| _asyncDataSourceFlags.collectionViewNodeBlockForItem
|| _asyncDataSourceFlags.collectionViewNodeForItem, @"Data source must implement collectionNode:nodeBlockForItemAtIndexPath: or collectionNode:nodeForItemAtIndexPath:");
}
super.dataSource = (id<UICollectionViewDataSource>)_proxyDataSource;
//Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one.
id<ASCollectionViewLayoutInspecting> layoutInspector = self.layoutInspector;
if (_layoutInspectorFlags.didChangeCollectionViewDataSource) {
[layoutInspector didChangeCollectionViewDataSource:asyncDataSource];
}
}
- (void)setAsyncDelegate:(id<ASCollectionDelegate>)asyncDelegate
{
// Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle
// the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate
// will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to hold a strong
// reference to the old delegate in this case because calls to ASCollectionViewProxy will start failing and cause crashes.
NS_VALID_UNTIL_END_OF_SCOPE id oldDelegate = super.delegate;
if (asyncDelegate == nil) {
_asyncDelegate = nil;
_proxyDelegate = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self];
memset(&_asyncDelegateFlags, 0, sizeof(_asyncDelegateFlags));
} else {
_asyncDelegate = asyncDelegate;
_proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self];
_asyncDelegateFlags.scrollViewDidScroll = [_asyncDelegate respondsToSelector:@selector(scrollViewDidScroll:)];
_asyncDelegateFlags.scrollViewWillEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)];
_asyncDelegateFlags.scrollViewWillBeginDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)];
_asyncDelegateFlags.scrollViewDidEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)];
_asyncDelegateFlags.collectionViewWillDisplayNodeForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNode:forItemAtIndexPath:)];
if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem == NO) {
_asyncDelegateFlags.collectionViewWillDisplayNodeForItemDeprecated = [_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)];
}
_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNode:forItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)];
_asyncDelegateFlags.shouldBatchFetchForCollectionView = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)];
_asyncDelegateFlags.collectionViewShouldSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldSelectItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewDidSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewShouldDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldDeselectItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewDidDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didDeselectItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewShouldHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldHighlightItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewDidHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didHighlightItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewDidUnhighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionView:didUnhighlightItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldShowMenuForItemAtIndexPath:)];
_asyncDelegateFlags.collectionViewCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:canPerformAction:forItemAtIndexPath:withSender:)];
_asyncDelegateFlags.collectionViewPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:performAction:forItemAtIndexPath:withSender:)];
_asyncDelegateFlags.collectionNodeWillDisplayItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayItemWithNode:)];
_asyncDelegateFlags.collectionNodeDidEndDisplayingItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingItemWithNode:)];
_asyncDelegateFlags.collectionNodeWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(collectionNode:willBeginBatchFetchWithContext:)];
_asyncDelegateFlags.shouldBatchFetchForCollectionNode = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionNode:)];
_asyncDelegateFlags.collectionNodeShouldSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldSelectItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeDidSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didSelectItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeShouldDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldDeselectItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeDidDeselectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didDeselectItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeShouldHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldHighlightItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeDidHighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didHighlightItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeDidUnhighlightItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didUnhighlightItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldShowMenuForItemAtIndexPath:)];
_asyncDelegateFlags.collectionNodeCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:canPerformAction:forItemAtIndexPath:sender:)];
_asyncDelegateFlags.collectionNodePerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:performAction:forItemAtIndexPath:sender:)];
}
super.delegate = (id<UICollectionViewDelegate>)_proxyDelegate;
//Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one.
id<ASCollectionViewLayoutInspecting> layoutInspector = self.layoutInspector;
if (_layoutInspectorFlags.didChangeCollectionViewDelegate) {
[layoutInspector didChangeCollectionViewDelegate:asyncDelegate];
}
}
- (void)setCollectionViewLayout:(UICollectionViewLayout *)collectionViewLayout
{
[super setCollectionViewLayout:collectionViewLayout];
// Trigger recreation of layout inspector with new collection view layout
if (_layoutInspector != nil) {
_layoutInspector = nil;
[self layoutInspector];
}
}
- (id<ASCollectionViewLayoutInspecting>)layoutInspector
{
if (_layoutInspector == nil) {
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout;
if (layout == nil) {
// Layout hasn't been set yet, we're still init'ing
return nil;
}
if ([layout asdk_isFlowLayout]) {
// Register the default layout inspector delegate for flow layouts only
_defaultLayoutInspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:self flowLayout:layout];
} else {
// Register the default layout inspector delegate for custom collection view layouts
_defaultLayoutInspector = [[ASCollectionViewLayoutInspector alloc] initWithCollectionView:self];
}
// Explicitly call the setter to wire up the _layoutInspectorFlags
self.layoutInspector = _defaultLayoutInspector;
}
return _layoutInspector;
}
- (void)setLayoutInspector:(id<ASCollectionViewLayoutInspecting>)layoutInspector
{
_layoutInspector = layoutInspector;
_layoutInspectorFlags.didChangeCollectionViewDataSource = [_layoutInspector respondsToSelector:@selector(didChangeCollectionViewDataSource:)];
_layoutInspectorFlags.didChangeCollectionViewDelegate = [_layoutInspector respondsToSelector:@selector(didChangeCollectionViewDelegate:)];
_layoutInspectorFlags.scrollableDirections = [_layoutInspector respondsToSelector:@selector(scrollableDirections)];
}
- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType
{
[_rangeController setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType];
}
- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType
{
return [_rangeController tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType];
}
- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
{
[_rangeController setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType];
}
- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
{
return [_rangeController tuningParametersForRangeMode:rangeMode rangeType:rangeType];
}
- (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
{
return [[self nodeForItemAtIndexPath:indexPath] calculatedSize];
}
- (NSArray<NSArray <ASCellNode *> *> *)completedNodes
{
return [_dataController completedNodes];
}
- (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [_dataController nodeAtCompletedIndexPath:indexPath];
}
- (NSIndexPath *)convertIndexPathFromCollectionNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait
{
// If this is a section index path, we don't currently have a method
// to do a mapping.
if (indexPath.item == NSNotFound) {
return indexPath;
} else {
ASCellNode *node = [_dataController nodeAtIndexPath:indexPath];
NSIndexPath *viewIndexPath = [self indexPathForNode:node];
if (viewIndexPath == nil && wait) {
[self waitUntilAllUpdatesAreCommitted];
viewIndexPath = [self indexPathForNode:node];
}
return viewIndexPath;
}
}
- (NSIndexPath *)convertIndexPathToCollectionNode:(NSIndexPath *)indexPath
{
// If this is a section index path, we don't currently have a method
// to do a mapping.
if (indexPath.item == NSNotFound) {
return indexPath;
} else {
ASCellNode *node = [self nodeForItemAtIndexPath:indexPath];
return [_dataController indexPathForNode:node];
}
}
- (NSArray<NSIndexPath *> *)convertIndexPathsToCollectionNode:(NSArray<NSIndexPath *> *)indexPaths
{
if (indexPaths == nil) {
return nil;
}
NSMutableArray<NSIndexPath *> *indexPathsArray = [NSMutableArray arrayWithCapacity:indexPaths.count];
for (NSIndexPath *indexPathInView in indexPaths) {
NSIndexPath *indexPath = [self convertIndexPathToCollectionNode:indexPathInView];
if (indexPath != nil) {
[indexPathsArray addObject:indexPath];
}
}
return indexPathsArray;
}
- (ASCellNode *)supplementaryNodeForElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
return [_dataController supplementaryNodeOfKind:elementKind atIndexPath:indexPath];
}
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
{
return [_dataController completedIndexPathForNode:cellNode];
}
- (NSArray *)visibleNodes
{
NSArray *indexPaths = [self indexPathsForVisibleItems];
NSMutableArray *visibleNodes = [[NSMutableArray alloc] init];
for (NSIndexPath *indexPath in indexPaths) {
ASCellNode *node = [self nodeForItemAtIndexPath:indexPath];
if (node) {
// It is possible for UICollectionView to return indexPaths before the node is completed.
[visibleNodes addObject:node];
}
}
return visibleNodes;
}
#pragma mark Internal
/**
Performing nested batch updates with super (e.g. resizing a cell node & updating collection view during same frame)
can cause super to throw data integrity exceptions because it checks the data source counts before
the update is complete.
Always call [self _superPerform:] rather than [super performBatch:] so that we can keep our `superPerformingBatchUpdates` flag updated.
*/
- (void)_superPerformBatchUpdates:(void(^)())updates completion:(void(^)(BOOL finished))completion
{
ASDisplayNodeAssertMainThread();
_superBatchUpdateCount++;
[super performBatchUpdates:updates completion:completion];
_superBatchUpdateCount--;
}
#pragma mark Assertions.
- (ASDataController *)dataController
{
return _dataController;
}
- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion
{
ASDisplayNodeAssertMainThread();
[_dataController beginUpdates];
updates();
[_dataController endUpdatesAnimated:animated completion:completion];
}
- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion
{
[self performBatchAnimated:YES updates:updates completion:completion];
}
- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind
{
ASDisplayNodeAssert(elementKind != nil, @"A kind is needed for supplementary node registration");
[_registeredSupplementaryKinds addObject:elementKind];
[self registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:elementKind
withReuseIdentifier:[self __reuseIdentifierForKind:elementKind]];
}
- (void)insertSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController insertSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)deleteSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController deleteSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)reloadSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController reloadSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
{
ASDisplayNodeAssertMainThread();
[_dataController moveSection:section toSection:newSection withAnimationOptions:kASCollectionViewAnimationNone];
}
- (id<ASSectionContext>)contextForSection:(NSInteger)section
{
ASDisplayNodeAssertMainThread();
return [_dataController contextForSection:section];
}
- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
{
ASDisplayNodeAssertMainThread();
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone];
}
- (NSString *)__reuseIdentifierForKind:(NSString *)kind
{
return [@"_ASCollectionSupplementaryView_" stringByAppendingString:kind];
}
#pragma mark -
#pragma mark Intercepted selectors.
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
_superIsPendingDataLoad = NO;
return [_dataController completedNumberOfSections];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [_dataController completedNumberOfRowsInSection:section];
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [[self nodeForItemAtIndexPath:indexPath] calculatedSize];
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = [self __reuseIdentifierForKind:kind];
UICollectionReusableView *view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:identifier forIndexPath:indexPath];
ASCellNode *node = [_dataController supplementaryNodeOfKind:kind atIndexPath:indexPath];
ASDisplayNodeAssert(node != nil, @"Supplementary node should exist. Kind = %@, indexPath = %@, collectionDataSource = %@", kind, indexPath, self);
[_rangeController configureContentView:view forCellNode:node];
return view;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
_ASCollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:kCellReuseIdentifier forIndexPath:indexPath];
ASCellNode *node = [self nodeForItemAtIndexPath:indexPath];
cell.node = node;
[_rangeController configureContentView:cell.contentView forCellNode:node];
if (!AS_AT_LEAST_IOS8) {
// Even though UICV was introduced in iOS 6, and UITableView has always had the equivalent method,
// -willDisplayCell: was not introduced until iOS 8 for UICV. didEndDisplayingCell, however, is available.
[self collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath];
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
ASCellNode *cellNode = [cell node];
cellNode.scrollView = collectionView;
// 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 (cell.layoutAttributes == nil) {
cell.layoutAttributes = [collectionView layoutAttributesForItemAtIndexPath:indexPath];
}
ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath);
if (_asyncDelegateFlags.collectionNodeWillDisplayItem) {
[_asyncDelegate collectionNode:self.collectionNode willDisplayItemWithNode:cellNode];
} else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self willDisplayNode:cellNode forItemAtIndexPath:indexPath];
} else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItemDeprecated) {
[_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath];
}
#pragma clang diagnostic pop
[_rangeController setNeedsUpdate];
if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) {
[_cellsForVisibilityUpdates addObject:cell];
}
}
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
ASCellNode *cellNode = [cell node];
ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil.");
if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) {
[_asyncDelegate collectionNode:self.collectionNode didEndDisplayingItemWithNode:cellNode];
} else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
[_rangeController setNeedsUpdate];
[_cellsForVisibilityUpdates removeObject:cell];
cellNode.scrollView = nil;
cell.layoutAttributes = nil;
}
- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement) {
ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath];
ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind);
[_asyncDelegate collectionNode:self.collectionNode willDisplaySupplementaryElementWithNode:node];
}
}
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement) {
ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath];
ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind);
[_asyncDelegate collectionNode:self.collectionNode didEndDisplayingSupplementaryElementWithNode:node];
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeShouldSelectItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
return [_asyncDelegate collectionNode:self.collectionNode shouldSelectItemAtIndexPath:indexPath];
}
} else if (_asyncDelegateFlags.collectionViewShouldSelectItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDelegate collectionView:self shouldSelectItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeDidSelectItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
[_asyncDelegate collectionNode:self.collectionNode didSelectItemAtIndexPath:indexPath];
}
} else if (_asyncDelegateFlags.collectionViewDidSelectItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self didSelectItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeShouldDeselectItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
return [_asyncDelegate collectionNode:self.collectionNode shouldDeselectItemAtIndexPath:indexPath];
}
} else if (_asyncDelegateFlags.collectionViewShouldDeselectItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDelegate collectionView:self shouldDeselectItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeDidDeselectItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
[_asyncDelegate collectionNode:self.collectionNode didDeselectItemAtIndexPath:indexPath];
}
} else if (_asyncDelegateFlags.collectionViewDidDeselectItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self didDeselectItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeShouldHighlightItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
return [_asyncDelegate collectionNode:self.collectionNode shouldHighlightItemAtIndexPath:indexPath];
} else {
return YES;
}
} else if (_asyncDelegateFlags.collectionViewShouldHighlightItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDelegate collectionView:self shouldHighlightItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeDidHighlightItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
[_asyncDelegate collectionNode:self.collectionNode didHighlightItemAtIndexPath:indexPath];
}
} else if (_asyncDelegateFlags.collectionViewDidHighlightItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self didHighlightItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
}
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeDidUnhighlightItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
[_asyncDelegate collectionNode:self.collectionNode didUnhighlightItemAtIndexPath:indexPath];
}
} else if (_asyncDelegateFlags.collectionViewDidUnhighlightItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self didUnhighlightItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (_asyncDelegateFlags.collectionNodeShouldShowMenuForItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
return [_asyncDelegate collectionNode:self.collectionNode shouldShowMenuForItemAtIndexPath:indexPath];
}
} else if (_asyncDelegateFlags.collectionViewShouldShowMenuForItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDelegate collectionView:self shouldShowMenuForItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
}
return NO;
}
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender
{
if (_asyncDelegateFlags.collectionNodeCanPerformActionForItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
return [_asyncDelegate collectionNode:self.collectionNode canPerformAction:action forItemAtIndexPath:indexPath sender:sender];
}
} else if (_asyncDelegateFlags.collectionViewCanPerformActionForItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDelegate collectionView:self canPerformAction:action forItemAtIndexPath:indexPath withSender:sender];
#pragma clang diagnostic pop
}
return NO;
}
- (void)collectionView:(UICollectionView *)collectionView performAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender
{
if (_asyncDelegateFlags.collectionNodePerformActionForItem) {
indexPath = [self convertIndexPathToCollectionNode:indexPath];
if (indexPath != nil) {
[_asyncDelegate collectionNode:self.collectionNode performAction:action forItemAtIndexPath:indexPath sender:sender];
}
} else if (_asyncDelegateFlags.collectionViewPerformActionForItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self performAction:action forItemAtIndexPath:indexPath withSender:sender];
#pragma clang diagnostic pop
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// If a scroll happenes the current range mode needs to go to full
ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController];
if (ASInterfaceStateIncludesVisible(interfaceState)) {
[_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull];
}
for (_ASCollectionViewCell *collectionCell in _cellsForVisibilityUpdates) {
// Only nodes that respond to the selector are added to _cellsForVisibilityUpdates
[[collectionCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged
inScrollView:scrollView
withCellFrame:collectionCell.frame];
}
if (_asyncDelegateFlags.scrollViewDidScroll) {
[_asyncDelegate scrollViewDidScroll:scrollView];
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
CGPoint contentOffset = scrollView.contentOffset;
_deceleratingVelocity = CGPointMake(
contentOffset.x - ((targetContentOffset != NULL) ? targetContentOffset->x : 0),
contentOffset.y - ((targetContentOffset != NULL) ? targetContentOffset->y : 0)
);
if (targetContentOffset != NULL) {
ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist");
[self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollDirection] contentOffset:*targetContentOffset];
}
if (_asyncDelegateFlags.scrollViewWillEndDragging) {
[_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:(targetContentOffset ? : &contentOffset)];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
for (_ASCollectionViewCell *collectionCell in _cellsForVisibilityUpdates) {
[[collectionCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging
inScrollView:scrollView
withCellFrame:collectionCell.frame];
}
if (_asyncDelegateFlags.scrollViewWillBeginDragging) {
[_asyncDelegate scrollViewWillBeginDragging: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];
}
}
#pragma mark - Scroll Direction.
- (ASScrollDirection)scrollDirection
{
CGPoint scrollVelocity;
if (self.isTracking) {
scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview];
} else {
scrollVelocity = _deceleratingVelocity;
}
ASScrollDirection scrollDirection = [self _scrollDirectionForVelocity:scrollVelocity];
return ASScrollDirectionApplyTransform(scrollDirection, self.transform);
}
- (ASScrollDirection)_scrollDirectionForVelocity:(CGPoint)scrollVelocity
{
ASScrollDirection direction = ASScrollDirectionNone;
ASScrollDirection scrollableDirections = [self scrollableDirections];
if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { // Can scroll horizontally.
if (scrollVelocity.x < 0.0) {
direction |= ASScrollDirectionRight;
} else if (scrollVelocity.x > 0.0) {
direction |= ASScrollDirectionLeft;
}
}
if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) { // Can scroll vertically.
if (scrollVelocity.y < 0.0) {
direction |= ASScrollDirectionDown;
} else if (scrollVelocity.y > 0.0) {
direction |= ASScrollDirectionUp;
}
}
return direction;
}
- (ASScrollDirection)scrollableDirections
{
//Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one.
id<ASCollectionViewLayoutInspecting> layoutInspector = self.layoutInspector;
if (_layoutInspectorFlags.scrollableDirections) {
return [layoutInspector scrollableDirections];
} else {
ASScrollDirection scrollableDirection = ASScrollDirectionNone;
CGFloat totalContentWidth = self.contentSize.width + self.contentInset.left + self.contentInset.right;
CGFloat totalContentHeight = self.contentSize.height + self.contentInset.top + self.contentInset.bottom;
if (self.alwaysBounceHorizontal || totalContentWidth > self.bounds.size.width) { // Can scroll horizontally.
scrollableDirection |= ASScrollDirectionHorizontalDirections;
}
if (self.alwaysBounceVertical || totalContentHeight > self.bounds.size.height) { // Can scroll vertically.
scrollableDirection |= ASScrollDirectionVerticalDirections;
}
return scrollableDirection;
}
}
- (ASScrollDirection)flowLayoutScrollableDirections:(UICollectionViewFlowLayout *)flowLayout {
return (flowLayout.scrollDirection == UICollectionViewScrollDirectionHorizontal) ? ASScrollDirectionHorizontalDirections : ASScrollDirectionVerticalDirections;
}
- (void)layoutSubviews
{
if (_zeroContentInsets) {
self.contentInset = UIEdgeInsetsZero;
}
// Flush any pending invalidation action if needed.
ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle;
_nextLayoutInvalidationStyle = ASCollectionViewInvalidationStyleNone;
switch (invalidationStyle) {
case ASCollectionViewInvalidationStyleWithAnimation:
if (0 == _superBatchUpdateCount) {
[self _superPerformBatchUpdates:^{ } completion:nil];
}
break;
case ASCollectionViewInvalidationStyleWithoutAnimation:
[self.collectionViewLayout invalidateLayout];
break;
default:
break;
}
// To ensure _maxSizeForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last
[super layoutSubviews];
// Update range controller immediately if possible & needed.
// Calling -updateIfNeeded in here with self.window == nil (early in the collection view's life)
// may cause UICollectionView data related crashes. We'll update in -didMoveToWindow anyway.
if (self.window != nil) {
[_rangeController updateIfNeeded];
}
}
#pragma mark - Batch Fetching
- (ASBatchContext *)batchContext
{
return _batchContext;
}
- (BOOL)canBatchFetch
{
// if the delegate does not respond to this method, there is no point in starting to fetch
BOOL canFetch = _asyncDelegateFlags.collectionNodeWillBeginBatchFetch || _asyncDelegateFlags.collectionViewWillBeginBatchFetch;
if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionNode) {
return [_asyncDelegate shouldBatchFetchForCollectionNode:self.collectionNode];
} else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionView) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDelegate shouldBatchFetchForCollectionView:self];
#pragma clang diagnostic pop
} else {
return canFetch;
}
}
- (void)_scheduleCheckForBatchFetchingForNumberOfChanges:(NSUInteger)changes
{
// Prevent fetching will continually trigger in a loop after reaching end of content and no new content was provided
if (changes == 0) {
return;
}
// Push this to the next runloop to be sure the scroll view has the right content size
dispatch_async(dispatch_get_main_queue(), ^{
[self _checkForBatchFetching];
});
}
- (void)_checkForBatchFetching
{
// Dragging will be handled in scrollViewWillEndDragging:withVelocity:targetContentOffset:
if (self.isDragging || self.isTracking) {
return;
}
[self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollableDirections] contentOffset:self.contentOffset];
}
- (void)_beginBatchFetchingIfNeededWithScrollView:(UIScrollView<ASBatchFetchingScrollView> *)scrollView forScrollDirection:(ASScrollDirection)scrollDirection contentOffset:(CGPoint)contentOffset
{
if (ASDisplayShouldFetchBatchForScrollView(self, scrollDirection, contentOffset)) {
[self _beginBatchFetching];
}
}
- (void)_beginBatchFetching
{
[_batchContext beginBatchFetching];
if (_asyncDelegateFlags.collectionNodeWillBeginBatchFetch) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_asyncDelegate collectionNode:self.collectionNode willBeginBatchFetchWithContext:_batchContext];
});
} else if (_asyncDelegateFlags.collectionViewWillBeginBatchFetch) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[_asyncDelegate collectionView:self willBeginBatchFetchWithContext:_batchContext];
#pragma clang diagnostic pop
});
}
}
#pragma mark - ASDataControllerSource
- (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath {
ASCellNodeBlock block = nil;
if (_asyncDataSourceFlags.collectionNodeNodeBlockForItem) {
block = [_asyncDataSource collectionNode:self.collectionNode nodeBlockForItemAtIndexPath:indexPath];
} else if (_asyncDataSourceFlags.collectionNodeNodeForItem) {
ASCellNode *node = [_asyncDataSource collectionNode:self.collectionNode nodeForItemAtIndexPath:indexPath];
if ([node isKindOfClass:[ASCellNode class]]) {
block = ^{
return node;
};
} else {
ASDisplayNodeFailAssert(@"Data source returned invalid node from tableNode:nodeForRowAtIndexPath:. Node: %@", node);
}
} else if (_asyncDataSourceFlags.collectionViewNodeBlockForItem) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
block = [_asyncDataSource collectionView:self nodeBlockForItemAtIndexPath:indexPath];
} else if (_asyncDataSourceFlags.collectionViewNodeForItem) {
ASCellNode *node = [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath];
#pragma clang diagnostic pop
if ([node isKindOfClass:[ASCellNode class]]) {
block = ^{
return node;
};
} else {
ASDisplayNodeFailAssert(@"Data source returned invalid node from tableView:nodeForRowAtIndexPath:. Node: %@", node);
}
}
// Handle nil node block
if (block == nil) {
ASDisplayNodeFailAssert(@"ASTableNode could not get a node block for row at index path %@", indexPath);
block = ^{
return [[ASCellNode alloc] init];
};
}
// Wrap the node block
__weak __typeof__(self) weakSelf = self;
return ^{
__typeof__(self) strongSelf = weakSelf;
ASCellNode *node = (block != nil ? block() : [[ASCellNode alloc] init]);
[node enterHierarchyState:ASHierarchyStateRangeManaged];
if (node.interactionDelegate == nil) {
node.interactionDelegate = strongSelf;
}
return node;
};
return block;
}
- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
{
return [self.layoutInspector collectionView:self constrainedSizeForNodeAtIndexPath:indexPath];
}
- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section
{
if (_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection) {
return [_asyncDataSource collectionNode:self.collectionNode numberOfItemsInSection:section];
} else if (_asyncDataSourceFlags.collectionViewNumberOfItemsInSection) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDataSource collectionView:self numberOfItemsInSection:section];
#pragma clang diagnostic pop
} else {
return 0;
}
}
- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController {
if (_asyncDataSourceFlags.numberOfSectionsInCollectionNode) {
return [_asyncDataSource numberOfSectionsInCollectionNode:self.collectionNode];
} else if (_asyncDataSourceFlags.numberOfSectionsInCollectionView) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [_asyncDataSource numberOfSectionsInCollectionView:self];
#pragma clang diagnostic pop
} else {
return 1;
}
}
- (id<ASEnvironment>)dataControllerEnvironment
{
return self.collectionNode;
}
#pragma mark - ASCollectionViewDataControllerSource
- (ASCellNode *)dataController:(ASCollectionDataController *)dataController supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
ASCellNode *node = nil;
if (_asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) {
node = [_asyncDataSource collectionNode:self.collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath];
} else if (_asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
node = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath];
#pragma clang diagnostic pop
}
ASDisplayNodeAssert(node != nil, @"A node must be returned for supplementary element of kind '%@' at index path '%@'", kind, indexPath);
return node;
}
// TODO: Lock this
- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController
{
return [_registeredSupplementaryKinds allObjects];
}
- (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
return [self.layoutInspector collectionView:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath];
}
- (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section
{
return [self.layoutInspector collectionView:self supplementaryNodesOfKind:kind inSection:section];
}
- (id<ASSectionContext>)dataController:(ASDataController *)dataController contextForSection:(NSInteger)section
{
ASDisplayNodeAssertMainThread();
id<ASSectionContext> context = nil;
if (_asyncDataSourceFlags.collectionNodeContextForSection) {
context = [_asyncDataSource collectionNode:self.collectionNode contextForSection:section];
}
if (context != nil) {
context.collectionView = self;
}
return context;
}
#pragma mark - ASRangeControllerDataSource
- (ASRangeController *)rangeController
{
return _rangeController;
}
- (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController
{
ASDisplayNodeAssertMainThread();
// Calling -indexPathsForVisibleItems will trigger UIKit to call reloadData if it never has, which can result
// in incorrect layout if performed at zero size. We can use the fact that nothing can be visible at zero size to return fast.
BOOL isZeroSized = CGSizeEqualToSize(self.bounds.size, CGSizeZero);
return isZeroSized ? @[] : [self indexPathsForVisibleItems];
}
- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController
{
return self.scrollDirection;
}
- (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController
{
ASDisplayNodeAssertMainThread();
return self.bounds.size;
}
- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController
{
return ASInterfaceStateForDisplayNode(self.collectionNode, self.window);
}
- (ASDisplayNode *)rangeController:(ASRangeController *)rangeController nodeAtIndexPath:(NSIndexPath *)indexPath
{
return [self nodeForItemAtIndexPath:indexPath];
}
- (NSString *)nameForRangeControllerDataSource
{
return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]);
}
#pragma mark - ASRangeControllerDelegate
- (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController
{
ASDisplayNodeAssertMainThread();
_performingBatchUpdates = YES;
}
- (void)rangeController:(ASRangeController *)rangeController didEndUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
{
ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource || _superIsPendingDataLoad) {
if (completion) {
completion(NO);
}
_performingBatchUpdates = NO;
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
}
ASPerformBlockWithoutAnimation(!animated, ^{
NSUInteger numberOfUpdateBlocks = _batchUpdateBlocks.count;
[_layoutFacilitator collectionViewWillPerformBatchUpdates];
[self _superPerformBatchUpdates:^{
for (dispatch_block_t block in _batchUpdateBlocks) {
block();
}
} completion:^(BOOL finished){
// Flush any range changes that happened as part of the update animations ending.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdateBlocks];
if (completion) { completion(finished); }
}];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
});
[_batchUpdateBlocks removeAllObjects];
_performingBatchUpdates = NO;
}
- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController
{
[self _checkForBatchFetching];
}
- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource || _superIsPendingDataLoad) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
}
[_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:_performingBatchUpdates];
if (_performingBatchUpdates) {
[_batchUpdateBlocks addObject:^{
[super insertItemsAtIndexPaths:indexPaths];
}];
} else {
[UIView performWithoutAnimation:^{
[super insertItemsAtIndexPaths:indexPaths];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count];
}];
}
}
- (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource || _superIsPendingDataLoad) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
}
[_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:_performingBatchUpdates];
if (_performingBatchUpdates) {
[_batchUpdateBlocks addObject:^{
[super deleteItemsAtIndexPaths:indexPaths];
}];
} else {
[UIView performWithoutAnimation:^{
[super deleteItemsAtIndexPaths:indexPaths];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count];
}];
}
}
- (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource || _superIsPendingDataLoad) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
}
[_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:_performingBatchUpdates];
if (_performingBatchUpdates) {
[_batchUpdateBlocks addObject:^{
[super insertSections:indexSet];
}];
} else {
[UIView performWithoutAnimation:^{
[super insertSections:indexSet];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count];
}];
}
}
- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource || _superIsPendingDataLoad) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
}
[_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:indexSet batched:_performingBatchUpdates];
if (_performingBatchUpdates) {
[_batchUpdateBlocks addObject:^{
[super deleteSections:indexSet];
}];
} else {
[UIView performWithoutAnimation:^{
[super deleteSections:indexSet];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count];
}];
}
}
#pragma mark - ASCellNodeDelegate
- (void)nodeSelectedStateDidChange:(ASCellNode *)node
{
NSIndexPath *indexPath = [self indexPathForNode:node];
if (indexPath) {
if (node.isSelected) {
[super selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
} else {
[super deselectItemAtIndexPath:indexPath animated:NO];
}
}
}
- (void)nodeHighlightedStateDidChange:(ASCellNode *)node
{
NSIndexPath *indexPath = [self indexPathForNode:node];
if (indexPath) {
[self cellForItemAtIndexPath:indexPath].highlighted = node.isHighlighted;
}
}
- (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged
{
ASDisplayNodeAssertMainThread();
if (!sizeChanged) {
return;
}
NSIndexPath *uikitIndexPath = [self indexPathForNode:node];
if (uikitIndexPath == nil) {
return;
}
[_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[ uikitIndexPath ] batched:NO];
ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle;
if (invalidationStyle == ASCollectionViewInvalidationStyleNone) {
[self setNeedsLayout];
invalidationStyle = ASCollectionViewInvalidationStyleWithAnimation;
}
// If we think we're going to animate, check if this node will prevent it.
if (invalidationStyle == ASCollectionViewInvalidationStyleWithAnimation) {
// TODO: Incorporate `shouldAnimateSizeChanges` into ASEnvironmentState for performance benefit.
static dispatch_once_t onceToken;
static BOOL (^shouldNotAnimateBlock)(ASDisplayNode *);
dispatch_once(&onceToken, ^{
shouldNotAnimateBlock = ^BOOL(ASDisplayNode * _Nonnull node) {
return (node.shouldAnimateSizeChanges == NO);
};
});
if (ASDisplayNodeFindFirstNode(node, shouldNotAnimateBlock) != nil) {
// One single non-animated node causes the whole layout update to be non-animated
invalidationStyle = ASCollectionViewInvalidationStyleWithoutAnimation;
}
}
_nextLayoutInvalidationStyle = invalidationStyle;
}
#pragma mark - Memory Management
- (void)clearContents
{
[_rangeController clearContents];
}
- (void)clearFetchedData
{
[_rangeController clearFetchedData];
}
#pragma mark - _ASDisplayView behavior substitutions
// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element.
// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView.
- (void)willMoveToWindow:(UIWindow *)newWindow
{
BOOL visible = (newWindow != nil);
ASDisplayNode *node = self.collectionNode;
if (visible && !node.inHierarchy) {
[node __enterHierarchy];
}
}
- (void)didMoveToWindow
{
BOOL visible = (self.window != nil);
ASDisplayNode *node = self.collectionNode;
if (!visible && node.inHierarchy) {
[node __exitHierarchy];
}
// Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their
// their update in the layout pass
if (![node supportsRangeManagedInterfaceState]) {
[_rangeController setNeedsUpdate];
[_rangeController updateIfNeeded];
}
}
#pragma mark ASCALayerExtendedDelegate
/**
* UICollectionView inadvertently triggers a -prepareLayout call to its layout object
* between [super setFrame:] and [self layoutSubviews] during size changes. So we need
* to get in there and re-measure our nodes before that -prepareLayout call.
* We can't wait until -layoutSubviews or the end of -setFrame:.
*
* @see @p testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation
*/
- (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds
{
CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes;
if (CGSizeEqualToSize(lastUsedSize, newBounds.size)) {
return;
}
_lastBoundsSizeUsedForMeasuringNodes = newBounds.size;
// First size change occurs during initial configuration. An expensive relayout pass is unnecessary at that time
// and should be avoided, assuming that the initial data loading automatically runs shortly afterward.
if (_ignoreNextBoundsSizeChangeForMeasuringNodes) {
_ignoreNextBoundsSizeChangeForMeasuringNodes = NO;
} else {
// Laying out all nodes is expensive, and performing an empty update may be unsafe
// if the data source has pending changes that it hasn't reported yet the collection
// view will requery the new counts and expect them to match the previous counts.
//
// We only need to do this if the bounds changed in the non-scrollable direction.
// If, for example, a vertical flow layout has its height changed due to a status bar
// appearance update, we do not need to relayout all nodes.
// For a more permanent fix to the unsafety mentioned above, see https://github.com/facebook/AsyncDisplayKit/pull/2182
ASScrollDirection scrollDirection = self.scrollableDirections;
BOOL fixedVertically = (ASScrollDirectionContainsVerticalDirection(scrollDirection) == NO);
BOOL fixedHorizontally = (ASScrollDirectionContainsHorizontalDirection(scrollDirection) == NO);
BOOL changedInNonScrollingDirection = (fixedHorizontally && newBounds.size.width != lastUsedSize.width) || (fixedVertically && newBounds.size.height != lastUsedSize.height);
if (changedInNonScrollingDirection) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// This actually doesn't perform an animation, but prevents the transaction block from being processed in the
// data controller's prevent animation block that would interrupt an interrupted relayout happening in an animation block
// ie. ASCollectionView bounds change on rotation or multi-tasking split view resize.
[self performBatchAnimated:YES updates:^{
[_dataController relayoutAllNodes];
} completion:nil];
// We need to ensure the size requery is done before we update our layout.
[self waitUntilAllUpdatesAreCommitted];
[self.collectionViewLayout invalidateLayout];
}
#pragma clang diagnostic pop
}
}
#pragma mark - UICollectionView dead-end intercepts
#if ASDISPLAYNODE_ASSERTIONS_ENABLED // Remove implementations entirely for efficiency if not asserting.
// intercepted due to not being supported by ASCollectionView (prevent bugs caused by usage)
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0)
{
ASDisplayNodeAssert(![self.asyncDataSource respondsToSelector:_cmd], @"%@ is not supported by ASCollectionView - please remove or disable this data source method.", NSStringFromSelector(_cmd));
return NO;
}
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath NS_AVAILABLE_IOS(9_0)
{
ASDisplayNodeAssert(![self.asyncDataSource respondsToSelector:_cmd], @"%@ is not supported by ASCollectionView - please remove or disable this data source method.", NSStringFromSelector(_cmd));
}
#endif
@end