// // ASTableView.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 "Private/ASInternalHelpers.h" #import #import #import #import #import #import #import static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) /** * See note at the top of ASCollectionView.mm near declaration of macro GET_COLLECTIONNODE_OR_RETURN */ #define GET_TABLENODE_OR_RETURN(__var, __val) \ ASTableNode *__var = self.tableNode; \ if (__var == nil) { \ return __val; \ } #define UITABLEVIEW_RESPONDS_TO_SELECTOR() \ ({ \ static BOOL superResponds; \ static dispatch_once_t onceToken; \ dispatch_once(&onceToken, ^{ \ superResponds = [UITableView instancesRespondToSelector:_cmd]; \ }); \ superResponds; \ }) @interface UITableView (ScrollViewDelegate) - (void)scrollViewDidScroll:(UIScrollView *)scrollView; - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView; - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset; - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; @end #pragma mark - #pragma mark ASCellNode<->UITableViewCell bridging. @class _ASTableViewCell; @protocol _ASTableViewCellDelegate - (void)didLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell; @end @interface _ASTableViewCell : UITableViewCell @property (nonatomic, weak) id<_ASTableViewCellDelegate> delegate; @property (nonatomic, readonly) ASCellNode *node; @property (nonatomic) ASCollectionElement *element; @end @implementation _ASTableViewCell // TODO add assertions to prevent use of view-backed UITableViewCell properties (eg .textLabel) - (void)layoutSubviews { [super layoutSubviews]; [_delegate didLayoutSubviewsOfTableViewCell:self]; } - (void)didTransitionToState:(UITableViewCellStateMask)state { [self setNeedsLayout]; [self layoutIfNeeded]; [super didTransitionToState:state]; } - (ASCellNode *)node { return self.element.node; } - (void)setElement:(ASCollectionElement *)element { _element = element; ASCellNode *node = element.node; if (node) { self.backgroundColor = node.backgroundColor; self.selectedBackgroundView = node.selectedBackgroundView; self.backgroundView = node.backgroundView; #if TARGET_OS_IOS self.separatorInset = node.separatorInset; #endif self.selectionStyle = node.selectionStyle; self.focusStyle = node.focusStyle; self.accessoryType = node.accessoryType; self.tintColor = node.tintColor; // the following ensures that we clip the entire cell to it's bounds if node.clipsToBounds is set (the default) // This is actually a workaround for a bug we are seeing in some rare cases (selected background view // overlaps other cells if size of ASCellNode has changed.) self.clipsToBounds = node.clipsToBounds; } [node __setSelectedFromUIKit:self.selected]; [node __setHighlightedFromUIKit:self.highlighted]; } - (BOOL)consumesCellNodeVisibilityEvents { ASCellNode *node = self.node; if (node == nil) { return NO; } return ASSubclassOverridesSelector([ASCellNode class], [node class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:)); } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; [self.node __setSelectedFromUIKit:selected]; } - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { [super setHighlighted:highlighted animated:animated]; [self.node __setHighlightedFromUIKit:highlighted]; } - (void)prepareForReuse { // Need to clear element before UIKit calls setSelected:NO / setHighlighted:NO on its cells self.element = nil; [super prepareForReuse]; } @end #pragma mark - #pragma mark ASTableView @interface ASTableView () { ASTableViewProxy *_proxyDataSource; ASTableViewProxy *_proxyDelegate; ASTableLayoutController *_layoutController; ASRangeController *_rangeController; ASBatchContext *_batchContext; // When we update our data controller in response to an interactive move, // we don't want to tell the table view about the change (it knows!) BOOL _updatingInResponseToInteractiveMove; BOOL _inverted; // The top cell node that was visible before the update. __weak ASCellNode *_contentOffsetAdjustmentTopVisibleNode; // The y-offset of the top visible row's origin before the update. CGFloat _contentOffsetAdjustmentTopVisibleNodeOffset; CGFloat _leadingScreensForBatching; BOOL _automaticallyAdjustsContentOffset; CGPoint _deceleratingVelocity; CGFloat _nodesConstrainedWidth; BOOL _queuedNodeHeightUpdate; BOOL _isDeallocating; NSHashTable<_ASTableViewCell *> *_cellsForVisibilityUpdates; // CountedSet because UIKit may display the same element in multiple cells e.g. during animations. NSCountedSet *_visibleElements; NSHashTable *_cellsForLayoutUpdates; // See documentation on same property in ASCollectionView BOOL _hasEverCheckedForBatchFetchingDueToUpdate; // The section index overlay view, if there is one present. // This is useful because we need to measure our row nodes against (width - indexView.width). __weak UIView *_sectionIndexView; /** * The change set that we're currently building, if any. */ _ASHierarchyChangeSet *_changeSet; /** * Counter used to keep track of nested batch updates. */ NSInteger _batchUpdateCount; /** * Keep a strong reference to node till view is ready to release. */ ASTableNode *_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 tableNodeWillDisplayNodeForRow:1; unsigned int tableViewWillDisplayNodeForRow:1; unsigned int tableViewWillDisplayNodeForRowDeprecated:1; unsigned int tableNodeDidEndDisplayingNodeForRow:1; unsigned int tableViewDidEndDisplayingNodeForRow:1; unsigned int tableNodeWillBeginBatchFetch:1; unsigned int tableViewWillBeginBatchFetch:1; unsigned int shouldBatchFetchForTableView:1; unsigned int shouldBatchFetchForTableNode:1; unsigned int tableViewConstrainedSizeForRow:1; unsigned int tableNodeConstrainedSizeForRow:1; unsigned int tableViewWillSelectRow:1; unsigned int tableNodeWillSelectRow:1; unsigned int tableViewDidSelectRow:1; unsigned int tableNodeDidSelectRow:1; unsigned int tableViewWillDeselectRow:1; unsigned int tableNodeWillDeselectRow:1; unsigned int tableViewDidDeselectRow:1; unsigned int tableNodeDidDeselectRow:1; unsigned int tableViewShouldHighlightRow:1; unsigned int tableNodeShouldHighlightRow:1; unsigned int tableViewDidHighlightRow:1; unsigned int tableNodeDidHighlightRow:1; unsigned int tableViewDidUnhighlightRow:1; unsigned int tableNodeDidUnhighlightRow:1; unsigned int tableViewShouldShowMenuForRow:1; unsigned int tableNodeShouldShowMenuForRow:1; unsigned int tableViewCanPerformActionForRow:1; unsigned int tableNodeCanPerformActionForRow:1; unsigned int tableViewPerformActionForRow:1; unsigned int tableNodePerformActionForRow:1; } _asyncDelegateFlags; struct { unsigned int numberOfSectionsInTableView:1; unsigned int numberOfSectionsInTableNode:1; unsigned int tableNodeNumberOfRowsInSection:1; unsigned int tableViewNumberOfRowsInSection:1; unsigned int tableViewNodeBlockForRow:1; unsigned int tableNodeNodeBlockForRow:1; unsigned int tableViewNodeForRow:1; unsigned int tableNodeNodeForRow:1; unsigned int tableViewCanMoveRow:1; unsigned int tableNodeCanMoveRow:1; unsigned int tableViewMoveRow:1; unsigned int tableNodeMoveRow:1; unsigned int sectionIndexMethods:1; // if both section index methods are implemented unsigned int modelIdentifierMethods:1; // if both modelIdentifierForElementAtIndexPath and indexPathForElementWithModelIdentifier are implemented } _asyncDataSourceFlags; } @property (nonatomic) ASDataController *dataController; @property (nonatomic, weak) ASTableNode *tableNode; @property (nonatomic) BOOL test_enableSuperUpdateCallLogging; @end @implementation ASTableView { __weak id _asyncDelegate; __weak id _asyncDataSource; } // Using _ASDisplayLayer ensures things like -layout are properly forwarded to ASTableNode. + (Class)layerClass { return [_ASDisplayLayer class]; } + (Class)dataControllerClass { return [ASDataController class]; } #pragma mark - #pragma mark Lifecycle - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { return [self _initWithFrame:frame style:style dataControllerClass:nil owningNode:nil eventLog:nil]; } - (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass owningNode:(ASTableNode *)tableNode eventLog:(ASEventLog *)eventLog { if (!(self = [super initWithFrame:frame style:style])) { return nil; } _cellsForVisibilityUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; _cellsForLayoutUpdates = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; if (!dataControllerClass) { dataControllerClass = [[self class] dataControllerClass]; } _layoutController = [[ASTableLayoutController alloc] initWithTableView:self]; _rangeController = [[ASRangeController alloc] init]; _rangeController.layoutController = _layoutController; _rangeController.dataSource = self; _rangeController.delegate = self; _dataController = [[dataControllerClass alloc] initWithDataSource:self node:tableNode eventLog:eventLog]; _dataController.delegate = _rangeController; _leadingScreensForBatching = 2.0; _batchContext = [[ASBatchContext alloc] init]; _visibleElements = [[NSCountedSet alloc] init]; _automaticallyAdjustsContentOffset = NO; _nodesConstrainedWidth = self.bounds.size.width; _proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; super.delegate = (id)_proxyDelegate; _proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; super.dataSource = (id)_proxyDataSource; [self registerClass:_ASTableViewCell.class forCellReuseIdentifier:kCellReuseIdentifier]; // iOS 11 automatically uses estimated heights, so disable those (see PR #485) if (AS_AT_LEAST_IOS11) { super.estimatedRowHeight = 0.0; super.estimatedSectionHeaderHeight = 0.0; super.estimatedSectionFooterHeight = 0.0; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { NSLog(@"Warning: AsyncDisplayKit is not designed to be used with Interface Builder. Table properties set in IB will be lost."); return [self initWithFrame:CGRectZero style:UITableViewStylePlain]; } - (void)dealloc { ASDisplayNodeAssertMainThread(); ASDisplayNodeCAssert(_batchUpdateCount == 0, @"ASTableView deallocated in the middle of a batch update."); // Sometimes the UIKit classes can call back to their delegate even during deallocation. _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 - (void)setDataSource:(id)dataSource { // UIKit can internally generate a call to this method upon changing the asyncDataSource; only assert for non-nil. ASDisplayNodeAssert(dataSource == nil, @"ASTableView uses asyncDataSource, not UITableView'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. ASDisplayNodeAssert(delegate == nil, @"ASTableView uses asyncDelegate, not UITableView's delegate property."); } - (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 ASTableViewProxy will start failing and cause crashes. NS_VALID_UNTIL_END_OF_SCOPE id oldDataSource = self.dataSource; if (asyncDataSource == nil) { _asyncDataSource = nil; _proxyDataSource = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; memset(&_asyncDataSourceFlags, 0, sizeof(_asyncDataSourceFlags)); } else { _asyncDataSource = asyncDataSource; _proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; _asyncDataSourceFlags.numberOfSectionsInTableView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]; _asyncDataSourceFlags.numberOfSectionsInTableNode = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableNode:)]; _asyncDataSourceFlags.tableViewNumberOfRowsInSection = [_asyncDataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]; _asyncDataSourceFlags.tableNodeNumberOfRowsInSection = [_asyncDataSource respondsToSelector:@selector(tableNode:numberOfRowsInSection:)]; _asyncDataSourceFlags.tableViewNodeForRow = [_asyncDataSource respondsToSelector:@selector(tableView:nodeForRowAtIndexPath:)]; _asyncDataSourceFlags.tableNodeNodeForRow = [_asyncDataSource respondsToSelector:@selector(tableNode:nodeForRowAtIndexPath:)]; _asyncDataSourceFlags.tableViewNodeBlockForRow = [_asyncDataSource respondsToSelector:@selector(tableView:nodeBlockForRowAtIndexPath:)]; _asyncDataSourceFlags.tableNodeNodeBlockForRow = [_asyncDataSource respondsToSelector:@selector(tableNode:nodeBlockForRowAtIndexPath:)]; _asyncDataSourceFlags.tableViewCanMoveRow = [_asyncDataSource respondsToSelector:@selector(tableView:canMoveRowAtIndexPath:)]; _asyncDataSourceFlags.tableViewMoveRow = [_asyncDataSource respondsToSelector:@selector(tableView:moveRowAtIndexPath:toIndexPath:)]; _asyncDataSourceFlags.sectionIndexMethods = [_asyncDataSource respondsToSelector:@selector(sectionIndexTitlesForTableView:)] && [_asyncDataSource respondsToSelector:@selector(tableView:sectionForSectionIndexTitle:atIndex:)]; _asyncDataSourceFlags.modelIdentifierMethods = [_asyncDataSource respondsToSelector:@selector(modelIdentifierForElementAtIndexPath:inNode:)] && [_asyncDataSource respondsToSelector:@selector(indexPathForElementWithModelIdentifier:inNode:)]; ASDisplayNodeAssert(_asyncDataSourceFlags.tableViewNodeBlockForRow || _asyncDataSourceFlags.tableViewNodeForRow || _asyncDataSourceFlags.tableNodeNodeBlockForRow || _asyncDataSourceFlags.tableNodeNodeForRow, @"Data source must implement tableNode:nodeBlockForRowAtIndexPath: or tableNode:nodeForRowAtIndexPath:"); ASDisplayNodeAssert(_asyncDataSourceFlags.tableNodeNumberOfRowsInSection || _asyncDataSourceFlags.tableViewNumberOfRowsInSection, @"Data source must implement tableNode:numberOfRowsInSection:"); } _dataController.validationErrorSource = asyncDataSource; super.dataSource = (id)_proxyDataSource; [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 ASTableViewProxy will start failing and cause crashes. NS_VALID_UNTIL_END_OF_SCOPE id oldDelegate = super.delegate; if (asyncDelegate == nil) { _asyncDelegate = nil; _proxyDelegate = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; memset(&_asyncDelegateFlags, 0, sizeof(_asyncDelegateFlags)); } else { _asyncDelegate = asyncDelegate; _proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; _asyncDelegateFlags.scrollViewDidScroll = [_asyncDelegate respondsToSelector:@selector(scrollViewDidScroll:)]; _asyncDelegateFlags.tableViewWillDisplayNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNode:forRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeWillDisplayNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:willDisplayRowWithNode:)]; if (_asyncDelegateFlags.tableViewWillDisplayNodeForRow == NO) { _asyncDelegateFlags.tableViewWillDisplayNodeForRowDeprecated = [_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNodeForRowAtIndexPath:)]; } _asyncDelegateFlags.tableViewDidEndDisplayingNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeDidEndDisplayingNodeForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didEndDisplayingRowWithNode:)]; _asyncDelegateFlags.scrollViewWillEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]; _asyncDelegateFlags.scrollViewDidEndDecelerating = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]; _asyncDelegateFlags.tableViewWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)]; _asyncDelegateFlags.tableNodeWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(tableNode:willBeginBatchFetchWithContext:)]; _asyncDelegateFlags.shouldBatchFetchForTableView = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]; _asyncDelegateFlags.shouldBatchFetchForTableNode = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableNode:)]; _asyncDelegateFlags.scrollViewWillBeginDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]; _asyncDelegateFlags.scrollViewDidEndDragging = [_asyncDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]; _asyncDelegateFlags.tableViewConstrainedSizeForRow = [_asyncDelegate respondsToSelector:@selector(tableView:constrainedSizeForRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeConstrainedSizeForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:constrainedSizeForRowAtIndexPath:)]; _asyncDelegateFlags.tableViewWillSelectRow = [_asyncDelegate respondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeWillSelectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:willSelectRowAtIndexPath:)]; _asyncDelegateFlags.tableViewDidSelectRow = [_asyncDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeDidSelectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didSelectRowAtIndexPath:)]; _asyncDelegateFlags.tableViewWillDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeWillDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:willDeselectRowAtIndexPath:)]; _asyncDelegateFlags.tableViewDidDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeDidDeselectRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didDeselectRowAtIndexPath:)]; _asyncDelegateFlags.tableViewShouldHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableView:shouldHighlightRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeShouldHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableNode:shouldHighlightRowAtIndexPath:)]; _asyncDelegateFlags.tableViewDidHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableView:didHighlightRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeDidHighlightRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didHighlightRowAtIndexPath:)]; _asyncDelegateFlags.tableViewDidUnhighlightRow = [_asyncDelegate respondsToSelector:@selector(tableView:didUnhighlightRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeDidUnhighlightRow = [_asyncDelegate respondsToSelector:@selector(tableNode:didUnhighlightRowAtIndexPath:)]; _asyncDelegateFlags.tableViewShouldShowMenuForRow = [_asyncDelegate respondsToSelector:@selector(tableView:shouldShowMenuForRowAtIndexPath:)]; _asyncDelegateFlags.tableNodeShouldShowMenuForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:shouldShowMenuForRowAtIndexPath:)]; _asyncDelegateFlags.tableViewCanPerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableView:canPerformAction:forRowAtIndexPath:withSender:)]; _asyncDelegateFlags.tableNodeCanPerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:canPerformAction:forRowAtIndexPath:withSender:)]; _asyncDelegateFlags.tableViewPerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableView:performAction:forRowAtIndexPath:withSender:)]; _asyncDelegateFlags.tableNodePerformActionForRow = [_asyncDelegate respondsToSelector:@selector(tableNode:performAction:forRowAtIndexPath:withSender:)]; } super.delegate = (id)_proxyDelegate; [self _asyncDelegateOrDataSourceDidChange]; } - (void)_asyncDelegateOrDataSourceDidChange { ASDisplayNodeAssertMainThread(); if (_asyncDataSource == nil && _asyncDelegate == nil && !ASActivateExperimentalFeature(ASExperimentalSkipClearData)) { [_dataController clearData]; } } - (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy { if (proxy == _proxyDelegate) { [self setAsyncDelegate:nil]; } else if (proxy == _proxyDataSource) { [self setAsyncDataSource:nil]; } } - (void)reloadDataWithCompletion:(void (^)())completion { ASDisplayNodeAssertMainThread(); if (! _dataController.initialReloadDataHasBeenCalled) { // If this is the first reload, forward to super immediately to prevent it from triggering more "initial" loads while our data controller is working. [super reloadData]; } void (^batchUpdatesCompletion)(BOOL); if (completion) { batchUpdatesCompletion = ^(BOOL) { completion(); }; } [self beginUpdates]; [_changeSet reloadData]; [self endUpdatesWithCompletion:batchUpdatesCompletion]; } - (void)reloadData { [self reloadDataWithCompletion:nil]; } - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated { if ([self validateIndexPath:indexPath]) { [super scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; } } - (void)relayoutItems { [_dataController relayoutAllNodesWithInvalidationBlock:nil]; } - (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]; } - (ASElementMap *)elementMapForRangeController:(ASRangeController *)rangeController { return _dataController.visibleMap; } - (ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath { return [_dataController.visibleMap elementForItemAtIndexPath:indexPath].node; } - (NSIndexPath *)convertIndexPathFromTableNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait { NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap]; if (viewIndexPath == nil && wait) { [self waitUntilAllUpdatesAreCommitted]; return [self convertIndexPathFromTableNode:indexPath waitingIfNeeded:NO]; } return viewIndexPath; } - (NSIndexPath *)convertIndexPathToTableNode:(NSIndexPath *)indexPath { if ([self validateIndexPath:indexPath] == nil) { return nil; } return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap]; } - (NSArray *)convertIndexPathsToTableNode:(NSArray *)indexPaths { if (indexPaths == nil) { return nil; } NSMutableArray *indexPathsArray = [NSMutableArray new]; for (NSIndexPath *indexPathInView in indexPaths) { NSIndexPath *indexPath = [self convertIndexPathToTableNode:indexPathInView]; if (indexPath != nil) { [indexPathsArray addObject:indexPath]; } } return indexPathsArray; } - (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode { return [self indexPathForNode:cellNode waitingIfNeeded:NO]; } /** * 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(@"Table 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 numberOfRowsInSection:section]) { ASDisplayNodeFailAssert(@"Table view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfRowsInSection:section]); return nil; } return indexPath; } - (nullable NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode waitingIfNeeded:(BOOL)wait { if (cellNode == nil) { return nil; } NSIndexPath *indexPath = [_dataController.visibleMap indexPathForElement:cellNode.collectionElement]; indexPath = [self validateIndexPath:indexPath]; if (indexPath == nil && wait) { [self waitUntilAllUpdatesAreCommitted]; return [self indexPathForNode:cellNode waitingIfNeeded:NO]; } return indexPath; } - (NSArray *)visibleNodes { const auto elements = [self visibleElementsForRangeController:_rangeController]; return ASArrayByFlatMapping(elements, ASCollectionElement *e, e.node); } - (void)beginUpdates { ASDisplayNodeAssertMainThread(); // _changeSet must be available during batch update ASDisplayNodeAssertTrue((_batchUpdateCount > 0) == (_changeSet != nil)); if (_batchUpdateCount == 0) { _changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[_dataController itemCountsFromDataSource]]; } _batchUpdateCount++; } - (void)endUpdates { [self endUpdatesWithCompletion:nil]; } - (void)endUpdatesWithCompletion:(void (^)(BOOL completed))completion { // We capture the current state of whether animations are enabled if they don't provide us with one. [self endUpdatesAnimated:[UIView areAnimationsEnabled] completion:completion]; } - (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))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]; } } - (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]; } - (void)layoutSubviews { // Remeasure all rows if our row width has changed. UIEdgeInsets contentInset = self.contentInset; CGFloat constrainedWidth = self.bounds.size.width - [self sectionIndexWidth] - contentInset.left - contentInset.right; if (constrainedWidth > 0 && _nodesConstrainedWidth != constrainedWidth) { _nodesConstrainedWidth = constrainedWidth; [_cellsForLayoutUpdates removeAllObjects]; [self beginUpdates]; [_dataController relayoutAllNodesWithInvalidationBlock:nil]; [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; } else { if (_cellsForLayoutUpdates.count > 0) { NSArray *nodes = [_cellsForLayoutUpdates allObjects]; [_cellsForLayoutUpdates removeAllObjects]; const auto nodesSizeChanged = [[NSMutableArray alloc] init]; [_dataController relayoutNodes:nodes nodesSizeChanged:nodesSizeChanged]; if (nodesSizeChanged.count > 0) { [self requeryNodeHeights]; } } } // To ensure _nodesConstrainedWidth is up-to-date for every usage, this call to super must be done last [super layoutSubviews]; [_rangeController updateIfNeeded]; } #pragma mark - #pragma mark Editing - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); if (sections.count == 0) { return; } [self beginUpdates]; [_changeSet insertSections:sections animationOptions:animation]; [self endUpdates]; } - (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); if (sections.count == 0) { return; } [self beginUpdates]; [_changeSet deleteSections:sections animationOptions:animation]; [self endUpdates]; } - (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); if (sections.count == 0) { return; } [self beginUpdates]; [_changeSet reloadSections:sections animationOptions:animation]; [self endUpdates]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { ASDisplayNodeAssertMainThread(); [self beginUpdates]; [_changeSet moveSection:section toSection:newSection animationOptions:UITableViewRowAnimationNone]; [self endUpdates]; } - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0) { return; } [self beginUpdates]; [_changeSet insertItems:indexPaths animationOptions:animation]; [self endUpdates]; } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0) { return; } [self beginUpdates]; [_changeSet deleteItems:indexPaths animationOptions:animation]; [self endUpdates]; } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0) { return; } [self beginUpdates]; [_changeSet reloadItems:indexPaths animationOptions:animation]; [self endUpdates]; } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { ASDisplayNodeAssertMainThread(); [self beginUpdates]; [_changeSet moveItemAtIndexPath:indexPath toIndexPath:newIndexPath animationOptions:UITableViewRowAnimationNone]; [self endUpdates]; } #pragma mark - #pragma mark adjust content offset - (void)beginAdjustingContentOffset { NSIndexPath *firstVisibleIndexPath = [self.indexPathsForVisibleRows sortedArrayUsingSelector:@selector(compare:)].firstObject; if (firstVisibleIndexPath) { ASCellNode *node = [self nodeForRowAtIndexPath:firstVisibleIndexPath]; if (node) { _contentOffsetAdjustmentTopVisibleNode = node; _contentOffsetAdjustmentTopVisibleNodeOffset = [self rectForRowAtIndexPath:firstVisibleIndexPath].origin.y - self.bounds.origin.y; } } } - (void)endAdjustingContentOffsetAnimated:(BOOL)animated { // We can't do this for animated updates. if (animated) { return; } // We can't do this if we didn't have a top visible row before. if (_contentOffsetAdjustmentTopVisibleNode == nil) { return; } NSIndexPath *newIndexPathForTopVisibleRow = [self indexPathForNode:_contentOffsetAdjustmentTopVisibleNode]; // We can't do this if our top visible row was deleted if (newIndexPathForTopVisibleRow == nil) { return; } CGFloat newRowOriginYInSelf = [self rectForRowAtIndexPath:newIndexPathForTopVisibleRow].origin.y - self.bounds.origin.y; CGPoint newContentOffset = self.contentOffset; newContentOffset.y += (newRowOriginYInSelf - _contentOffsetAdjustmentTopVisibleNodeOffset); self.contentOffset = newContentOffset; _contentOffsetAdjustmentTopVisibleNode = nil; } #pragma mark - Intercepted selectors - (void)setTableHeaderView:(UIView *)tableHeaderView { // Typically the view will be nil before setting it, but reset state if it is being re-hosted. [self.tableHeaderView.asyncdisplaykit_node exitHierarchyState:ASHierarchyStateRangeManaged]; [super setTableHeaderView:tableHeaderView]; [self.tableHeaderView.asyncdisplaykit_node enterHierarchyState:ASHierarchyStateRangeManaged]; } - (void)setTableFooterView:(UIView *)tableFooterView { // Typically the view will be nil before setting it, but reset state if it is being re-hosted. [self.tableFooterView.asyncdisplaykit_node exitHierarchyState:ASHierarchyStateRangeManaged]; [super setTableFooterView:tableFooterView]; [self.tableFooterView.asyncdisplaykit_node enterHierarchyState:ASHierarchyStateRangeManaged]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { _ASTableViewCell *cell = [self dequeueReusableCellWithIdentifier:kCellReuseIdentifier forIndexPath:indexPath]; cell.delegate = self; ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; cell.element = element; ASCellNode *node = element.node; if (node) { [_rangeController configureContentView:cell.contentView forCellNode:node]; } return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { CGFloat height = 0.0; ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; if (element != nil) { ASCellNode *node = element.node; ASDisplayNodeAssertNotNil(node, @"Node must not be nil!"); height = [node layoutThatFits:element.constrainedSize].size.height; } #if TARGET_OS_IOS /** * Weirdly enough, Apple expects the return value here to _include_ the height * of the separator, if there is one! So if our node wants to be 43.5, we need * to return 44. UITableView will make a cell of height 44 with a content view * of height 43.5. */ if (tableView.separatorStyle != UITableViewCellSeparatorStyleNone) { height += 1.0 / ASScreenScale(); } #endif return height; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return _dataController.visibleMap.numberOfSections; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [_dataController.visibleMap numberOfItemsInSection:section]; } - (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { if (_asyncDataSourceFlags.modelIdentifierMethods) { GET_TABLENODE_OR_RETURN(tableNode, nil); NSIndexPath *convertedPath = [self convertIndexPathToTableNode:indexPath]; if (convertedPath == nil) { return nil; } else { return [_asyncDataSource modelIdentifierForElementAtIndexPath:convertedPath inNode:tableNode]; } } else { return nil; } } - (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { if (_asyncDataSourceFlags.modelIdentifierMethods) { GET_TABLENODE_OR_RETURN(tableNode, nil); return [_asyncDataSource indexPathForElementWithModelIdentifier:identifier inNode:tableNode]; } else { return nil; } } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDataSourceFlags.tableViewCanMoveRow) { return [_asyncDataSource tableView:self canMoveRowAtIndexPath:indexPath]; } else { return NO; } } - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { if (_asyncDataSourceFlags.tableViewMoveRow) { [_asyncDataSource tableView:self moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; } // Move node after informing data source in case they call nodeAtIndexPath: // Get up to date [self waitUntilAllUpdatesAreCommitted]; // Set our flag to suppress informing super about the change. _updatingInResponseToInteractiveMove = YES; // Submit the move [self moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; // Wait for it to finish – should be fast! [self waitUntilAllUpdatesAreCommitted]; // Clear the flag _updatingInResponseToInteractiveMove = NO; } - (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { ASCollectionElement *element = cell.element; if (element) { ASDisplayNodeAssertTrue([_dataController.visibleMap elementForItemAtIndexPath:indexPath] == element); [_visibleElements addObject:element]; } else { ASDisplayNodeAssert(NO, @"Unexpected nil element for willDisplayCell: %@, %@, %@", cell, self, indexPath); return; } ASCellNode *cellNode = element.node; cellNode.scrollView = tableView; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); if (_asyncDelegateFlags.tableNodeWillDisplayNodeForRow) { GET_TABLENODE_OR_RETURN(tableNode, (void)0); [_asyncDelegate tableNode:tableNode willDisplayRowWithNode:cellNode]; } else if (_asyncDelegateFlags.tableViewWillDisplayNodeForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self willDisplayNode:cellNode forRowAtIndexPath:indexPath]; } else if (_asyncDelegateFlags.tableViewWillDisplayNodeForRowDeprecated) { [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; } #pragma clang diagnostic pop [_rangeController setNeedsUpdate]; if ([cell consumesCellNodeVisibilityEvents]) { [_cellsForVisibilityUpdates addObject:cell]; } } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { // 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: %@, %@, %@", cell, self, indexPath); return; } ASCellNode *cellNode = element.node; [_rangeController setNeedsUpdate]; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); if (_asyncDelegateFlags.tableNodeDidEndDisplayingNodeForRow) { if (ASTableNode *tableNode = self.tableNode) { [_asyncDelegate tableNode:tableNode didEndDisplayingRowWithNode:cellNode]; } } else if (_asyncDelegateFlags.tableViewDidEndDisplayingNodeForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } [_cellsForVisibilityUpdates removeObject:cell]; cellNode.scrollView = nil; } - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeWillSelectRow) { GET_TABLENODE_OR_RETURN(tableNode, indexPath); NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; // If this item was is gone, just let the table view do its default behavior and select. if (result == nil) { return indexPath; } else { result = [_asyncDelegate tableNode:tableNode willSelectRowAtIndexPath:result]; result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; return result; } } else if (_asyncDelegateFlags.tableViewWillSelectRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDelegate tableView:self willSelectRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } else { return indexPath; } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidSelectRow) { GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { [_asyncDelegate tableNode:tableNode didSelectRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidSelectRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self didSelectRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } } - (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeWillDeselectRow) { GET_TABLENODE_OR_RETURN(tableNode, indexPath); NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; // If this item was is gone, just let the table view do its default behavior and deselect. if (result == nil) { return indexPath; } else { result = [_asyncDelegate tableNode:tableNode willDeselectRowAtIndexPath:result]; result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; return result; } } else if (_asyncDelegateFlags.tableViewWillDeselectRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDelegate tableView:self willDeselectRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } return indexPath; } - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidDeselectRow) { GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { [_asyncDelegate tableNode:tableNode didDeselectRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidDeselectRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self didDeselectRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } } - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeShouldHighlightRow) { GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate tableNode:tableNode shouldHighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewShouldHighlightRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDelegate tableView:self shouldHighlightRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } return YES; } - (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidHighlightRow) { GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate tableNode:tableNode didHighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidHighlightRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self didHighlightRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } } - (void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidHighlightRow) { GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate tableNode:tableNode didUnhighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidUnhighlightRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self didUnhighlightRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } } - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeShouldShowMenuForRow) { GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate tableNode:tableNode shouldShowMenuForRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewShouldShowMenuForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDelegate tableView:self shouldShowMenuForRowAtIndexPath:indexPath]; #pragma clang diagnostic pop } return NO; } - (BOOL)tableView:(UITableView *)tableView canPerformAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.tableNodeCanPerformActionForRow) { GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { return [_asyncDelegate tableNode:tableNode canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; } } else if (_asyncDelegateFlags.tableViewCanPerformActionForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDelegate tableView:self canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; #pragma clang diagnostic pop } return NO; } - (void)tableView:(UITableView *)tableView performAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.tableNodePerformActionForRow) { GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { [_asyncDelegate tableNode:tableNode performAction:action forRowAtIndexPath:indexPath withSender:sender]; } } else if (_asyncDelegateFlags.tableViewPerformActionForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self performAction:action forRowAtIndexPath:indexPath withSender:sender]; #pragma clang diagnostic pop } } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { [super scrollViewDidScroll:scrollView]; return; } ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; if (ASInterfaceStateIncludesVisible(interfaceState)) { [self _checkForBatchFetching]; } for (_ASTableViewCell *tableCell in _cellsForVisibilityUpdates) { [[tableCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView withCellFrame:tableCell.frame]; } if (_asyncDelegateFlags.scrollViewDidScroll) { [_asyncDelegate scrollViewDidScroll:scrollView]; } } - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { [super scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; return; } 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 { if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { [super scrollViewDidEndDecelerating:scrollView]; return; } _deceleratingVelocity = CGPointZero; if (_asyncDelegateFlags.scrollViewDidEndDecelerating) { [_asyncDelegate scrollViewDidEndDecelerating:scrollView]; } } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { [super scrollViewWillBeginDragging:scrollView]; return; } // If a scroll happens the current range mode needs to go to full _rangeController.contentHasBeenScrolled = YES; [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; for (_ASTableViewCell *tableViewCell in _cellsForVisibilityUpdates) { [[tableViewCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventWillBeginDragging inScrollView:scrollView withCellFrame:tableViewCell.frame]; } if (_asyncDelegateFlags.scrollViewWillBeginDragging) { [_asyncDelegate scrollViewWillBeginDragging:scrollView]; } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (scrollView != self && UITABLEVIEW_RESPONDS_TO_SELECTOR()) { [super scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; return; } for (_ASTableViewCell *tableViewCell in _cellsForVisibilityUpdates) { [[tableViewCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventDidEndDragging inScrollView:scrollView withCellFrame:tableViewCell.frame]; } if (_asyncDelegateFlags.scrollViewDidEndDragging) { [_asyncDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; } } #pragma mark - Misc - (BOOL)inverted { return _inverted; } - (void)setInverted:(BOOL)inverted { _inverted = inverted; } - (CGFloat)leadingScreensForBatching { return _leadingScreensForBatching; } - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { if (_leadingScreensForBatching != leadingScreensForBatching) { _leadingScreensForBatching = leadingScreensForBatching; ASPerformBlockOnMainThread(^{ [self _checkForBatchFetching]; }); } } - (BOOL)automaticallyAdjustsContentOffset { return _automaticallyAdjustsContentOffset; } - (void)setAutomaticallyAdjustsContentOffset:(BOOL)automaticallyAdjustsContentOffset { _automaticallyAdjustsContentOffset = automaticallyAdjustsContentOffset; } #pragma mark - Scroll Direction - (ASScrollDirection)scrollDirection { CGPoint scrollVelocity; if (self.isTracking) { scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; } else { scrollVelocity = _deceleratingVelocity; } ASScrollDirection scrollDirection = [self _scrollDirectionForVelocity:scrollVelocity]; return ASScrollDirectionApplyTransform(scrollDirection, self.transform); } - (ASScrollDirection)_scrollDirectionForVelocity:(CGPoint)scrollVelocity { ASScrollDirection direction = ASScrollDirectionNone; ASScrollDirection scrollableDirections = [self scrollableDirections]; if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) { // Can scroll vertically. if (scrollVelocity.y < 0.0) { direction |= ASScrollDirectionDown; } else if (scrollVelocity.y > 0.0) { direction |= ASScrollDirectionUp; } } return direction; } - (ASScrollDirection)scrollableDirections { ASScrollDirection scrollableDirection = ASScrollDirectionNone; CGFloat totalContentWidth = self.contentSize.width + self.contentInset.left + self.contentInset.right; CGFloat totalContentHeight = self.contentSize.height + self.contentInset.top + self.contentInset.bottom; if (self.alwaysBounceHorizontal || totalContentWidth > self.bounds.size.width) { // Can scroll horizontally. scrollableDirection |= ASScrollDirectionHorizontalDirections; } if (self.alwaysBounceVertical || totalContentHeight > self.bounds.size.height) { // Can scroll vertically. scrollableDirection |= ASScrollDirectionVerticalDirections; } return scrollableDirection; } #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.tableNodeWillBeginBatchFetch || _asyncDelegateFlags.tableViewWillBeginBatchFetch; if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableNode) { GET_TABLENODE_OR_RETURN(tableNode, NO); return [_asyncDelegate shouldBatchFetchForTableNode:tableNode]; } else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDelegate shouldBatchFetchForTableView:self]; #pragma clang diagnostic pop } else { return canFetch; } } - (id)batchFetchingDelegate { return self.tableNode.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, ASScrollDirectionVerticalDirections, contentOffset, velocity)) { [self _beginBatchFetching]; } } - (void)_beginBatchFetching { [_batchContext beginBatchFetching]; if (_asyncDelegateFlags.tableNodeWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ GET_TABLENODE_OR_RETURN(tableNode, (void)0); [_asyncDelegate tableNode:tableNode willBeginBatchFetchWithContext:_batchContext]; }); } else if (_asyncDelegateFlags.tableViewWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext]; #pragma clang diagnostic pop }); } } #pragma mark - ASRangeControllerDataSource - (ASRangeController *)rangeController { return _rangeController; } - (NSHashTable *)visibleElementsForRangeController:(ASRangeController *)rangeController { return ASPointerTableByFlatMapping(_visibleElements, id element, element); } - (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController { return self.scrollDirection; } - (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController { return ASInterfaceStateForDisplayNode(self.tableNode, self.window); } - (NSString *)nameForRangeControllerDataSource { return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); } #pragma mark - ASRangeControllerDelegate - (BOOL)rangeControllerShouldUpdateRanges:(ASRangeController *)rangeController { return YES; } - (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates { ASDisplayNodeAssertMainThread(); if (!self.asyncDataSource || _updatingInResponseToInteractiveMove) { updates(); [changeSet executeCompletionHandlerWithFinished:NO]; return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } if (changeSet.includesReloadData) { LOG(@"UITableView reloadData"); ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ if (self.test_enableSuperUpdateCallLogging) { NSLog(@"-[super reloadData]"); } updates(); [super reloadData]; // Flush any range changes that happened as part of submitting the reload. [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:1]; [changeSet executeCompletionHandlerWithFinished:YES]; }); return; } BOOL shouldAdjustContentOffset = (_automaticallyAdjustsContentOffset && !changeSet.includesReloadData); if (shouldAdjustContentOffset) { [self beginAdjustingContentOffset]; } NSUInteger numberOfUpdates = 0; LOG(@"--- UITableView beginUpdates"); [super beginUpdates]; updates(); for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { NSArray *indexPaths = change.indexPaths; UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; LOG(@"UITableView reloadRows:%ld rows", indexPaths.count); BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ if (self.test_enableSuperUpdateCallLogging) { NSLog(@"-[super reloadRowsAtIndexPaths]: %@", indexPaths); } [super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animationOptions]; }); numberOfUpdates++; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { NSIndexSet *sectionIndexes = change.indexSet; UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; LOG(@"UITableView reloadSections:%@", sectionIndexes); BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); ASPerformBlockWithoutAnimation(preventAnimation, ^{ if (self.test_enableSuperUpdateCallLogging) { NSLog(@"-[super reloadSections]: %@", sectionIndexes); } [super reloadSections:sectionIndexes withRowAnimation:animationOptions]; }); numberOfUpdates++; } for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { NSArray *indexPaths = change.indexPaths; UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; LOG(@"UITableView deleteRows:%ld rows", indexPaths.count); BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ if (self.test_enableSuperUpdateCallLogging) { NSLog(@"-[super deleteRowsAtIndexPaths]: %@", indexPaths); } [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animationOptions]; }); numberOfUpdates++; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalDelete]) { NSIndexSet *sectionIndexes = change.indexSet; UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; LOG(@"UITableView deleteSections:%@", sectionIndexes); BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); ASPerformBlockWithoutAnimation(preventAnimation, ^{ if (self.test_enableSuperUpdateCallLogging) { NSLog(@"-[super deleteSections]: %@", sectionIndexes); } [super deleteSections:sectionIndexes withRowAnimation:animationOptions]; }); numberOfUpdates++; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { NSIndexSet *sectionIndexes = change.indexSet; UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; LOG(@"UITableView insertSections:%@", sectionIndexes); BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); ASPerformBlockWithoutAnimation(preventAnimation, ^{ if (self.test_enableSuperUpdateCallLogging) { NSLog(@"-[super insertSections]: %@", sectionIndexes); } [super insertSections:sectionIndexes withRowAnimation:animationOptions]; }); numberOfUpdates++; } for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeOriginalInsert]) { NSArray *indexPaths = change.indexPaths; UITableViewRowAnimation animationOptions = (UITableViewRowAnimation)change.animationOptions; LOG(@"UITableView insertRows:%ld rows", indexPaths.count); BOOL preventAnimation = (animationOptions == UITableViewRowAnimationNone); ASPerformBlockWithoutAnimation(preventAnimation, ^{ if (self.test_enableSuperUpdateCallLogging) { NSLog(@"-[super insertRowsAtIndexPaths]: %@", indexPaths); } [super insertRowsAtIndexPaths:indexPaths withRowAnimation:animationOptions]; }); numberOfUpdates++; } LOG(@"--- UITableView endUpdates"); ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ [super endUpdates]; [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates]; }); if (shouldAdjustContentOffset) { [self endAdjustingContentOffsetAnimated:changeSet.animated]; } [changeSet executeCompletionHandlerWithFinished:YES]; } #pragma mark - ASDataControllerSource - (BOOL)dataController:(ASDataController *)dataController shouldEagerlyLayoutNode:(ASCellNode *)node { return YES; } - (BOOL)dataControllerShouldSerializeNodeCreation:(ASDataController *)dataController { return NO; } - (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyProcessChangeSet:(_ASHierarchyChangeSet *)changeSet { // Reload data is expensive, don't block main while doing so. if (changeSet.includesReloadData) { return NO; } // For more details on this method, see the comment in the ASCollectionView implementation. 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; } - (void)dataControllerDidFinishWaiting:(ASDataController *)dataController { // ASCellLayoutMode is not currently supported on ASTableView (see ASCollectionView for details). } - (id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath { // Not currently supported for tables. Will be added when the collection API stabilizes. return nil; } - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath shouldAsyncLayout:(BOOL *)shouldAsyncLayout { ASCellNodeBlock block = nil; if (_asyncDataSourceFlags.tableNodeNodeBlockForRow) { if (ASTableNode *tableNode = self.tableNode) { block = [_asyncDataSource tableNode:tableNode nodeBlockForRowAtIndexPath:indexPath]; } } else if (_asyncDataSourceFlags.tableNodeNodeForRow) { ASCellNode *node = nil; if (ASTableNode *tableNode = self.tableNode) { node = [_asyncDataSource tableNode:tableNode nodeForRowAtIndexPath:indexPath]; } if ([node isKindOfClass:[ASCellNode class]]) { block = ^{ return node; }; } else { ASDisplayNodeFailAssert(@"Data source returned invalid node from tableNode:nodeForRowAtIndexPath:. Node: %@", node); } } else if (_asyncDataSourceFlags.tableViewNodeBlockForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" block = [_asyncDataSource tableView:self nodeBlockForRowAtIndexPath:indexPath]; } else if (_asyncDataSourceFlags.tableViewNodeForRow) { ASCellNode *node = [_asyncDataSource tableView:self nodeForRowAtIndexPath:indexPath]; #pragma clang diagnostic pop if ([node isKindOfClass:[ASCellNode class]]) { block = ^{ return node; }; } else { ASDisplayNodeFailAssert(@"Data source returned invalid node from tableView:nodeForRowAtIndexPath:. Node: %@", node); } } // Handle nil node block if (block == nil) { ASDisplayNodeFailAssert(@"ASTableNode could not get a node block for row at index path %@", indexPath); block = ^{ return [[ASCellNode alloc] init]; }; } // Wrap the node block __weak __typeof__(self) weakSelf = self; return ^{ __typeof__(self) strongSelf = weakSelf; ASCellNode *node = (block != nil ? block() : [[ASCellNode alloc] init]); ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"ASTableNode provided a non-ASCellNode! %@, %@", node, strongSelf); [node enterHierarchyState:ASHierarchyStateRangeManaged]; if (node.interactionDelegate == nil) { node.interactionDelegate = strongSelf; } if (_inverted) { node.transform = CATransform3DMakeScale(1, -1, 1) ; } return node; }; return block; } - (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { ASSizeRange constrainedSize = ASSizeRangeZero; if (_asyncDelegateFlags.tableNodeConstrainedSizeForRow) { GET_TABLENODE_OR_RETURN(tableNode, constrainedSize); ASSizeRange delegateConstrainedSize = [_asyncDelegate tableNode:tableNode constrainedSizeForRowAtIndexPath:indexPath]; // ignore widths in the returned size range (for TableView) constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.min.height), CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.max.height)); } else if (_asyncDelegateFlags.tableViewConstrainedSizeForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" ASSizeRange delegateConstrainedSize = [_asyncDelegate tableView:self constrainedSizeForRowAtIndexPath:indexPath]; #pragma clang diagnostic pop // ignore widths in the returned size range (for TableView) constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.min.height), CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.max.height)); } else { constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, 0), CGSizeMake(_nodesConstrainedWidth, CGFLOAT_MAX)); } return constrainedSize; } - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section { if (_asyncDataSourceFlags.tableNodeNumberOfRowsInSection) { GET_TABLENODE_OR_RETURN(tableNode, 0); return [_asyncDataSource tableNode:tableNode numberOfRowsInSection:section]; } else if (_asyncDataSourceFlags.tableViewNumberOfRowsInSection) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDataSource tableView:self numberOfRowsInSection:section]; #pragma clang diagnostic pop } else { return 0; } } - (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if (_asyncDataSourceFlags.numberOfSectionsInTableNode) { GET_TABLENODE_OR_RETURN(tableNode, 0); return [_asyncDataSource numberOfSectionsInTableNode:tableNode]; } else if (_asyncDataSourceFlags.numberOfSectionsInTableView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [_asyncDataSource numberOfSectionsInTableView:self]; #pragma clang diagnostic pop } else { return 1; // default section number } } - (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; } CGRect rect = [self rectForRowAtIndexPath:indexPath]; #if TARGET_OS_IOS /** * Weirdly enough, Apple expects the return value in tableView:heightForRowAtIndexPath: to _include_ the height * of the separator, if there is one! So if rectForRow would return 44.0 we need to use 43.5. */ if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { rect.size.height -= 1.0 / ASScreenScale(); } #endif return (fabs(rect.size.height - size.height) < FLT_EPSILON); } #pragma mark - _ASTableViewCellDelegate - (void)didLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell { ASCellNode *node = tableViewCell.node; if (node == nil || _asyncDataSource == nil) { return; } CGFloat contentViewWidth = tableViewCell.contentView.bounds.size.width; ASSizeRange constrainedSize = node.constrainedSizeForCalculatedLayout; // Table view cells should always fill its content view width. // Normally the content view width equals to the constrained size width (which equals to the table view width). // If there is a mismatch between these values, for example after the table view entered or left editing mode, // content view width is preferred and used to re-measure the cell node. if (CGSizeEqualToSize(node.calculatedSize, CGSizeZero) == NO && contentViewWidth != constrainedSize.max.width) { constrainedSize.min.width = contentViewWidth; constrainedSize.max.width = contentViewWidth; // Re-measurement is done on main to ensure thread affinity. In the worst case, this is as fast as UIKit's implementation. // // Unloaded nodes *could* be re-measured off the main thread, but only with the assumption that content view width // is the same for all cells (because there is no easy way to get that individual value before the node being assigned to a _ASTableViewCell). // Also, in many cases, some nodes may not need to be re-measured at all, such as when user enters and then immediately leaves editing mode. // To avoid premature optimization and making such assumption, as well as to keep ASTableView simple, re-measurement is strictly done on main. CGSize oldSize = node.bounds.size; const CGSize calculatedSize = [node layoutThatFits:constrainedSize].size; node.frame = { .size = calculatedSize }; // After the re-measurement, set the new constrained size to the node's backing colleciton element. node.collectionElement.constrainedSize = constrainedSize; // If the node height changed, trigger a height requery. if (oldSize.height != calculatedSize.height) { [self beginUpdates]; [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; } } } #pragma mark - ASCellNodeDelegate - (void)nodeSelectedStateDidChange:(ASCellNode *)node { NSIndexPath *indexPath = [self indexPathForNode:node]; if (indexPath) { if (node.isSelected) { [self selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; } else { [self deselectRowAtIndexPath:indexPath animated:NO]; } } } - (void)nodeHighlightedStateDidChange:(ASCellNode *)node { NSIndexPath *indexPath = [self indexPathForNode:node]; if (indexPath) { [self cellForRowAtIndexPath:indexPath].highlighted = node.isHighlighted; } } - (void)nodeDidInvalidateSize:(ASCellNode *)node { [_cellsForLayoutUpdates addObject:node]; [self setNeedsLayout]; } // Cause UITableView to requery for the new height of this node - (void)requeryNodeHeights { _queuedNodeHeightUpdate = NO; [super beginUpdates]; [super endUpdates]; } #pragma mark - Helper Methods // Note: This is called every layout, and so it is very performance sensitive. - (CGFloat)sectionIndexWidth { // If they don't implement the methods, then there's no section index. if (_asyncDataSourceFlags.sectionIndexMethods == NO) { return 0; } UIView *indexView = _sectionIndexView; if (indexView.superview == self) { return indexView.frame.size.width; } CGRect bounds = self.bounds; for (UIView *view in self.subviews) { CGRect frame = view.frame; // Section index is right-aligned and less than half-width. if (CGRectGetMaxX(frame) == CGRectGetMaxX(bounds) && frame.size.width * 2 < bounds.size.width) { _sectionIndexView = view; return frame.size.width; } } return 0; } #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.tableNode; if (visible && !node.inHierarchy) { [node __enterHierarchy]; } } - (void)didMoveToWindow { BOOL visible = (self.window != nil); ASDisplayNode *node = self.tableNode; BOOL rangeControllerNeedsUpdate = ![node supportsRangeManagedInterfaceState];; if (!visible && node.inHierarchy) { if (rangeControllerNeedsUpdate) { rangeControllerNeedsUpdate = NO; // Exit CellNodes first before Table 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.tableNode; } } - (void)didMoveToSuperview { if (self.superview == nil) { _keepalive_node = nil; } } #pragma mark - Accessibility overrides - (NSArray *)accessibilityElements { [self waitUntilAllUpdatesAreCommitted]; return [super accessibilityElements]; } @end #endif