mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-09-08 21:49:41 +00:00
The API allows consumer of ASTableView or ASCollectionCiew to block execution of the main thread until all section and row updates are committed.
1081 lines
38 KiB
Plaintext
1081 lines
38 KiB
Plaintext
/* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "ASTableViewInternal.h"
|
|
|
|
#import "ASAssert.h"
|
|
#import "ASBatchFetching.h"
|
|
#import "ASCellNode+Internal.h"
|
|
#import "ASChangeSetDataController.h"
|
|
#import "ASDelegateProxy.h"
|
|
#import "ASDisplayNode+Beta.h"
|
|
#import "ASDisplayNode+FrameworkPrivate.h"
|
|
#import "ASInternalHelpers.h"
|
|
#import "ASLayout.h"
|
|
#import "ASLayoutController.h"
|
|
#import "ASRangeController.h"
|
|
#import "_ASDisplayLayer.h"
|
|
|
|
#import <CoreFoundation/CoreFoundation.h>
|
|
|
|
static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
|
|
|
|
//#define LOG(...) NSLog(__VA_ARGS__)
|
|
#define LOG(...)
|
|
|
|
#pragma mark -
|
|
#pragma mark ASCellNode<->UITableViewCell bridging.
|
|
|
|
@class _ASTableViewCell;
|
|
|
|
@protocol _ASTableViewCellDelegate <NSObject>
|
|
- (void)didLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell;
|
|
@end
|
|
|
|
@interface _ASTableViewCell : UITableViewCell
|
|
@property (nonatomic, weak) id<_ASTableViewCellDelegate> delegate;
|
|
@property (nonatomic, weak) ASCellNode *node;
|
|
@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];
|
|
}
|
|
|
|
- (void)setNode:(ASCellNode *)node
|
|
{
|
|
_node = node;
|
|
node.selected = self.selected;
|
|
node.highlighted = self.highlighted;
|
|
}
|
|
|
|
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
|
|
{
|
|
[super setSelected:selected animated:animated];
|
|
_node.selected = selected;
|
|
}
|
|
|
|
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
|
|
{
|
|
[super setHighlighted:highlighted animated:animated];
|
|
_node.highlighted = highlighted;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark ASTableView
|
|
|
|
@interface ASTableNode ()
|
|
- (instancetype)_initWithTableView:(ASTableView *)tableView;
|
|
@end
|
|
|
|
@interface ASTableView () <ASRangeControllerDataSource, ASRangeControllerDelegate,
|
|
ASDataControllerSource, _ASTableViewCellDelegate,
|
|
ASCellNodeLayoutDelegate, ASDelegateProxyInterceptor>
|
|
{
|
|
ASTableViewProxy *_proxyDataSource;
|
|
ASTableViewProxy *_proxyDelegate;
|
|
|
|
ASFlowLayoutController *_layoutController;
|
|
|
|
ASRangeController *_rangeController;
|
|
|
|
BOOL _asyncDataFetchingEnabled;
|
|
|
|
ASBatchContext *_batchContext;
|
|
|
|
NSIndexPath *_pendingVisibleIndexPath;
|
|
|
|
NSIndexPath *_contentOffsetAdjustmentTopVisibleRow;
|
|
CGFloat _contentOffsetAdjustment;
|
|
|
|
CGPoint _deceleratingVelocity;
|
|
|
|
CGFloat _nodesConstrainedWidth;
|
|
BOOL _ignoreNodesConstrainedWidthChange;
|
|
BOOL _queuedNodeHeightUpdate;
|
|
BOOL _isDeallocating;
|
|
BOOL _dataSourceImplementsNodeBlockForRowAtIndexPath;
|
|
BOOL _asyncDelegateImplementsScrollviewDidScroll;
|
|
NSMutableSet *_cellsForVisibilityUpdates;
|
|
}
|
|
|
|
@property (atomic, assign) BOOL asyncDataSourceLocked;
|
|
@property (nonatomic, retain, readwrite) ASDataController *dataController;
|
|
|
|
// Used only when ASTableView is created directly rather than through ASTableNode.
|
|
// We create a node so that logic related to appearance, memory management, etc can be located there
|
|
// for both the node-based and view-based version of the table.
|
|
// This also permits sharing logic with ASCollectionNode, as the superclass is not UIKit-controlled.
|
|
@property (nonatomic, retain) ASTableNode *strongTableNode;
|
|
|
|
// Always set, whether ASCollectionView is created directly or via ASCollectionNode.
|
|
@property (nonatomic, weak) ASTableNode *tableNode;
|
|
|
|
@end
|
|
|
|
@implementation ASTableView
|
|
|
|
// Using _ASDisplayLayer ensures things like -layout are properly forwarded to ASTableNode.
|
|
+ (Class)layerClass
|
|
{
|
|
return [_ASDisplayLayer class];
|
|
}
|
|
|
|
+ (Class)dataControllerClass
|
|
{
|
|
return [ASChangeSetDataController class];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Lifecycle
|
|
|
|
- (void)configureWithDataControllerClass:(Class)dataControllerClass
|
|
{
|
|
_layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical];
|
|
|
|
_rangeController = [[ASRangeController alloc] init];
|
|
_rangeController.layoutController = _layoutController;
|
|
_rangeController.dataSource = self;
|
|
_rangeController.delegate = self;
|
|
|
|
_dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:NO];
|
|
_dataController.dataSource = self;
|
|
_dataController.delegate = _rangeController;
|
|
|
|
_layoutController.dataSource = _dataController;
|
|
|
|
_asyncDataFetchingEnabled = NO;
|
|
_asyncDataSourceLocked = NO;
|
|
|
|
_leadingScreensForBatching = 2.0;
|
|
_batchContext = [[ASBatchContext alloc] init];
|
|
|
|
_automaticallyAdjustsContentOffset = NO;
|
|
|
|
_nodesConstrainedWidth = self.bounds.size.width;
|
|
// If the initial size is 0, expect a size change very soon which is part of the initial configuration
|
|
// and should not trigger a relayout.
|
|
_ignoreNodesConstrainedWidthChange = (_nodesConstrainedWidth == 0);
|
|
|
|
_proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self];
|
|
super.delegate = (id<UITableViewDelegate>)_proxyDelegate;
|
|
|
|
_proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self];
|
|
super.dataSource = (id<UITableViewDataSource>)_proxyDataSource;
|
|
|
|
[self registerClass:_ASTableViewCell.class forCellReuseIdentifier:kCellReuseIdentifier];
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
|
|
{
|
|
return [self _initWithFrame:frame style:style dataControllerClass:nil ownedByNode:NO];
|
|
}
|
|
|
|
// FIXME: This method is deprecated and will probably be removed in or shortly after 2.0.
|
|
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled
|
|
{
|
|
return [self _initWithFrame:frame style:style dataControllerClass:nil ownedByNode:NO];
|
|
}
|
|
|
|
- (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass ownedByNode:(BOOL)ownedByNode
|
|
{
|
|
if (!(self = [super initWithFrame:frame style:style])) {
|
|
return nil;
|
|
}
|
|
_cellsForVisibilityUpdates = [NSMutableSet set];
|
|
if (!dataControllerClass) {
|
|
dataControllerClass = [[self class] dataControllerClass];
|
|
}
|
|
|
|
[self configureWithDataControllerClass:dataControllerClass];
|
|
|
|
if (!ownedByNode) {
|
|
// See commentary at the definition of .strongTableNode for why we create an ASTableNode.
|
|
// FIXME: The _view pointer of the node retains us, but the node will die immediately if we don't
|
|
// retain it. At the moment there isn't a great solution to this, so we can't yet move our core
|
|
// logic to ASTableNode (required to have a shared superclass with ASCollection*).
|
|
ASTableNode *tableNode = nil; //[[ASTableNode alloc] _initWithTableView:self];
|
|
self.strongTableNode = tableNode;
|
|
}
|
|
|
|
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
|
|
{
|
|
// Sometimes the UIKit classes can call back to their delegate even during deallocation.
|
|
_isDeallocating = YES;
|
|
[self setAsyncDelegate:nil];
|
|
[self setAsyncDataSource:nil];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Overrides
|
|
|
|
- (void)setDataSource:(id<UITableViewDataSource>)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<UITableViewDelegate>)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.");
|
|
}
|
|
|
|
- (void)setAsyncDataSource:(id<ASTableViewDataSource>)asyncDataSource
|
|
{
|
|
// Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle
|
|
// the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource
|
|
// will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to nil out
|
|
// super.dataSource in this case because calls to ASTableViewProxy will start failing and cause crashes.
|
|
|
|
super.dataSource = nil;
|
|
|
|
if (asyncDataSource == nil) {
|
|
_asyncDataSource = nil;
|
|
_proxyDataSource = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self];
|
|
_dataSourceImplementsNodeBlockForRowAtIndexPath = NO;
|
|
} else {
|
|
_asyncDataSource = asyncDataSource;
|
|
_dataSourceImplementsNodeBlockForRowAtIndexPath = [_asyncDataSource respondsToSelector:@selector(tableView:nodeBlockForRowAtIndexPath:)];
|
|
// Data source must implement tableView:nodeBlockForRowAtIndexPath: or tableView:nodeForRowAtIndexPath:
|
|
ASDisplayNodeAssertTrue(_dataSourceImplementsNodeBlockForRowAtIndexPath || [_asyncDataSource respondsToSelector:@selector(tableView:nodeForRowAtIndexPath:)]);
|
|
_proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self];
|
|
}
|
|
|
|
super.dataSource = (id<UITableViewDataSource>)_proxyDataSource;
|
|
}
|
|
|
|
- (void)setAsyncDelegate:(id<ASTableViewDelegate>)asyncDelegate
|
|
{
|
|
// Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle
|
|
// the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate
|
|
// will return as nil (ARC magic) even though the _proxyDelegate still exists. It's really important to nil out
|
|
// super.delegate in this case because calls to ASTableViewProxy will start failing and cause crashes.
|
|
|
|
// Order is important here, the asyncDelegate must be callable while nilling super.delegate to avoid random crashes
|
|
// in UIScrollViewAccessibility.
|
|
|
|
super.delegate = nil;
|
|
|
|
if (asyncDelegate == nil) {
|
|
_asyncDelegate = nil;
|
|
_proxyDelegate = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self];
|
|
_asyncDelegateImplementsScrollviewDidScroll = NO;
|
|
} else {
|
|
_asyncDelegate = asyncDelegate;
|
|
_asyncDelegateImplementsScrollviewDidScroll = [_asyncDelegate respondsToSelector:@selector(scrollViewDidScroll:)];
|
|
_proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self];
|
|
}
|
|
|
|
super.delegate = (id<UITableViewDelegate>)_proxyDelegate;
|
|
}
|
|
|
|
- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy
|
|
{
|
|
if (proxy == _proxyDelegate) {
|
|
[self setAsyncDelegate:nil];
|
|
} else if (proxy == _proxyDataSource) {
|
|
[self setAsyncDataSource:nil];
|
|
}
|
|
}
|
|
|
|
- (void)reloadDataWithCompletion:(void (^)())completion
|
|
{
|
|
ASPerformBlockOnMainThread(^{
|
|
[super reloadData];
|
|
});
|
|
[_dataController reloadDataWithAnimationOptions:UITableViewRowAnimationNone completion:completion];
|
|
}
|
|
|
|
- (void)reloadData
|
|
{
|
|
[self reloadDataWithCompletion:nil];
|
|
}
|
|
|
|
- (void)reloadDataImmediately
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController reloadDataImmediatelyWithAnimationOptions:UITableViewRowAnimationNone];
|
|
[super reloadData];
|
|
}
|
|
|
|
- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType
|
|
{
|
|
[_layoutController setTuningParameters:tuningParameters forRangeMode:ASLayoutRangeModeFull rangeType:rangeType];
|
|
}
|
|
|
|
- (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType
|
|
{
|
|
return [_layoutController tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:rangeType];
|
|
}
|
|
|
|
- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
|
|
{
|
|
[_layoutController setTuningParameters:tuningParameters forRangeMode:rangeMode rangeType:rangeType];
|
|
}
|
|
|
|
- (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType
|
|
{
|
|
return [_layoutController tuningParametersForRangeMode:rangeMode rangeType:rangeType];
|
|
}
|
|
|
|
- (NSArray<NSArray <ASCellNode *> *> *)completedNodes
|
|
{
|
|
return [_dataController completedNodes];
|
|
}
|
|
|
|
- (ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
return [_dataController nodeAtIndexPath:indexPath];
|
|
}
|
|
|
|
- (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode
|
|
{
|
|
return [_dataController indexPathForNode:cellNode];
|
|
}
|
|
|
|
- (NSArray *)visibleNodes
|
|
{
|
|
NSArray *indexPaths = [self indexPathsForVisibleRows];
|
|
NSMutableArray *visibleNodes = [[NSMutableArray alloc] init];
|
|
|
|
for (NSIndexPath *indexPath in indexPaths) {
|
|
ASCellNode *node = [self nodeForRowAtIndexPath:indexPath];
|
|
if (node) {
|
|
// It is possible for UITableView to return indexPaths before the node is completed.
|
|
[visibleNodes addObject:node];
|
|
}
|
|
}
|
|
|
|
return visibleNodes;
|
|
}
|
|
|
|
- (void)beginUpdates
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController beginUpdates];
|
|
}
|
|
|
|
- (void)endUpdates
|
|
{
|
|
[self endUpdatesAnimated:YES completion:nil];
|
|
}
|
|
|
|
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion;
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController endUpdatesAnimated:animated completion:completion];
|
|
}
|
|
|
|
- (void)waitUntilAllUpdatesAreCommitted
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController waitUntilAllUpdatesAreCommitted];
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
if (_nodesConstrainedWidth != self.bounds.size.width) {
|
|
_nodesConstrainedWidth = self.bounds.size.width;
|
|
|
|
// First width change occurs during initial configuration. An expensive relayout pass is unnecessary at that time
|
|
// and should be avoided, assuming that the initial data loading automatically runs shortly afterward.
|
|
if (_ignoreNodesConstrainedWidthChange) {
|
|
_ignoreNodesConstrainedWidthChange = NO;
|
|
} else {
|
|
[self beginUpdates];
|
|
[_dataController relayoutAllNodes];
|
|
[self endUpdates];
|
|
}
|
|
}
|
|
|
|
// To ensure _nodesConstrainedWidth is up-to-date for every usage, this call to super must be done last
|
|
[super layoutSubviews];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Editing
|
|
|
|
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController insertSections:sections withAnimationOptions:animation];
|
|
}
|
|
|
|
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController deleteSections:sections withAnimationOptions:animation];
|
|
}
|
|
|
|
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController reloadSections:sections withAnimationOptions:animation];
|
|
}
|
|
|
|
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController moveSection:section toSection:newSection withAnimationOptions:UITableViewRowAnimationNone];
|
|
}
|
|
|
|
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
|
|
}
|
|
|
|
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
|
|
}
|
|
|
|
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
|
|
}
|
|
|
|
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark adjust content offset
|
|
|
|
- (void)beginAdjustingContentOffset
|
|
{
|
|
ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES");
|
|
_contentOffsetAdjustment = 0;
|
|
_contentOffsetAdjustmentTopVisibleRow = self.indexPathsForVisibleRows.firstObject;
|
|
}
|
|
|
|
- (void)endAdjustingContentOffset
|
|
{
|
|
ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES");
|
|
if (_contentOffsetAdjustment != 0) {
|
|
self.contentOffset = CGPointMake(0, self.contentOffset.y+_contentOffsetAdjustment);
|
|
}
|
|
|
|
_contentOffsetAdjustment = 0;
|
|
_contentOffsetAdjustmentTopVisibleRow = nil;
|
|
}
|
|
|
|
- (void)adjustContentOffsetWithNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths inserting:(BOOL)inserting {
|
|
// Maintain the users visible window when inserting or deleteing cells by adjusting the content offset for nodes
|
|
// before the visible area. If in a begin/end updates block this will update _contentOffsetAdjustment, otherwise it will
|
|
// update self.contentOffset directly.
|
|
|
|
ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES");
|
|
|
|
CGFloat dir = (inserting) ? +1 : -1;
|
|
CGFloat adjustment = 0;
|
|
NSIndexPath *top = _contentOffsetAdjustmentTopVisibleRow ?: self.indexPathsForVisibleRows.firstObject;
|
|
|
|
for (int index = 0; index < indexPaths.count; index++) {
|
|
NSIndexPath *indexPath = indexPaths[index];
|
|
if ([indexPath compare:top] <= 0) { // if this row is before or equal to the topmost visible row, make adjustments...
|
|
ASCellNode *cellNode = nodes[index];
|
|
adjustment += cellNode.calculatedSize.height * dir;
|
|
if (indexPath.section == top.section) {
|
|
top = [NSIndexPath indexPathForRow:top.row+dir inSection:top.section];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_contentOffsetAdjustmentTopVisibleRow) { // true of we are in a begin/end update block (see beginAdjustingContentOffset)
|
|
_contentOffsetAdjustmentTopVisibleRow = top;
|
|
_contentOffsetAdjustment += adjustment;
|
|
} else if (adjustment != 0) {
|
|
self.contentOffset = CGPointMake(0, self.contentOffset.y+adjustment);
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#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;
|
|
|
|
ASCellNode *node = [_dataController nodeAtIndexPath:indexPath];
|
|
[_rangeController configureContentView:cell.contentView forCellNode:node];
|
|
|
|
cell.node = node;
|
|
cell.backgroundColor = node.backgroundColor;
|
|
cell.selectionStyle = node.selectionStyle;
|
|
|
|
// 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.)
|
|
cell.clipsToBounds = node.clipsToBounds;
|
|
|
|
return cell;
|
|
}
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
ASCellNode *node = [_dataController nodeAtIndexPath:indexPath];
|
|
return node.calculatedSize.height;
|
|
}
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
|
{
|
|
return [_dataController numberOfSections];
|
|
}
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
|
{
|
|
return [_dataController numberOfRowsInSection:section];
|
|
}
|
|
|
|
- (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)velocity
|
|
{
|
|
ASScrollDirection direction = ASScrollDirectionNone;
|
|
if (velocity.y > 0) {
|
|
direction = ASScrollDirectionDown;
|
|
} else if (velocity.y < 0) {
|
|
direction = ASScrollDirectionUp;
|
|
}
|
|
return direction;
|
|
}
|
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
|
{
|
|
for (_ASTableViewCell *tableCell in _cellsForVisibilityUpdates) {
|
|
[[tableCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged
|
|
inScrollView:scrollView
|
|
withCellFrame:tableCell.frame];
|
|
}
|
|
if (_asyncDelegateImplementsScrollviewDidScroll) {
|
|
[_asyncDelegate scrollViewDidScroll:scrollView];
|
|
}
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
_pendingVisibleIndexPath = indexPath;
|
|
|
|
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection];
|
|
|
|
if ([_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNodeForRowAtIndexPath:)]) {
|
|
[_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath];
|
|
}
|
|
|
|
ASCellNode *cellNode = [cell node];
|
|
|
|
if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) {
|
|
[_cellsForVisibilityUpdates addObject:cell];
|
|
[cellNode cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisible
|
|
inScrollView:tableView
|
|
withCellFrame:cell.frame];
|
|
}
|
|
if (cellNode.neverShowPlaceholders) {
|
|
[cellNode recursivelyEnsureDisplaySynchronously:YES];
|
|
}
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
|
|
{
|
|
if ([_pendingVisibleIndexPath isEqual:indexPath]) {
|
|
_pendingVisibleIndexPath = nil;
|
|
}
|
|
|
|
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection];
|
|
|
|
if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]) {
|
|
ASCellNode *node = ((_ASTableViewCell *)cell).node;
|
|
ASDisplayNodeAssertNotNil(node, @"Expected node associated with removed cell not to be nil.");
|
|
[_asyncDelegate tableView:self didEndDisplayingNode:node forRowAtIndexPath:indexPath];
|
|
}
|
|
|
|
if ([_cellsForVisibilityUpdates containsObject:cell]) {
|
|
[_cellsForVisibilityUpdates removeObject:cell];
|
|
ASCellNode *node = ((_ASTableViewCell *)cell).node;
|
|
[node cellNodeVisibilityEvent:ASCellNodeVisibilityEventInvisible
|
|
inScrollView:tableView
|
|
withCellFrame:cell.frame];
|
|
}
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNodeForRowAtIndexPath:)]) {
|
|
[_asyncDelegate tableView:self didEndDisplayingNodeForRowAtIndexPath:indexPath];
|
|
}
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Batch Fetching
|
|
|
|
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
|
|
{
|
|
_deceleratingVelocity = CGPointMake(
|
|
scrollView.contentOffset.x - ((targetContentOffset != NULL) ? targetContentOffset->x : 0),
|
|
scrollView.contentOffset.y - ((targetContentOffset != NULL) ? targetContentOffset->y : 0)
|
|
);
|
|
|
|
if (targetContentOffset != NULL) {
|
|
[self handleBatchFetchScrollingToOffset:*targetContentOffset];
|
|
}
|
|
|
|
if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) {
|
|
[_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
|
|
}
|
|
}
|
|
|
|
- (BOOL)shouldBatchFetch
|
|
{
|
|
// if the delegate does not respond to this method, there is no point in starting to fetch
|
|
BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)];
|
|
if (canFetch && [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]) {
|
|
return [_asyncDelegate shouldBatchFetchForTableView:self];
|
|
} else {
|
|
return canFetch;
|
|
}
|
|
}
|
|
|
|
- (void)handleBatchFetchScrollingToOffset:(CGPoint)targetOffset
|
|
{
|
|
ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist");
|
|
|
|
if (![self shouldBatchFetch]) {
|
|
return;
|
|
}
|
|
|
|
if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) {
|
|
[_batchContext beginBatchFetching];
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
[_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext];
|
|
});
|
|
}
|
|
}
|
|
|
|
#pragma mark - ASRangeControllerDataSource
|
|
|
|
- (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
|
|
NSArray *visibleIndexPaths = self.indexPathsForVisibleRows;
|
|
|
|
if (_pendingVisibleIndexPath) {
|
|
NSMutableSet *indexPaths = [NSMutableSet setWithArray:self.indexPathsForVisibleRows];
|
|
|
|
BOOL (^isAfter)(NSIndexPath *, NSIndexPath *) = ^BOOL(NSIndexPath *indexPath, NSIndexPath *anchor) {
|
|
if (!anchor || !indexPath) {
|
|
return NO;
|
|
}
|
|
if (indexPath.section == anchor.section) {
|
|
return (indexPath.row == anchor.row+1); // assumes that indexes are valid
|
|
|
|
} else if (indexPath.section > anchor.section && indexPath.row == 0) {
|
|
if (anchor.row != [_dataController numberOfRowsInSection:anchor.section] -1) {
|
|
return NO; // anchor is not at the end of the section
|
|
}
|
|
|
|
NSInteger nextSection = anchor.section+1;
|
|
while([_dataController numberOfRowsInSection:nextSection] == 0) {
|
|
++nextSection;
|
|
}
|
|
|
|
return indexPath.section == nextSection;
|
|
}
|
|
|
|
return NO;
|
|
};
|
|
|
|
BOOL (^isBefore)(NSIndexPath *, NSIndexPath *) = ^BOOL(NSIndexPath *indexPath, NSIndexPath *anchor) {
|
|
return isAfter(anchor, indexPath);
|
|
};
|
|
|
|
if ([indexPaths containsObject:_pendingVisibleIndexPath]) {
|
|
_pendingVisibleIndexPath = nil; // once it has shown up in visibleIndexPaths, we can stop tracking it
|
|
} else if (!isBefore(_pendingVisibleIndexPath, visibleIndexPaths.firstObject) &&
|
|
!isAfter(_pendingVisibleIndexPath, visibleIndexPaths.lastObject)) {
|
|
_pendingVisibleIndexPath = nil; // not contiguous, ignore.
|
|
} else {
|
|
[indexPaths addObject:_pendingVisibleIndexPath];
|
|
visibleIndexPaths = [indexPaths.allObjects sortedArrayUsingSelector:@selector(compare:)];
|
|
}
|
|
}
|
|
|
|
return visibleIndexPaths;
|
|
}
|
|
|
|
- (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths
|
|
{
|
|
return [_dataController nodesAtIndexPaths:indexPaths];
|
|
}
|
|
|
|
- (ASDisplayNode *)rangeController:(ASRangeController *)rangeController nodeAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
return [_dataController nodeAtIndexPath:indexPath];
|
|
}
|
|
|
|
- (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
return self.bounds.size;
|
|
}
|
|
|
|
- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController
|
|
{
|
|
ASTableNode *tableNode = self.tableNode;
|
|
if (tableNode) {
|
|
return self.tableNode.interfaceState;
|
|
} else {
|
|
// Until we can always create an associated ASTableNode without a retain cycle,
|
|
// we might be on our own to try to guess if we're visible. The node normally
|
|
// handles this even if it is the root / directly added to the view hierarchy.
|
|
return (self.window != nil ? ASInterfaceStateVisible : ASInterfaceStateNone);
|
|
}
|
|
}
|
|
|
|
#pragma mark - ASRangeControllerDelegate
|
|
|
|
- (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
LOG(@"--- UITableView beginUpdates");
|
|
|
|
if (!self.asyncDataSource) {
|
|
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
|
|
}
|
|
|
|
[super beginUpdates];
|
|
|
|
if (_automaticallyAdjustsContentOffset) {
|
|
[self beginAdjustingContentOffset];
|
|
}
|
|
}
|
|
|
|
- (void)rangeController:(ASRangeController *)rangeController didEndUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
LOG(@"--- UITableView endUpdates");
|
|
|
|
if (!self.asyncDataSource) {
|
|
if (completion) {
|
|
completion(NO);
|
|
}
|
|
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
|
|
}
|
|
|
|
if (_automaticallyAdjustsContentOffset) {
|
|
[self endAdjustingContentOffset];
|
|
}
|
|
|
|
ASPerformBlockWithoutAnimation(!animated, ^{
|
|
[super endUpdates];
|
|
});
|
|
|
|
if (completion) {
|
|
completion(YES);
|
|
}
|
|
}
|
|
|
|
- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
LOG(@"UITableView insertRows:%ld rows", indexPaths.count);
|
|
|
|
if (!self.asyncDataSource) {
|
|
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
|
|
}
|
|
|
|
BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone;
|
|
ASPerformBlockWithoutAnimation(preventAnimation, ^{
|
|
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions];
|
|
});
|
|
|
|
if (_automaticallyAdjustsContentOffset) {
|
|
[self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:YES];
|
|
}
|
|
}
|
|
|
|
- (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
LOG(@"UITableView deleteRows:%ld rows", indexPaths.count);
|
|
|
|
if (!self.asyncDataSource) {
|
|
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
|
|
}
|
|
|
|
BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone;
|
|
ASPerformBlockWithoutAnimation(preventAnimation, ^{
|
|
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions];
|
|
});
|
|
|
|
if (_automaticallyAdjustsContentOffset) {
|
|
[self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:NO];
|
|
}
|
|
}
|
|
|
|
- (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
LOG(@"UITableView insertSections:%@", indexSet);
|
|
|
|
|
|
if (!self.asyncDataSource) {
|
|
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
|
|
}
|
|
|
|
BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone;
|
|
ASPerformBlockWithoutAnimation(preventAnimation, ^{
|
|
[super insertSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions];
|
|
});
|
|
}
|
|
|
|
- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
LOG(@"UITableView deleteSections:%@", indexSet);
|
|
|
|
if (!self.asyncDataSource) {
|
|
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
|
|
}
|
|
|
|
BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone;
|
|
ASPerformBlockWithoutAnimation(preventAnimation, ^{
|
|
[super deleteSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions];
|
|
});
|
|
}
|
|
|
|
#pragma mark - ASDataControllerDelegate
|
|
|
|
- (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAtIndexPath:(NSIndexPath *)indexPath {
|
|
if (![_asyncDataSource respondsToSelector:@selector(tableView:nodeBlockForRowAtIndexPath:)]) {
|
|
ASCellNode *node = [_asyncDataSource tableView:self nodeForRowAtIndexPath:indexPath];
|
|
ASDisplayNodeAssert([node isKindOfClass:ASCellNode.class], @"invalid node class, expected ASCellNode");
|
|
__weak __typeof__(self) weakSelf = self;
|
|
return ^{
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
[node enterHierarchyState:ASHierarchyStateRangeManaged];
|
|
if (node.layoutDelegate == nil) {
|
|
node.layoutDelegate = strongSelf;
|
|
}
|
|
return node;
|
|
};
|
|
}
|
|
|
|
ASCellNodeBlock block = [_asyncDataSource tableView:self nodeBlockForRowAtIndexPath:indexPath];
|
|
__weak __typeof__(self) weakSelf = self;
|
|
ASCellNodeBlock configuredNodeBlock = ^{
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
ASCellNode *node = block();
|
|
[node enterHierarchyState:ASHierarchyStateRangeManaged];
|
|
if (node.layoutDelegate == nil) {
|
|
node.layoutDelegate = strongSelf;
|
|
}
|
|
return node;
|
|
};
|
|
return configuredNodeBlock;
|
|
}
|
|
|
|
- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
return ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, 0),
|
|
CGSizeMake(_nodesConstrainedWidth, FLT_MAX));
|
|
}
|
|
|
|
- (void)dataControllerLockDataSource
|
|
{
|
|
ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked");
|
|
|
|
self.asyncDataSourceLocked = YES;
|
|
|
|
if ([_asyncDataSource respondsToSelector:@selector(tableViewLockDataSource:)]) {
|
|
[_asyncDataSource tableViewLockDataSource:self];
|
|
}
|
|
}
|
|
|
|
- (void)dataControllerUnlockDataSource
|
|
{
|
|
ASDisplayNodeAssert(self.asyncDataSourceLocked, @"The data source has already been unlocked");
|
|
|
|
self.asyncDataSourceLocked = NO;
|
|
|
|
if ([_asyncDataSource respondsToSelector:@selector(tableViewUnlockDataSource:)]) {
|
|
[_asyncDataSource tableViewUnlockDataSource:self];
|
|
}
|
|
}
|
|
|
|
- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section
|
|
{
|
|
return [_asyncDataSource tableView:self numberOfRowsInSection:section];
|
|
}
|
|
|
|
- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController
|
|
{
|
|
if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
|
|
return [_asyncDataSource numberOfSectionsInTableView:self];
|
|
} else {
|
|
return 1; // default section number
|
|
}
|
|
}
|
|
|
|
#pragma mark - _ASTableViewCellDelegate
|
|
|
|
- (void)didLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell
|
|
{
|
|
CGFloat contentViewWidth = tableViewCell.contentView.bounds.size.width;
|
|
ASCellNode *node = tableViewCell.node;
|
|
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 (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.
|
|
[self beginUpdates];
|
|
CGSize calculatedSize = [[node measureWithSizeRange:constrainedSize] size];
|
|
node.frame = CGRectMake(0, 0, calculatedSize.width, calculatedSize.height);
|
|
[self endUpdates];
|
|
}
|
|
}
|
|
|
|
#pragma mark - ASCellNodeLayoutDelegate
|
|
|
|
- (void)nodeDidRelayout:(ASCellNode *)node sizeChanged:(BOOL)sizeChanged
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
|
|
if (!sizeChanged || _queuedNodeHeightUpdate) {
|
|
return;
|
|
}
|
|
|
|
_queuedNodeHeightUpdate = YES;
|
|
[self performSelector:@selector(requeryNodeHeights)
|
|
withObject:nil
|
|
afterDelay:0
|
|
inModes:@[ NSRunLoopCommonModes ]];
|
|
}
|
|
|
|
// Cause UITableView to requery for the new height of this node
|
|
- (void)requeryNodeHeights
|
|
{
|
|
_queuedNodeHeightUpdate = NO;
|
|
|
|
[super beginUpdates];
|
|
[super endUpdates];
|
|
}
|
|
|
|
#pragma mark - Memory Management
|
|
|
|
- (void)clearContents
|
|
{
|
|
for (NSArray *section in [_dataController completedNodes]) {
|
|
for (ASDisplayNode *node in section) {
|
|
[node recursivelyClearContents];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)clearFetchedData
|
|
{
|
|
for (NSArray *section in [_dataController completedNodes]) {
|
|
for (ASDisplayNode *node in section) {
|
|
[node recursivelyClearFetchedData];
|
|
}
|
|
}
|
|
}
|
|
|
|
#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;
|
|
if (!visible && node.inHierarchy) {
|
|
[node __exitHierarchy];
|
|
}
|
|
|
|
// Trigger updating interfaceState for cells in case ASTableView becomes visible or invisible
|
|
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection];
|
|
}
|
|
|
|
@end
|