// // ASCollectionView.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #ifndef MINIMAL_ASDK #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "Private/ASInternalHelpers.h" #import #import #import #import #import #import #import #import #import #import #import #import /** * A macro to get self.collectionNode and assign it to a local variable, or return * the given value if nil. * * Previously we would set ASCollectionNode's dataSource & delegate to nil * during dealloc. However, our asyncDelegate & asyncDataSource must be set on the * main thread, so if the node is deallocated off-main, we won't learn about the change * until later on. Since our @c collectionNode parameter to delegate methods (e.g. * collectionNode:didEndDisplayingItemWithNode:) is nonnull, it's important that we never * unintentionally pass nil (this will crash in Swift, in production). So we can use * this macro to ensure that our node is still alive before calling out to the user * on its behalf. */ #define GET_COLLECTIONNODE_OR_RETURN(__var, __val) \ ASCollectionNode *__var = self.collectionNode; \ if (__var == nil) { \ return __val; \ } #define ASFlowLayoutDefault(layout, property, default) \ ({ \ UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ flowLayout ? flowLayout.property : default; \ }) // ASCellLayoutMode is an NSUInteger-based NS_OPTIONS field. Be careful with BOOL handling on the // 32-bit Objective-C runtime, and pattern after ASInterfaceStateIncludesVisible() & friends. #define ASCellLayoutModeIncludes(layoutMode) ((_cellLayoutMode & layoutMode) == layoutMode) /// 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; /// Used for all cells and supplementaries. UICV keys by supp-kind+reuseID so this is plenty. static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; #pragma mark - #pragma mark ASCollectionView. @interface ASCollectionView () { ASCollectionViewProxy *_proxyDataSource; ASCollectionViewProxy *_proxyDelegate; ASDataController *_dataController; ASRangeController *_rangeController; ASCollectionViewLayoutController *_layoutController; id _defaultLayoutInspector; __weak id _layoutInspector; NSHashTable<_ASCollectionViewCell *> *_cellsForVisibilityUpdates; NSHashTable *_cellsForLayoutUpdates; id _layoutFacilitator; CGFloat _leadingScreensForBatching; // When we update our data controller in response to an interactive move, // we don't want to tell the collection view about the change (it knows!) BOOL _updatingInResponseToInteractiveMove; BOOL _inverted; NSUInteger _superBatchUpdateCount; BOOL _isDeallocating; ASBatchContext *_batchContext; CGSize _lastBoundsSizeUsedForMeasuringNodes; NSMutableSet *_registeredSupplementaryKinds; // CountedSet because UIKit may display the same element in multiple cells e.g. during animations. NSCountedSet *_visibleElements; CGPoint _deceleratingVelocity; BOOL _zeroContentInsets; ASCollectionViewInvalidationStyle _nextLayoutInvalidationStyle; /** * 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; /** * It's important that we always check for batch fetching at least once, but also * that we do not check for batch fetching for empty updates (as that may cause an infinite * loop of batch fetching, where the batch completes and performBatchUpdates: is called without * actually making any changes.) So to handle the case where a collection is completely empty * (0 sections) we always check at least once after each update (initial reload is the first update.) */ BOOL _hasEverCheckedForBatchFetchingDueToUpdate; /** * Set during beginInteractiveMovementForItemAtIndexPath and UIGestureRecognizerStateEnded * (or UIGestureRecognizerStateFailed, UIGestureRecognizerStateCancelled. */ BOOL _reordering; /** * Counter used to keep track of nested batch updates. */ NSInteger _batchUpdateCount; /** * Keep a strong reference to node till view is ready to release. */ ASCollectionNode *_keepalive_node; struct { unsigned int scrollViewDidScroll:1; unsigned int scrollViewWillBeginDragging:1; unsigned int scrollViewDidEndDragging:1; unsigned int scrollViewWillEndDragging:1; unsigned int scrollViewDidEndDecelerating: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; // Interop flags unsigned int interop:1; unsigned int interopWillDisplayCell:1; unsigned int interopDidEndDisplayingCell:1; unsigned int interopWillDisplaySupplementaryView:1; unsigned int interopdidEndDisplayingSupplementaryView: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 nodeModelForItem:1; unsigned int collectionNodeNodeForSupplementaryElement:1; unsigned int collectionNodeNodeBlockForSupplementaryElement:1; unsigned int collectionNodeSupplementaryElementKindsInSection:1; unsigned int numberOfSectionsInCollectionNode:1; unsigned int collectionNodeNumberOfItemsInSection:1; unsigned int collectionNodeContextForSection:1; unsigned int collectionNodeCanMoveItem:1; unsigned int collectionNodeMoveItem:1; // Whether this data source conforms to ASCollectionDataSourceInterop unsigned int interop:1; // Whether this interop data source returns YES from +dequeuesCellsForNodeBackedItems unsigned int interopAlwaysDequeue:1; // Whether this interop data source implements viewForSupplementaryElementOfKind: unsigned int interopViewForSupplementaryElement:1; unsigned int modelIdentifierMethods:1; // if both modelIdentifierForElementAtIndexPath and indexPathForElementWithModelIdentifier are implemented } _asyncDataSourceFlags; struct { unsigned int constrainedSizeForSupplementaryNodeOfKindAtIndexPath:1; unsigned int supplementaryNodesOfKindInSection:1; unsigned int didChangeCollectionViewDataSource:1; unsigned int didChangeCollectionViewDelegate:1; } _layoutInspectorFlags; BOOL _hasDataControllerLayoutDelegate; } @end @implementation ASCollectionView { __weak id _asyncDelegate; __weak id _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 owningNode:nil eventLog:nil]; } - (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(id)layoutFacilitator owningNode:(ASCollectionNode *)owningNode eventLog:(ASEventLog *)eventLog { if (!(self = [super initWithFrame:frame collectionViewLayout:layout])) return nil; // Disable UICollectionView prefetching. Use super, because self disables this method. // Experiments done by Instagram show that this option being YES (default) // when unused causes a significant hit to scroll performance. // https://github.com/Instagram/IGListKit/issues/318 if (AS_AVAILABLE_IOS_TVOS(10, 10)) { super.prefetchingEnabled = NO; } _layoutController = [[ASCollectionViewLayoutController alloc] initWithCollectionView:self]; _rangeController = [[ASRangeController alloc] init]; _rangeController.dataSource = self; _rangeController.delegate = self; _rangeController.layoutController = _layoutController; _dataController = [[ASDataController alloc] initWithDataSource:self node:owningNode eventLog:eventLog]; _dataController.delegate = _rangeController; _batchContext = [[ASBatchContext alloc] init]; _leadingScreensForBatching = 2.0; _lastBoundsSizeUsedForMeasuringNodes = self.bounds.size; _layoutFacilitator = layoutFacilitator; _proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; super.delegate = (id)_proxyDelegate; _proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; super.dataSource = (id)_proxyDataSource; _registeredSupplementaryKinds = [[NSMutableSet alloc] init]; _visibleElements = [[NSCountedSet alloc] init]; _cellsForVisibilityUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; _cellsForLayoutUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; self.backgroundColor = [UIColor whiteColor]; [self registerClass:[_ASCollectionViewCell class] forCellWithReuseIdentifier:kReuseIdentifier]; [self _configureCollectionViewLayout:layout]; return self; } - (void)dealloc { ASDisplayNodeAssertMainThread(); ASDisplayNodeCAssert(_batchUpdateCount == 0, @"ASCollectionView deallocated in the middle of a batch update."); // Sometimes the UIKit classes can call back to their delegate even during deallocation, due to animation completion blocks etc. _isDeallocating = YES; if (!ASActivateExperimentalFeature(ASExperimentalCollectionTeardown)) { [self setAsyncDelegate:nil]; [self setAsyncDataSource:nil]; } // Data controller & range controller may own a ton of nodes, let's deallocate those off-main. ASPerformBackgroundDeallocation(&_dataController); ASPerformBackgroundDeallocation(&_rangeController); } #pragma mark - #pragma mark Overrides. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" /** * This method is not available to be called by the public i.e. * it should only be called by UICollectionView itself. UICollectionView * does this e.g. during the first layout pass, or if you call -numberOfSections * before its content is loaded. */ - (void)reloadData { [self _superReloadData:nil completion:nil]; // UICollectionView calls -reloadData during first layoutSubviews and when the data source changes. // This fires off the first load of cell nodes. if (_asyncDataSource != nil && !self.dataController.initialReloadDataHasBeenCalled) { [self performBatchUpdates:^{ [_changeSet reloadData]; } completion:nil]; } } #pragma clang diagnostic pop - (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated { if ([self validateIndexPath:indexPath]) { [super scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; } } - (void)relayoutItems { [_dataController relayoutAllNodesWithInvalidationBlock:^{ [self.collectionViewLayout invalidateLayout]; [self invalidateFlowLayoutDelegateMetrics]; }]; } - (BOOL)isProcessingUpdates { return [_dataController isProcessingUpdates]; } - (void)onDidFinishProcessingUpdates:(void (^)())completion { [_dataController onDidFinishProcessingUpdates:completion]; } - (void)waitUntilAllUpdatesAreCommitted { ASDisplayNodeAssertMainThread(); if (_batchUpdateCount > 0) { // This assertion will be enabled soon. // ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd)); return; } [_dataController waitUntilAllUpdatesAreProcessed]; } - (BOOL)isSynchronized { return [_dataController isSynchronized]; } - (void)onDidFinishSynchronizing:(void (^)())completion { [_dataController onDidFinishSynchronizing:completion]; } - (void)setDataSource:(id)dataSource { // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. We also allow this when we're doing interop. ASDisplayNodeAssert(_asyncDelegateFlags.interop || dataSource == nil, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property."); } - (void)setDelegate:(id)delegate { // Our UIScrollView superclass sets its delegate to nil on dealloc. Only assert if we get a non-nil value here. We also allow this when we're doing interop. ASDisplayNodeAssert(_asyncDelegateFlags.interop || 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]; } } - (id)asyncDataSource { return _asyncDataSource; } - (void)setAsyncDataSource:(id)asyncDataSource { // Changing super.dataSource will trigger a setNeedsLayout, so this must happen on the main thread. ASDisplayNodeAssertMainThread(); // 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]; _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:)]; _asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeBlockForSupplementaryElementOfKind:atIndexPath:)]; _asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection = [_asyncDataSource respondsToSelector:@selector(collectionNode:supplementaryElementKindsInSection:)]; _asyncDataSourceFlags.nodeModelForItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:nodeModelForItemAtIndexPath:)]; _asyncDataSourceFlags.collectionNodeCanMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:canMoveItemWithNode:)]; _asyncDataSourceFlags.collectionNodeMoveItem = [_asyncDataSource respondsToSelector:@selector(collectionNode:moveItemAtIndexPath:toIndexPath:)]; _asyncDataSourceFlags.interop = [_asyncDataSource conformsToProtocol:@protocol(ASCollectionDataSourceInterop)]; if (_asyncDataSourceFlags.interop) { id interopDataSource = (id)_asyncDataSource; _asyncDataSourceFlags.interopAlwaysDequeue = [[interopDataSource class] respondsToSelector:@selector(dequeuesCellsForNodeBackedItems)] && [[interopDataSource class] dequeuesCellsForNodeBackedItems]; _asyncDataSourceFlags.interopViewForSupplementaryElement = [interopDataSource respondsToSelector:@selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:)]; } _asyncDataSourceFlags.modelIdentifierMethods = [_asyncDataSource respondsToSelector:@selector(modelIdentifierForElementAtIndexPath:inNode:)] && [_asyncDataSource respondsToSelector:@selector(indexPathForElementWithModelIdentifier:inNode:)]; 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:"); } _dataController.validationErrorSource = asyncDataSource; super.dataSource = (id)_proxyDataSource; //Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one. id layoutInspector = self.layoutInspector; if (_layoutInspectorFlags.didChangeCollectionViewDataSource) { [layoutInspector didChangeCollectionViewDataSource:asyncDataSource]; } [self _asyncDelegateOrDataSourceDidChange]; } - (id)asyncDelegate { return _asyncDelegate; } - (void)setAsyncDelegate:(id)asyncDelegate { // Changing super.delegate will trigger a setNeedsLayout, so this must happen on the main thread. ASDisplayNodeAssertMainThread(); // 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]; _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.scrollViewDidEndDecelerating = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]; _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:)]; _asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplaySupplementaryElementWithNode:)]; _asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingSupplementaryElementWithNode:)]; _asyncDelegateFlags.interop = [_asyncDelegate conformsToProtocol:@protocol(ASCollectionDelegateInterop)]; if (_asyncDelegateFlags.interop) { id interopDelegate = (id)_asyncDelegate; _asyncDelegateFlags.interopWillDisplayCell = [interopDelegate respondsToSelector:@selector(collectionView:willDisplayCell:forItemAtIndexPath:)]; _asyncDelegateFlags.interopDidEndDisplayingCell = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)]; _asyncDelegateFlags.interopWillDisplaySupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:)]; _asyncDelegateFlags.interopdidEndDisplayingSupplementaryView = [interopDelegate respondsToSelector:@selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:)]; } } super.delegate = (id)_proxyDelegate; //Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one. id layoutInspector = self.layoutInspector; if (_layoutInspectorFlags.didChangeCollectionViewDelegate) { [layoutInspector didChangeCollectionViewDelegate:asyncDelegate]; } [self _asyncDelegateOrDataSourceDidChange]; } - (void)_asyncDelegateOrDataSourceDidChange { ASDisplayNodeAssertMainThread(); if (_asyncDataSource == nil && _asyncDelegate == nil && !ASActivateExperimentalFeature(ASExperimentalSkipClearData)) { [_dataController clearData]; } } - (void)setCollectionViewLayout:(nonnull UICollectionViewLayout *)collectionViewLayout { ASDisplayNodeAssertMainThread(); [super setCollectionViewLayout:collectionViewLayout]; [self _configureCollectionViewLayout:collectionViewLayout]; // Trigger recreation of layout inspector with new collection view layout if (_layoutInspector != nil) { _layoutInspector = nil; [self layoutInspector]; } } - (id)layoutInspector { if (_layoutInspector == nil) { UICollectionViewLayout *layout = self.collectionViewLayout; if (layout == nil) { // Layout hasn't been set yet, we're still init'ing return nil; } _defaultLayoutInspector = [layout asdk_layoutInspector]; ASDisplayNodeAssertNotNil(_defaultLayoutInspector, @"You must not return nil from -asdk_layoutInspector. Return [super asdk_layoutInspector] if you have to! Layout: %@", layout); // Explicitly call the setter to wire up the _layoutInspectorFlags self.layoutInspector = _defaultLayoutInspector; } return _layoutInspector; } - (void)setLayoutInspector:(id)layoutInspector { _layoutInspector = layoutInspector; _layoutInspectorFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [_layoutInspector respondsToSelector:@selector(collectionView:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)]; _layoutInspectorFlags.supplementaryNodesOfKindInSection = [_layoutInspector respondsToSelector:@selector(collectionView:supplementaryNodesOfKind:inSection:)]; _layoutInspectorFlags.didChangeCollectionViewDataSource = [_layoutInspector respondsToSelector:@selector(didChangeCollectionViewDataSource:)]; _layoutInspectorFlags.didChangeCollectionViewDelegate = [_layoutInspector respondsToSelector:@selector(didChangeCollectionViewDelegate:)]; if (_layoutInspectorFlags.didChangeCollectionViewDataSource) { [_layoutInspector didChangeCollectionViewDataSource:self.asyncDataSource]; } if (_layoutInspectorFlags.didChangeCollectionViewDelegate) { [_layoutInspector didChangeCollectionViewDelegate:self.asyncDelegate]; } } - (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]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (void)setZeroContentInsets:(BOOL)zeroContentInsets { _zeroContentInsets = zeroContentInsets; } - (BOOL)zeroContentInsets { return _zeroContentInsets; } #pragma clang diagnostic pop /// Uses latest size range from data source and -layoutThatFits:. - (CGSize)sizeForElement:(ASCollectionElement *)element { ASDisplayNodeAssertMainThread(); if (element == nil) { return CGSizeZero; } ASCellNode *node = element.node; ASDisplayNodeAssertNotNil(node, @"Node must not be nil!"); BOOL useUIKitCell = node.shouldUseUIKitCell; if (useUIKitCell) { ASWrapperCellNode *wrapperNode = (ASWrapperCellNode *)node; if (wrapperNode.sizeForItemBlock) { return wrapperNode.sizeForItemBlock(wrapperNode, element.constrainedSize.max); } else { // In this case, it is possible the model indexPath for this element will be nil. Attempt to convert it, // and call out to the delegate directly. If it has been deleted from the model, the size returned will be the layout's default. NSIndexPath *indexPath = [_dataController.visibleMap indexPathForElement:element]; return [self _sizeForUIKitCellWithKind:element.supplementaryElementKind atIndexPath:indexPath]; } } else { return [node layoutThatFits:element.constrainedSize].size; } } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; return [self sizeForElement:e]; } #pragma clang diagnostic pop - (ASCellNode *)nodeForItemAtIndexPath:(NSIndexPath *)indexPath { return [_dataController.visibleMap elementForItemAtIndexPath:indexPath].node; } - (NSIndexPath *)convertIndexPathFromCollectionNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait { if (indexPath == nil) { return nil; } NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; if (viewIndexPath == nil && wait) { [self waitUntilAllUpdatesAreCommitted]; return [self convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:NO]; } return viewIndexPath; } /** * Asserts that the index path is a valid view-index-path, and returns it if so, nil otherwise. */ - (nullable NSIndexPath *)validateIndexPath:(nullable NSIndexPath *)indexPath { if (indexPath == nil) { return nil; } NSInteger section = indexPath.section; if (section >= self.numberOfSections) { ASDisplayNodeFailAssert(@"Collection view index path has invalid section %lu, section count = %lu", (unsigned long)section, (unsigned long)self.numberOfSections); return nil; } NSInteger item = indexPath.item; // item == NSNotFound means e.g. "scroll to this section" and is acceptable if (item != NSNotFound && item >= [self numberOfItemsInSection:section]) { ASDisplayNodeFailAssert(@"Collection view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfItemsInSection:section]); return nil; } return indexPath; } - (NSIndexPath *)convertIndexPathToCollectionNode:(NSIndexPath *)indexPath { if ([self validateIndexPath:indexPath] == nil) { return nil; } return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; } - (NSArray *)convertIndexPathsToCollectionNode:(NSArray *)indexPaths { return ASArrayByFlatMapping(indexPaths, NSIndexPath *viewIndexPath, [self convertIndexPathToCollectionNode:viewIndexPath]); } - (ASCellNode *)supplementaryNodeForElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { return [_dataController.visibleMap supplementaryElementOfKind:elementKind atIndexPath:indexPath].node; } - (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode { return [_dataController.visibleMap indexPathForElement:cellNode.collectionElement]; } - (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; } - (void)invalidateFlowLayoutDelegateMetrics { // Subclass hook } - (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { if (_asyncDataSourceFlags.modelIdentifierMethods) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); NSIndexPath *convertedPath = [self convertIndexPathToCollectionNode:indexPath]; if (convertedPath == nil) { return nil; } else { return [_asyncDataSource modelIdentifierForElementAtIndexPath:convertedPath inNode:collectionNode]; } } else { return nil; } } - (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { if (_asyncDataSourceFlags.modelIdentifierMethods) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); return [_asyncDataSource indexPathForElementWithModelIdentifier:identifier inNode:collectionNode]; } else { return nil; } } #pragma mark Internal - (void)_configureCollectionViewLayout:(nonnull UICollectionViewLayout *)layout { _hasDataControllerLayoutDelegate = [layout conformsToProtocol:@protocol(ASDataControllerLayoutDelegate)]; if (_hasDataControllerLayoutDelegate) { _dataController.layoutDelegate = (id)layout; } } /** This method is called only for UIKit Passthrough cells - either regular Items or Supplementary elements. It checks if the delegate implements the UICollectionViewFlowLayout methods that provide sizes, and if not, uses the default values set on the flow layout. If a flow layout is not in use, UICollectionView Passthrough cells must be sized by logic in the Layout object, and Texture does not participate in these paths. */ - (CGSize)_sizeForUIKitCellWithKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { CGSize size = CGSizeZero; UICollectionViewLayout *l = self.collectionViewLayout; if (kind == nil) { ASDisplayNodeAssert(_asyncDataSourceFlags.interop, @"This code should not be called except for UIKit passthrough compatibility"); SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:); if (indexPath && [_asyncDelegate respondsToSelector:sizeForItem]) { size = [(id)_asyncDelegate collectionView:self layout:l sizeForItemAtIndexPath:indexPath]; } else { size = ASFlowLayoutDefault(l, itemSize, CGSizeZero); } } else if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:); if (indexPath && [_asyncDelegate respondsToSelector:sizeForHeader]) { size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForHeaderInSection:indexPath.section]; } else { size = ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); } } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { ASDisplayNodeAssert(_asyncDataSourceFlags.interopViewForSupplementaryElement, @"This code should not be called except for UIKit passthrough compatibility"); SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:); if (indexPath && [_asyncDelegate respondsToSelector:sizeForFooter]) { size = [(id)_asyncDelegate collectionView:self layout:l referenceSizeForFooterInSection:indexPath.section]; } else { size = ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); } } return size; } - (void)_superReloadData:(void(^)())updates completion:(void(^)(BOOL finished))completion { if (updates) { updates(); } [super reloadData]; if (completion) { completion(YES); } } /** * 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)beginUpdates { ASDisplayNodeAssertMainThread(); // _changeSet must be available during batch update ASDisplayNodeAssertTrue((_batchUpdateCount > 0) == (_changeSet != nil)); if (_batchUpdateCount == 0) { _changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[_dataController itemCountsFromDataSource]]; _changeSet.rootActivity = as_activity_create("Perform async collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT); _changeSet.submitActivity = as_activity_create("Submit changes for collection update", _changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT); } _batchUpdateCount++; } - (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertNotNil(_changeSet, @"_changeSet must be available when batch update ends"); _batchUpdateCount--; // Prevent calling endUpdatesAnimated:completion: in an unbalanced way NSAssert(_batchUpdateCount >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call"); [_changeSet addCompletionHandler:completion]; if (_batchUpdateCount == 0) { _ASHierarchyChangeSet *changeSet = _changeSet; // Nil out _changeSet before forwarding to _dataController to allow the change set to cause subsequent batch updates on the same run loop _changeSet = nil; changeSet.animated = animated; [_dataController updateWithChangeSet:changeSet]; } } - (void)performBatchAnimated:(BOOL)animated updates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion { ASDisplayNodeAssertMainThread(); [self beginUpdates]; as_activity_scope(_changeSet.rootActivity); { // Only include client code in the submit activity, the rest just lives in the root activity. as_activity_scope(_changeSet.submitActivity); if (updates) { updates(); } } [self endUpdatesAnimated:animated completion:completion]; } - (void)performBatchUpdates:(NS_NOESCAPE void (^)())updates completion:(void (^)(BOOL))completion { // We capture the current state of whether animations are enabled if they don't provide us with one. [self performBatchAnimated:[UIView areAnimationsEnabled] updates:updates completion:completion]; } - (void)registerSupplementaryNodeOfKind:(NSString *)elementKind { ASDisplayNodeAssert(elementKind != nil, @"A kind is needed for supplementary node registration"); [_registeredSupplementaryKinds addObject:elementKind]; [self registerClass:[_ASCollectionReusableView class] forSupplementaryViewOfKind:elementKind withReuseIdentifier:kReuseIdentifier]; } - (void)insertSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); if (sections.count == 0) { return; } [self performBatchUpdates:^{ [_changeSet insertSections:sections animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } - (void)deleteSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); if (sections.count == 0) { return; } [self performBatchUpdates:^{ [_changeSet deleteSections:sections animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } - (void)reloadSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); if (sections.count == 0) { return; } [self performBatchUpdates:^{ [_changeSet reloadSections:sections animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); [self performBatchUpdates:^{ [_changeSet moveSection:section toSection:newSection animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } - (id)contextForSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); return [_dataController.visibleMap contextForSection:section]; } - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0) { return; } [self performBatchUpdates:^{ [_changeSet insertItems:indexPaths animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0) { return; } [self performBatchUpdates:^{ [_changeSet deleteItems:indexPaths animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0) { return; } [self performBatchUpdates:^{ [_changeSet reloadItems:indexPaths animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); if (!_reordering) { [self performBatchUpdates:^{ [_changeSet moveItemAtIndexPath:indexPath toIndexPath:newIndexPath animationOptions:kASCollectionViewAnimationNone]; } completion:nil]; } else { [super moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; } } - (BOOL)beginInteractiveMovementForItemAtIndexPath:(NSIndexPath *)indexPath { BOOL success = [super beginInteractiveMovementForItemAtIndexPath:indexPath]; _reordering = success; return success; } - (void)endInteractiveMovement { _reordering = NO; [super endInteractiveMovement]; } - (void)cancelInteractiveMovement { _reordering = NO; [super cancelInteractiveMovement]; } #pragma mark - #pragma mark Intercepted selectors. - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { if (_superIsPendingDataLoad) { [_rangeController setNeedsUpdate]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:1]; _superIsPendingDataLoad = NO; } return _dataController.visibleMap.numberOfSections; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [_dataController.visibleMap numberOfItemsInSection:section]; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; return e ? [self sizeForElement:e] : ASFlowLayoutDefault(layout, itemSize, CGSizeZero); } - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l referenceSizeForHeaderInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); ASElementMap *map = _dataController.visibleMap; ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); } - (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l referenceSizeForFooterInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); ASElementMap *map = _dataController.visibleMap; ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]]; return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); } // For the methods that call delegateIndexPathForSection:withSelector:, translate the section from // visibleMap to pendingMap. If the section no longer exists, or the delegate doesn't implement // the selector, we will return NSNotFound (and then use the ASFlowLayoutDefault). - (NSInteger)delegateIndexForSection:(NSInteger)section withSelector:(SEL)selector { if ([_asyncDelegate respondsToSelector:selector]) { return [_dataController.pendingMap convertSection:section fromMap:_dataController.visibleMap]; } else { return NSNotFound; } } - (UIEdgeInsets)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l insetForSectionAtIndex:(NSInteger)section { section = [self delegateIndexForSection:section withSelector:_cmd]; if (section != NSNotFound) { return [(id)_asyncDelegate collectionView:cv layout:l insetForSectionAtIndex:section]; } return ASFlowLayoutDefault(l, sectionInset, UIEdgeInsetsZero); } - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { section = [self delegateIndexForSection:section withSelector:_cmd]; if (section != NSNotFound) { return [(id)_asyncDelegate collectionView:cv layout:l minimumInteritemSpacingForSectionAtIndex:section]; } return ASFlowLayoutDefault(l, minimumInteritemSpacing, 10.0); // Default is documented as 10.0 } - (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l minimumLineSpacingForSectionAtIndex:(NSInteger)section { section = [self delegateIndexForSection:section withSelector:_cmd]; if (section != NSNotFound) { return [(id)_asyncDelegate collectionView:cv layout:l minimumLineSpacingForSectionAtIndex:section]; } return ASFlowLayoutDefault(l, minimumLineSpacing, 10.0); // Default is documented as 10.0 } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { if ([_registeredSupplementaryKinds containsObject:kind] == NO) { [self registerSupplementaryNodeOfKind:kind]; } UICollectionReusableView *view = nil; ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:kind atIndexPath:indexPath]; ASCellNode *node = element.node; ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil); BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interopViewForSupplementaryElement && wrapperNode); if (wrapperNode.viewForSupplementaryBlock) { view = wrapperNode.viewForSupplementaryBlock(wrapperNode); } else if (shouldDequeueExternally) { // This codepath is used for both IGListKit mode, and app-level UICollectionView interop. view = [(id)_asyncDataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } else { ASDisplayNodeAssert(node != nil, @"Supplementary node should exist. Kind = %@, indexPath = %@, collectionDataSource = %@", kind, indexPath, self); view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; } if (_ASCollectionReusableView *reusableView = ASDynamicCastStrict(view, _ASCollectionReusableView)) { reusableView.element = element; } if (node) { [_rangeController configureContentView:view forCellNode:node]; } return view; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = nil; ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; ASCellNode *node = element.node; ASWrapperCellNode *wrapperNode = (node.shouldUseUIKitCell ? (ASWrapperCellNode *)node : nil); BOOL shouldDequeueExternally = _asyncDataSourceFlags.interopAlwaysDequeue || (_asyncDataSourceFlags.interop && wrapperNode); if (wrapperNode.cellForItemBlock) { cell = wrapperNode.cellForItemBlock(wrapperNode); } else if (shouldDequeueExternally) { cell = [(id)_asyncDataSource collectionView:collectionView cellForItemAtIndexPath:indexPath]; } else { cell = [self dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; } ASDisplayNodeAssert(element != nil, @"Element should exist. indexPath = %@, collectionDataSource = %@", indexPath, self); ASDisplayNodeAssert(cell != nil, @"UICollectionViewCell must not be nil. indexPath = %@, collectionDataSource = %@", indexPath, self); if (_ASCollectionViewCell *asCell = ASDynamicCastStrict(cell, _ASCollectionViewCell)) { asCell.element = element; [_rangeController configureContentView:cell.contentView forCellNode:node]; } return cell; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopWillDisplayCell) { ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; if (node.shouldUseUIKitCell) { [(id )_asyncDelegate collectionView:collectionView willDisplayCell:rawCell forItemAtIndexPath:indexPath]; } } _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); if (cell == nil) { [_rangeController setNeedsUpdate]; return; } 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); return; } ASCellNode *cellNode = element.node; cellNode.scrollView = collectionView; // Update the selected background view in collectionView:willDisplayCell:forItemAtIndexPath: otherwise it could be too // early e.g. if the selectedBackgroundView was set in didLoad() cell.selectedBackgroundView = cellNode.selectedBackgroundView; cell.backgroundView = cellNode.backgroundView; // 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 && self.collectionNode != nil) { [_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 ([cell consumesCellNodeVisibilityEvents]) { [_cellsForVisibilityUpdates addObject:cell]; } } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)rawCell forItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopDidEndDisplayingCell) { ASCellNode *node = [self nodeForItemAtIndexPath:indexPath]; if (node.shouldUseUIKitCell) { [(id )_asyncDelegate collectionView:collectionView didEndDisplayingCell:rawCell forItemAtIndexPath:indexPath]; } } _ASCollectionViewCell *cell = ASDynamicCastStrict(rawCell, _ASCollectionViewCell); if (cell == nil) { [_rangeController setNeedsUpdate]; return; } // 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]; } else { ASDisplayNodeAssert(NO, @"Unexpected nil element for didEndDisplayingCell: %@, %@, %@", rawCell, self, indexPath); return; } ASCellNode *cellNode = element.node; if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { if (ASCollectionNode *collectionNode = self.collectionNode) { [_asyncDelegate collectionNode: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 *)rawView forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.interopWillDisplaySupplementaryView) { ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; if (node.shouldUseUIKitCell) { [(id )_asyncDelegate collectionView:collectionView willDisplaySupplementaryView:rawView forElementKind:elementKind atIndexPath:indexPath]; } } _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); if (view == nil) { return; } 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; } // 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 = 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 (_asyncDelegateFlags.interopdidEndDisplayingSupplementaryView) { ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; if (node.shouldUseUIKitCell) { [(id )_asyncDelegate collectionView:collectionView didEndDisplayingSupplementaryView:rawView forElementOfKind:elementKind atIndexPath:indexPath]; } } _ASCollectionReusableView *view = ASDynamicCastStrict(rawView, _ASCollectionReusableView); if (view == nil) { return; } // 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 = element.node; ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); [_asyncDelegate collectionNode:collectionNode didEndDisplayingSupplementaryElementWithNode:node]; } } - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldSelectItem) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { [_asyncDelegate collectionNode: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 } } - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath { // Mimic UIKit's gating logic. // If the data source doesn't support moving, then all bets are off. if (!_asyncDataSourceFlags.collectionNodeMoveItem) { return NO; } // Currently we do not support interactive moves when using async layout. The reason is, we do not have a mechanism // to propagate the "presentation data" element map (containing the speculative in-progress moves) to the layout delegate, // and this can cause exceptions to be thrown from UICV. For example, if you drag an item out of a section, // the element map will still contain N items in that section, even though there's only N-1 shown, and UICV will // throw an exception that you specified an element that doesn't exist. // // In iOS >= 11, this is made much easier by the UIDataSourceTranslating API. In previous versions of iOS our best bet // would be to capture the invalidation contexts that are sent during interactive moves and make our own data source translator. if ([self.collectionViewLayout isKindOfClass:[ASCollectionLayout class]]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ as_log_debug(ASCollectionLog(), "Collection node item interactive movement is not supported when using a layout delegate. This message will only be logged once. Node: %@", ASObjectDescriptionMakeTiny(self)); }); return NO; } // If the data source implements canMoveItem, let them decide. if (_asyncDataSourceFlags.collectionNodeCanMoveItem) { if (ASCellNode *cellNode = [self nodeForItemAtIndexPath:indexPath]) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); return [_asyncDataSource collectionNode:collectionNode canMoveItemWithNode:cellNode]; } } // Otherwise allow the move for all items. return YES; } - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeMoveItem, @"Should not allow interactive collection item movement if data source does not support it."); // Inform the data source first, in case they call nodeForItemAtIndexPath:. // We want to make sure we return them the node for the item they have in mind. if (ASCollectionNode *collectionNode = self.collectionNode) { [_asyncDataSource collectionNode:collectionNode moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; } // Now we update our data controller's store. // Get up to date [self waitUntilAllUpdatesAreCommitted]; // Set our flag to suppress informing super about the change. ASDisplayNodeAssertFalse(_updatingInResponseToInteractiveMove); _updatingInResponseToInteractiveMove = YES; // Submit the move [self moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; // Wait for it to finish – should be fast! [self waitUntilAllUpdatesAreCommitted]; // Clear the flag _updatingInResponseToInteractiveMove = NO; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; if (ASInterfaceStateIncludesVisible(interfaceState)) { [self _checkForBatchFetching]; } for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { // _cellsForVisibilityUpdates only includes cells for ASCellNode subclasses with overrides of the visibility method. [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView]; } 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 _beginBatchFetchingIfNeededWithContentOffset:*targetContentOffset velocity:velocity]; } if (_asyncDelegateFlags.scrollViewWillEndDragging) { [_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:(targetContentOffset ? : &contentOffset)]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { _deceleratingVelocity = CGPointZero; if (_asyncDelegateFlags.scrollViewDidEndDecelerating) { [_asyncDelegate scrollViewDidEndDecelerating:scrollView]; } } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // If a scroll happens the current range mode needs to go to full _rangeController.contentHasBeenScrolled = YES; [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging inScrollView:scrollView]; } if (_asyncDelegateFlags.scrollViewWillBeginDragging) { [_asyncDelegate scrollViewWillBeginDragging:scrollView]; } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { for (_ASCollectionViewCell *cell in _cellsForVisibilityUpdates) { [cell cellNodeVisibilityEvent:ASCellNodeVisibilityEventDidEndDragging inScrollView:scrollView]; } if (_asyncDelegateFlags.scrollViewDidEndDragging) { [_asyncDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; } } #pragma mark - Scroll Direction. - (BOOL)inverted { return _inverted; } - (void)setInverted:(BOOL)inverted { _inverted = inverted; } - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { if (_leadingScreensForBatching != leadingScreensForBatching) { _leadingScreensForBatching = leadingScreensForBatching; // 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]; }); } } - (CGFloat)leadingScreensForBatching { return _leadingScreensForBatching; } - (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 { ASDisplayNodeAssertNotNil(self.layoutInspector, @"Layout inspector should be assigned."); return [self.layoutInspector scrollableDirections]; } - (void)layoutSubviews { if (_cellsForLayoutUpdates.count > 0) { NSArray *nodes = [_cellsForLayoutUpdates allObjects]; [_cellsForLayoutUpdates removeAllObjects]; NSMutableArray *nodesSizeChanged = [[NSMutableArray alloc] init]; [_dataController relayoutNodes:nodes nodesSizeChanged:nodesSizeChanged]; [self nodesDidRelayout:nodesSizeChanged]; } // Flush any pending invalidation action if needed. ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle; _nextLayoutInvalidationStyle = ASCollectionViewInvalidationStyleNone; switch (invalidationStyle) { case ASCollectionViewInvalidationStyleWithAnimation: if (0 == _superBatchUpdateCount) { if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysReloadData)) { [self _superReloadData:nil completion:nil]; } else { [self _superPerformBatchUpdates:nil 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]; if (_zeroContentInsets) { self.contentInset = UIEdgeInsetsZero; } // 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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); return [_asyncDelegate shouldBatchFetchForCollectionNode: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; } } - (id)batchFetchingDelegate{ return self.collectionNode.batchFetchingDelegate; } - (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 && _hasEverCheckedForBatchFetchingDueToUpdate) { return; } _hasEverCheckedForBatchFetchingDueToUpdate = YES; // 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 _beginBatchFetchingIfNeededWithContentOffset:self.contentOffset velocity:CGPointZero]; } - (void)_beginBatchFetchingIfNeededWithContentOffset:(CGPoint)contentOffset velocity:(CGPoint)velocity { if (ASDisplayShouldFetchBatchForScrollView(self, self.scrollDirection, self.scrollableDirections, contentOffset, velocity)) { [self _beginBatchFetching]; } } - (void)_beginBatchFetching { as_activity_create_for_scope("Batch fetch for collection node"); [_batchContext beginBatchFetching]; if (_asyncDelegateFlags.collectionNodeWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); as_log_debug(ASCollectionLog(), "Beginning batch fetch for %@ with context %@", collectionNode, _batchContext); [_asyncDelegate collectionNode: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 - (BOOL)dataController:(ASDataController *)dataController shouldEagerlyLayoutNode:(ASCellNode *)node { NSAssert(!ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysLazy), @"ASCellLayoutModeAlwaysLazy flag is no longer supported"); return !node.shouldUseUIKitCell; } - (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyProcessChangeSet:(_ASHierarchyChangeSet *)changeSet { // If we have AlwaysSync set, block and donate main priority. if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysSync)) { return YES; } // Prioritize AlwaysAsync over the remaining heuristics for the Default mode. if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysAsync)) { return NO; } // Reload data is expensive, don't block main while doing so. if (changeSet.includesReloadData) { return NO; } // If we have very few ASCellNodes (besides UIKit passthrough ones), match UIKit by blocking. if (changeSet.countForAsyncLayout < 2) { return YES; } CGSize contentSize = self.contentSize; CGSize boundsSize = self.bounds.size; if (contentSize.height <= boundsSize.height && contentSize.width <= boundsSize.width) { return YES; } return NO; // ASCellLayoutModeNone } - (BOOL)dataControllerShouldSerializeNodeCreation:(ASDataController *)dataController { return ASCellLayoutModeIncludes(ASCellLayoutModeSerializeNodeCreation); } - (id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath { if (!_asyncDataSourceFlags.nodeModelForItem) { return nil; } GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); return [_asyncDataSource collectionNode:collectionNode nodeModelForItemAtIndexPath:indexPath]; } - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout { ASDisplayNodeAssertMainThread(); ASCellNodeBlock block = nil; ASCellNode *cell = nil; if (_asyncDataSourceFlags.collectionNodeNodeBlockForItem) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); block = [_asyncDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]; } if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForItem) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); cell = [_asyncDataSource collectionNode:collectionNode nodeForItemAtIndexPath:indexPath]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeBlockForItem) { block = [_asyncDataSource collectionView:self nodeBlockForItemAtIndexPath:indexPath]; } if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForItem) { cell = [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath]; } #pragma clang diagnostic pop if (block == nil) { if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) { // In this case, either the client is expecting a UIKit passthrough cell to be created automatically, // or it is an error. if (_asyncDataSourceFlags.interop) { cell = [[ASWrapperCellNode alloc] init]; } else { ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the protocol!", indexPath, cell, block); cell = [[ASCellNode alloc] init]; } } // This condition is intended to run for either cells received from the datasource, or created just above. if (cell.shouldUseUIKitCell) { *shouldAsyncLayout = NO; } } // Wrap the node block BOOL disableRangeController = ASCellLayoutModeIncludes(ASCellLayoutModeDisableRangeController); __weak __typeof__(self) weakSelf = self; return ^{ __typeof__(self) strongSelf = weakSelf; ASCellNode *node = (block ? block() : cell); ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"ASCollectionNode provided a non-ASCellNode! %@, %@", node, strongSelf); if (!disableRangeController) { [node enterHierarchyState:ASHierarchyStateRangeManaged]; } if (node.interactionDelegate == nil) { node.interactionDelegate = strongSelf; } if (strongSelf.inverted) { node.transform = CATransform3DMakeScale(1, -1, 1); } return node; }; } - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section { if (_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); return [_asyncDataSource collectionNode: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) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); return [_asyncDataSource numberOfSectionsInCollectionNode: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; } } - (BOOL)dataController:(ASDataController *)dataController presentedSizeForElement:(ASCollectionElement *)element matchesSize:(CGSize)size { NSIndexPath *indexPath = [self indexPathForNode:element.node]; if (indexPath == nil) { ASDisplayNodeFailAssert(@"Data controller should not ask for presented size for element that is not presented."); return YES; } UICollectionViewLayoutAttributes *attributes; if (element.supplementaryElementKind == nil) { attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; } else { attributes = [self layoutAttributesForSupplementaryElementOfKind:element.supplementaryElementKind atIndexPath:indexPath]; } return CGSizeEqualToSizeWithIn(attributes.size, size, FLT_EPSILON); } #pragma mark - ASDataControllerSource optional methods - (ASCellNodeBlock)dataController:(ASDataController *)dataController supplementaryNodeBlockOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout { ASDisplayNodeAssertMainThread(); ASCellNodeBlock block = nil; ASCellNode *cell = nil; if (_asyncDataSourceFlags.collectionNodeNodeBlockForSupplementaryElement) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); block = [_asyncDataSource collectionNode:collectionNode nodeBlockForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } if (!block && !cell && _asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); cell = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } if (!block && !cell && _asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" cell = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; #pragma clang diagnostic pop } if (block == nil) { if (cell == nil || ASDynamicCast(cell, ASCellNode) == nil) { // In this case, the app code returned nil for the node and the nodeBlock. // If the UIKit method is implemented, then we should use a passthrough cell. // Otherwise the CGSizeZero default will cause UIKit to not show it (so this isn't an error like the cellForItem case). BOOL useUIKitCell = _asyncDataSourceFlags.interopViewForSupplementaryElement; if (useUIKitCell) { cell = [[ASWrapperCellNode alloc] init]; } else { cell = [[ASCellNode alloc] init]; } } // This condition is intended to run for either cells received from the datasource, or created just above. if (cell.shouldUseUIKitCell) { *shouldAsyncLayout = NO; } block = ^{ return cell; }; } // Wrap the node block // BOOL disableRangeController = ASCellLayoutModeIncludes(ASCellLayoutModeDisableRangeController); __weak __typeof__(self) weakSelf = self; return ^{ __typeof__(self) strongSelf = weakSelf; ASCellNode *node = block(); ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"ASCollectionNode provided a non-ASCellNode! %@, %@", node, strongSelf); // TODO: ASRangeController doesn't currently support managing interfaceState for supplementary nodes. // For now, we allow the standard ASInterfaceStateInHierarchy behavior by ensuring we do not inform // the node that it should expect external management of interfaceState. /* if (!disableRangeController) { [node enterHierarchyState:ASHierarchyStateRangeManaged]; } */ if (node.interactionDelegate == nil) { node.interactionDelegate = strongSelf; } if (strongSelf.inverted) { node.transform = CATransform3DMakeScale(1, -1, 1); } return node; }; } - (NSArray *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections { if (_asyncDataSourceFlags.collectionNodeSupplementaryElementKindsInSection) { const auto kinds = [[NSMutableSet alloc] init]; GET_COLLECTIONNODE_OR_RETURN(collectionNode, @[]); [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { NSArray *kindsForSection = [_asyncDataSource collectionNode:collectionNode supplementaryElementKindsInSection:section]; [kinds addObjectsFromArray:kindsForSection]; }]; return [kinds allObjects]; } else { // TODO: Lock this return [_registeredSupplementaryKinds allObjects]; } } - (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { return [self.layoutInspector collectionView:self constrainedSizeForNodeAtIndexPath:indexPath]; } - (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { if (_layoutInspectorFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath) { return [self.layoutInspector collectionView:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; } ASDisplayNodeAssert(NO, @"To support supplementary nodes in ASCollectionView, it must have a layoutInspector for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); return ASSizeRangeMake(CGSizeZero, CGSizeZero); } - (NSUInteger)dataController:(ASDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section { if (_asyncDataSource == nil) { return 0; } if (_layoutInspectorFlags.supplementaryNodesOfKindInSection) { return [self.layoutInspector collectionView:self supplementaryNodesOfKind:kind inSection:section]; } ASDisplayNodeAssert(NO, @"To support supplementary nodes in ASCollectionView, it must have a layoutInspector for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); return 0; } - (id)dataController:(ASDataController *)dataController contextForSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); id context = nil; if (_asyncDataSourceFlags.collectionNodeContextForSection) { GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); context = [_asyncDataSource collectionNode:collectionNode contextForSection:section]; } if (context != nil) { context.collectionView = self; } return context; } #pragma mark - ASRangeControllerDataSource - (ASRangeController *)rangeController { return _rangeController; } - (NSHashTable *)visibleElementsForRangeController:(ASRangeController *)rangeController { return ASPointerTableByFlatMapping(_visibleElements, id element, element); } - (ASElementMap *)elementMapForRangeController:(ASRangeController *)rangeController { return _dataController.visibleMap; } - (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController { return self.scrollDirection; } - (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController { return ASInterfaceStateForDisplayNode(self.collectionNode, self.window); } - (NSString *)nameForRangeControllerDataSource { return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); } #pragma mark - ASRangeControllerDelegate - (BOOL)rangeControllerShouldUpdateRanges:(ASRangeController *)rangeController { return !ASCellLayoutModeIncludes(ASCellLayoutModeDisableRangeController); } - (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates { ASDisplayNodeAssertMainThread(); if (!self.asyncDataSource || _superIsPendingDataLoad || _updatingInResponseToInteractiveMove) { updates(); [changeSet executeCompletionHandlerWithFinished:NO]; return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } //TODO Do we need to notify _layoutFacilitator before reloadData? for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; } for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; } ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ as_activity_scope(as_activity_create("Commit collection update", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); if (changeSet.includesReloadData) { _superIsPendingDataLoad = YES; updates(); [self _superReloadData:nil completion:nil]; as_log_debug(ASCollectionLog(), "Did reloadData %@", self.collectionNode); [changeSet executeCompletionHandlerWithFinished:YES]; } else { [_layoutFacilitator collectionViewWillPerformBatchUpdates]; __block NSUInteger numberOfUpdates = 0; const auto completion = ^(BOOL finished) { as_activity_scope(as_activity_create("Handle collection update completion", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); as_log_verbose(ASCollectionLog(), "Update animation finished %{public}@", self.collectionNode); // Flush any range changes that happened as part of the update animations ending. [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates]; [changeSet executeCompletionHandlerWithFinished:finished]; }; BOOL shouldReloadData = ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysReloadData); // TODO: Consider adding !changeSet.isEmpty as a check to also disable shouldReloadData. if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysBatchUpdateSectionReload) && [changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload].count > 0) { shouldReloadData = NO; } if (shouldReloadData) { // When doing a reloadData, the insert / delete calls are not necessary. // Calling updates() is enough, as it commits .pendingMap to .visibleMap. [self _superReloadData:updates completion:completion]; } else { [self _superPerformBatchUpdates:^{ updates(); for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { [super reloadItemsAtIndexPaths:change.indexPaths]; numberOfUpdates++; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { [super reloadSections:change.indexSet]; numberOfUpdates++; } for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { [super deleteItemsAtIndexPaths:change.indexPaths]; numberOfUpdates++; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { [super deleteSections:change.indexSet]; numberOfUpdates++; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { [super insertSections:change.indexSet]; numberOfUpdates++; } for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { [super insertItemsAtIndexPaths:change.indexPaths]; numberOfUpdates++; } } completion:completion]; } as_log_debug(ASCollectionLog(), "Completed batch update %{public}@", self.collectionNode); // Flush any range changes that happened as part of submitting the update. as_activity_scope(changeSet.rootActivity); [_rangeController updateIfNeeded]; } }); } #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)nodeDidInvalidateSize:(ASCellNode *)node { [_cellsForLayoutUpdates addObject:node]; [self setNeedsLayout]; } - (void)nodesDidRelayout:(NSArray *)nodes { ASDisplayNodeAssertMainThread(); if (nodes.count == 0) { return; } const auto uikitIndexPaths = ASArrayByFlatMapping(nodes, ASCellNode *node, [self indexPathForNode:node]); [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:uikitIndexPaths batched:NO]; ASCollectionViewInvalidationStyle invalidationStyle = _nextLayoutInvalidationStyle; for (ASCellNode *node in nodes) { if (invalidationStyle == ASCollectionViewInvalidationStyleNone) { // We nodesDidRelayout also while we are in layoutSubviews. This should be no problem as CA will ignore this // call while be in a layout pass [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; break; } } } _nextLayoutInvalidationStyle = invalidationStyle; } #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; BOOL rangeControllerNeedsUpdate = ![node supportsRangeManagedInterfaceState];; if (!visible && node.inHierarchy) { if (rangeControllerNeedsUpdate) { rangeControllerNeedsUpdate = NO; // Exit CellNodes first before Collection to match UIKit behaviors (tear down bottom up). // Although we have not yet cleared the interfaceState's Visible bit (this happens in __exitHierarchy), // the ASRangeController will get the correct value from -interfaceStateForRangeController:. [_rangeController updateRanges]; } [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 (rangeControllerNeedsUpdate) { [_rangeController updateRanges]; } // When we aren't visible, we will only fetch up to the visible area. Now that we are visible, // we will fetch visible area + leading screens, so we need to check. if (visible) { [self _checkForBatchFetching]; } } - (void)willMoveToSuperview:(UIView *)newSuperview { if (self.superview == nil && newSuperview != nil) { _keepalive_node = self.collectionNode; } } - (void)didMoveToSuperview { if (self.superview == nil) { _keepalive_node = nil; } } #pragma mark ASCALayerExtendedDelegate /** * TODO: This code was added when we used @c calculatedSize as the size for * items (e.g. collectionView:layout:sizeForItemAtIndexPath:) and so it * was critical that we remeasured all nodes at this time. * * The assumption was that cv-bounds-size-change -> constrained-size-change, so * this was the time when we get new constrained sizes for all items and remeasure * them. However, the constrained sizes for items can be invalidated for many other * reasons, hence why we never reuse the old constrained size anymore. * * 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 newSize = newBounds.size; CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes; if (CGSizeEqualToSize(lastUsedSize, newSize)) { return; } if (_hasDataControllerLayoutDelegate || self.collectionViewLayout == nil) { // Let the layout delegate handle bounds changes if it's available. If no layout, it will init in the new state. return; } _lastBoundsSizeUsedForMeasuringNodes = newSize; // Laying out all nodes is expensive. // 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 && newSize.width != lastUsedSize.width) || (fixedVertically && newSize.height != lastUsedSize.height); if (changedInNonScrollingDirection) { [self relayoutItems]; } } #pragma mark - UICollectionView dead-end intercepts - (void)setPrefetchDataSource:(id)prefetchDataSource { return; } - (void)setPrefetchingEnabled:(BOOL)prefetchingEnabled { return; } #pragma mark - Accessibility overrides - (NSArray *)accessibilityElements { [self waitUntilAllUpdatesAreCommitted]; return [super accessibilityElements]; } @end #endif