mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-16 18:40:05 +00:00
622 lines
21 KiB
Plaintext
622 lines
21 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 "ASRangeController.h"
|
|
|
|
#import "ASAssert.h"
|
|
#import "ASDisplayNodeExtras.h"
|
|
#import "ASDisplayNodeInternal.h"
|
|
#import "ASRangeControllerInternal.h"
|
|
|
|
typedef NS_ENUM(NSInteger, ASScrollDirection) {
|
|
ASScrollDirectionUp,
|
|
ASScrollDirectionDown,
|
|
};
|
|
|
|
@interface ASRangeController () {
|
|
// index path -> node mapping
|
|
NSMutableDictionary *_nodes;
|
|
|
|
// array of boxed CGSizes. _nodeSizes.count == the number of nodes that have been sized
|
|
// TODO optimise this, perhaps by making _nodes an array
|
|
NSMutableArray *_nodeSizes;
|
|
|
|
// consumer data source information
|
|
NSArray *_sectionCounts;
|
|
NSInteger _totalNodeCount;
|
|
|
|
// used for global <-> section.row mapping. _sectionOffsets[section] is the index at which the section starts
|
|
NSArray *_sectionOffsets;
|
|
|
|
// sized data source information
|
|
NSInteger _sizedNodeCount;
|
|
|
|
// ranges
|
|
BOOL _queuedRangeUpdate;
|
|
ASScrollDirection _scrollDirection;
|
|
NSRange _visibleRange;
|
|
NSRange _workingRange;
|
|
NSMutableOrderedSet *_workingIndexPaths;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation ASRangeController
|
|
|
|
#pragma mark -
|
|
#pragma mark Lifecycle.
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (!(self = [super init]))
|
|
return nil;
|
|
|
|
_tuningParameters = {
|
|
.trailingBufferScreenfuls = 1,
|
|
.leadingBufferScreenfuls = 2,
|
|
};
|
|
|
|
return self;
|
|
}
|
|
|
|
+ (dispatch_queue_t)sizingQueue
|
|
{
|
|
static dispatch_queue_t sizingQueue = NULL;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
sizingQueue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASRangeController.sizingQueue", DISPATCH_QUEUE_CONCURRENT);
|
|
// we use the highpri queue to prioritize UI rendering over other async operations
|
|
dispatch_set_target_queue(sizingQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
|
|
});
|
|
|
|
return sizingQueue;
|
|
}
|
|
|
|
+ (UIView *)workingView
|
|
{
|
|
// we add nodes' views to this invisible window to start async rendering
|
|
static UIWindow *workingWindow = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
workingWindow = [[UIWindow alloc] initWithFrame:CGRectZero];
|
|
workingWindow.windowLevel = UIWindowLevelNormal - 1000;
|
|
workingWindow.userInteractionEnabled = NO;
|
|
workingWindow.clipsToBounds = YES;
|
|
workingWindow.hidden = YES;
|
|
});
|
|
|
|
return workingWindow;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Helpers.
|
|
|
|
static NSOrderedSet *ASCopySetMinusSet(NSOrderedSet *minuend, NSOrderedSet *subtrahend)
|
|
{
|
|
NSMutableOrderedSet *difference = [minuend mutableCopy];
|
|
[difference minusOrderedSet:subtrahend];
|
|
return difference;
|
|
}
|
|
|
|
// useful for debugging: working range, buffer sizes, and visible range
|
|
__attribute__((unused)) static NSString *ASWorkingRangeDebugDescription(NSRange workingRange, NSRange visibleRange)
|
|
{
|
|
NSInteger visibleRangeLastElement = NSMaxRange(visibleRange) - 1;
|
|
NSInteger workingRangeLastElement = NSMaxRange(workingRange) - 1;
|
|
return [NSString stringWithFormat:@"[%zd(%zd) [%zd, %zd] (%zd)%zd]",
|
|
workingRange.location,
|
|
visibleRange.location - workingRange.location,
|
|
visibleRange.location,
|
|
visibleRangeLastElement,
|
|
workingRangeLastElement - visibleRangeLastElement,
|
|
workingRangeLastElement];
|
|
}
|
|
|
|
#pragma mark NSRange <-> NSIndexPath.
|
|
|
|
static BOOL ASRangeIsValid(NSRange range)
|
|
{
|
|
return range.location != NSNotFound && range.length > 0;
|
|
}
|
|
|
|
- (NSIndexPath *)indexPathForIndex:(NSInteger)index
|
|
{
|
|
ASDisplayNodeAssert(index < _totalNodeCount, @"invalid argument");
|
|
|
|
for (NSInteger section = _sectionCounts.count - 1; section >= 0; section--) {
|
|
NSInteger offset = [_sectionOffsets[section] integerValue];
|
|
if (offset <= index) {
|
|
return [NSIndexPath indexPathForRow:index - offset inSection:section];
|
|
}
|
|
}
|
|
|
|
ASDisplayNodeAssert(NO, @"logic error");
|
|
return nil;
|
|
}
|
|
|
|
- (NSArray *)indexPathsForRange:(NSRange)range
|
|
{
|
|
ASDisplayNodeAssert(ASRangeIsValid(range) && NSMaxRange(range) <= _totalNodeCount, @"invalid argument");
|
|
|
|
NSMutableArray *result = [NSMutableArray arrayWithCapacity:range.length];
|
|
|
|
NSIndexPath *indexPath = [self indexPathForIndex:range.location];
|
|
for (NSInteger i = range.location; i < NSMaxRange(range); i++) {
|
|
[result addObject:indexPath];
|
|
|
|
if (indexPath.row + 1 >= [_sectionCounts[indexPath.section] integerValue]) {
|
|
indexPath = [NSIndexPath indexPathForRow:0 inSection:indexPath.section + 1];
|
|
} else {
|
|
indexPath = [NSIndexPath indexPathForRow:indexPath.row + 1 inSection:indexPath.section];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSInteger)indexForIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
NSInteger index = [_sectionOffsets[indexPath.section] integerValue] + indexPath.row;
|
|
ASDisplayNodeAssert(index < _totalNodeCount, @"invalid argument");
|
|
return index;
|
|
}
|
|
|
|
#pragma mark View manipulation.
|
|
|
|
- (void)discardNode:(ASCellNode *)node
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
ASDisplayNodeAssert(node, @"invalid argument");
|
|
|
|
NSInteger index = [self indexForIndexPath:node.asyncdisplaykit_indexPath];
|
|
if (NSLocationInRange(index, _workingRange)) {
|
|
// move the node's view to the working range area, so its rendering persists
|
|
[self moveNodeToWorkingView:node];
|
|
} else {
|
|
// this node isn't in the working range, remove it from the view hierarchy
|
|
[self removeNodeFromWorkingView:node];
|
|
}
|
|
}
|
|
|
|
- (void)removeNodeFromWorkingView:(ASCellNode *)node
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
ASDisplayNodeAssert(node, @"invalid argument");
|
|
|
|
[node recursiveSetPreventOrCancelDisplay:YES];
|
|
[node.view removeFromSuperview];
|
|
[_workingIndexPaths removeObject:node.asyncdisplaykit_indexPath];
|
|
}
|
|
|
|
- (void)moveNodeToWorkingView:(ASCellNode *)node
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
ASDisplayNodeAssert(node, @"invalid argument");
|
|
|
|
[self moveNode:node toView:[ASRangeController workingView]];
|
|
[_workingIndexPaths addObject:node.asyncdisplaykit_indexPath];
|
|
}
|
|
|
|
- (void)moveNode:(ASCellNode *)node toView:(UIView *)view
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
ASDisplayNodeAssert(node && view, @"invalid argument, did you mean -removeNodeFromWorkingView:?");
|
|
|
|
// use an explicit transaction to force CoreAnimation to display nodes in order
|
|
[CATransaction begin];
|
|
|
|
// if this node is in the view hierarchy, moving it will cause a redisplay, so we disable hierarchy notifications.
|
|
// if it *isn't* in the view hierarchy, we need it to receive those notifications to trigger its first display.
|
|
BOOL nodeIsInHierarchy = (node.view.superview != nil);
|
|
|
|
if (nodeIsInHierarchy)
|
|
ASDisplayNodeDisableHierarchyNotifications(node);
|
|
|
|
[view addSubview:node.view];
|
|
|
|
if (nodeIsInHierarchy)
|
|
ASDisplayNodeEnableHierarchyNotifications(node);
|
|
|
|
[CATransaction commit];
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark API.
|
|
|
|
- (void)recalculateDataSourceCounts
|
|
{
|
|
// data source information (_sectionCounts, _sectionOffsets, _totalNodeCount) is not currently thread-safe
|
|
ASDisplayNodeAssertMainThread();
|
|
|
|
NSInteger sections = [_delegate rangeControllerSections:self];
|
|
|
|
NSMutableArray *sectionCounts = [NSMutableArray arrayWithCapacity:sections];
|
|
for (NSInteger section = 0; section < sections; section++) {
|
|
sectionCounts[section] = @([_delegate rangeController:self rowsInSection:section]);
|
|
}
|
|
|
|
NSMutableArray *sectionOffsets = [NSMutableArray arrayWithCapacity:sections];
|
|
NSInteger offset = 0;
|
|
for (NSInteger section = 0; section < sections; section++) {
|
|
sectionOffsets[section] = @(offset);
|
|
offset += [sectionCounts[section] integerValue];
|
|
}
|
|
|
|
_sectionCounts = sectionCounts;
|
|
_sectionOffsets = sectionOffsets;
|
|
_totalNodeCount = offset;
|
|
}
|
|
|
|
- (void)rebuildData
|
|
{
|
|
/*
|
|
* teardown
|
|
*/
|
|
for (ASCellNode *node in _nodes.objectEnumerator) {
|
|
[node removeFromSupernode];
|
|
[node.view removeFromSuperview];
|
|
}
|
|
[_nodes removeAllObjects];
|
|
_nodes = nil;
|
|
|
|
for (UIView *view in [[ASRangeController workingView] subviews]) {
|
|
[view removeFromSuperview];
|
|
}
|
|
|
|
/*
|
|
* setup
|
|
*/
|
|
[self recalculateDataSourceCounts];
|
|
_nodes = [NSMutableDictionary dictionaryWithCapacity:_totalNodeCount];
|
|
_visibleRange = _workingRange = NSMakeRange(NSNotFound, 0);
|
|
_sizedNodeCount = 0;
|
|
_nodeSizes = [NSMutableArray array];
|
|
_scrollDirection = ASScrollDirectionDown;
|
|
_workingIndexPaths = [NSMutableOrderedSet orderedSet];
|
|
|
|
// don't bother sizing if the data source is empty
|
|
if (_totalNodeCount > 0) {
|
|
[self sizeNextBlock];
|
|
}
|
|
}
|
|
|
|
- (void)visibleNodeIndexPathsDidChange
|
|
{
|
|
if (_queuedRangeUpdate)
|
|
return;
|
|
|
|
// coalesce these events -- handling them multiple times per runloop is noisy and expensive
|
|
_queuedRangeUpdate = YES;
|
|
[self performSelector:@selector(updateVisibleNodeIndexPaths)
|
|
withObject:nil
|
|
afterDelay:0
|
|
inModes:@[ NSRunLoopCommonModes ]];
|
|
}
|
|
|
|
- (void)updateVisibleNodeIndexPaths
|
|
{
|
|
NSArray *indexPaths = [_delegate rangeControllerVisibleNodeIndexPaths:self];
|
|
if (indexPaths.count) {
|
|
[self setVisibleRange:NSMakeRange([self indexForIndexPath:[indexPaths firstObject]],
|
|
indexPaths.count)];
|
|
}
|
|
|
|
_queuedRangeUpdate = NO;
|
|
}
|
|
|
|
- (NSInteger)numberOfSizedSections
|
|
{
|
|
// short-circuit if we haven't started sizing
|
|
if (_sizedNodeCount == 0)
|
|
return 0;
|
|
|
|
NSIndexPath *lastSizedIndex = [self indexPathForIndex:_sizedNodeCount - 1];
|
|
NSInteger sizedSectionCount = lastSizedIndex.section + 1;
|
|
|
|
ASDisplayNodeAssert(sizedSectionCount <= _sectionCounts.count, @"logic error");
|
|
return sizedSectionCount;
|
|
}
|
|
|
|
- (NSInteger)numberOfSizedRowsInSection:(NSInteger)section
|
|
{
|
|
// short-circuit if we haven't started sizing
|
|
if (_sizedNodeCount == 0)
|
|
return 0;
|
|
|
|
if (section > _sectionCounts.count) {
|
|
ASDisplayNodeAssert(NO, @"this isn't even a valid section");
|
|
return 0;
|
|
}
|
|
|
|
NSIndexPath *lastSizedIndex = [self indexPathForIndex:_sizedNodeCount - 1];
|
|
if (section > lastSizedIndex.section) {
|
|
ASDisplayNodeAssert(NO, @"this section hasn't been sized yet");
|
|
return 0;
|
|
} else if (section == lastSizedIndex.section) {
|
|
// we're still sizing this section, return the count we have
|
|
return lastSizedIndex.row + 1;
|
|
} else {
|
|
// we've already sized beyond this section, return the full count
|
|
return [_sectionCounts[section] integerValue];
|
|
}
|
|
}
|
|
|
|
- (void)configureContentView:(UIView *)contentView forIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
ASCellNode *newNode = [self sizedNodeForIndexPath:indexPath];
|
|
ASDisplayNodeAssert(newNode, @"this node hasn't been sized yet!");
|
|
|
|
if (newNode.view.superview == contentView) {
|
|
// this content view is already correctly configured
|
|
return;
|
|
}
|
|
|
|
for (UIView *view in contentView.subviews) {
|
|
ASDisplayNode *node = view.asyncdisplaykit_node;
|
|
if (node) {
|
|
// plunk this node back into the working range, if appropriate
|
|
ASDisplayNodeAssert([node isKindOfClass:[ASCellNode class]], @"invalid node");
|
|
[self discardNode:(ASCellNode *)node];
|
|
} else {
|
|
// if it's not a node, it's something random UITableView added to the hierarchy. kill it.
|
|
[view removeFromSuperview];
|
|
}
|
|
}
|
|
|
|
[self moveNode:newNode toView:contentView];
|
|
}
|
|
|
|
- (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
// TODO add an assertion (here or in ASTableView) that the calculated size isn't bogus (eg must be < tableview width)
|
|
ASCellNode *node = [self sizedNodeForIndexPath:indexPath];
|
|
return node.calculatedSize;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Working range.
|
|
|
|
- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters
|
|
{
|
|
_tuningParameters = tuningParameters;
|
|
|
|
if (ASRangeIsValid(_visibleRange)) {
|
|
[self recalculateWorkingRange];
|
|
}
|
|
}
|
|
|
|
static NSRange ASCalculateWorkingRange(ASRangeTuningParameters params, ASScrollDirection scrollDirection,
|
|
NSRange visibleRange, NSArray *nodeSizes, CGSize viewport)
|
|
{
|
|
ASDisplayNodeCAssert(NSMaxRange(visibleRange) <= nodeSizes.count, @"nodes can't be visible until they're sized");
|
|
|
|
// extend the visible range by enough nodes to fill at least the requested number of screenfuls
|
|
// NB. this logic assumes (UITableView-style) vertical scrolling and would need to be changed for ASCollectionView
|
|
CGFloat minUpperBufferSize, minLowerBufferSize;
|
|
switch (scrollDirection) {
|
|
case ASScrollDirectionUp:
|
|
minUpperBufferSize = viewport.height * params.leadingBufferScreenfuls;
|
|
minLowerBufferSize = viewport.height * params.trailingBufferScreenfuls;
|
|
break;
|
|
|
|
case ASScrollDirectionDown:
|
|
minUpperBufferSize = viewport.height * params.trailingBufferScreenfuls;
|
|
minLowerBufferSize = viewport.height * params.leadingBufferScreenfuls;
|
|
break;
|
|
}
|
|
|
|
// "top" buffer (above the screen, if we're scrolling vertically)
|
|
NSInteger upperBuffer = 0;
|
|
CGFloat upperBufferHeight = 0.0f;
|
|
for (NSInteger idx = visibleRange.location - 1; idx >= 0 && upperBufferHeight < minUpperBufferSize; idx--) {
|
|
upperBuffer++;
|
|
upperBufferHeight += [nodeSizes[idx] CGSizeValue].height;
|
|
}
|
|
|
|
// "bottom" buffer (below the screen, if we're scrolling vertically)
|
|
NSInteger lowerBuffer = 0;
|
|
CGFloat lowerBufferHeight = 0.0f;
|
|
for (NSInteger idx = NSMaxRange(visibleRange); idx < nodeSizes.count && lowerBufferHeight < minLowerBufferSize; idx++) {
|
|
lowerBuffer++;
|
|
lowerBufferHeight += [nodeSizes[idx] CGSizeValue].height;
|
|
}
|
|
|
|
return NSMakeRange(visibleRange.location - upperBuffer,
|
|
visibleRange.length + upperBuffer + lowerBuffer);
|
|
}
|
|
|
|
- (void)setVisibleRange:(NSRange)visibleRange
|
|
{
|
|
if (NSEqualRanges(_visibleRange, visibleRange))
|
|
return;
|
|
|
|
ASDisplayNodeAssert(ASRangeIsValid(visibleRange), @"invalid argument");
|
|
NSRange previouslyVisible = ASRangeIsValid(_visibleRange) ? _visibleRange : visibleRange;
|
|
_visibleRange = visibleRange;
|
|
|
|
// figure out where we're going, because that's where the bulk of the working range needs to be
|
|
NSInteger scrollDelta = _visibleRange.location - previouslyVisible.location;
|
|
if (scrollDelta < 0)
|
|
_scrollDirection = ASScrollDirectionUp;
|
|
if (scrollDelta > 0)
|
|
_scrollDirection = ASScrollDirectionDown;
|
|
|
|
[self recalculateWorkingRange];
|
|
}
|
|
|
|
- (void)recalculateWorkingRange
|
|
{
|
|
NSRange workingRange = ASCalculateWorkingRange(_tuningParameters,
|
|
_scrollDirection,
|
|
_visibleRange,
|
|
_nodeSizes,
|
|
[_delegate rangeControllerViewportSize:self]);
|
|
[self setWorkingRange:workingRange];
|
|
}
|
|
|
|
- (void)setWorkingRange:(NSRange)newWorkingRange
|
|
{
|
|
if (NSEqualRanges(_workingRange, newWorkingRange))
|
|
return;
|
|
|
|
// the working range is a superset of the visible range, but we only care about offscreen nodes
|
|
ASDisplayNodeAssert(NSEqualRanges(_visibleRange, NSIntersectionRange(_visibleRange, newWorkingRange)), @"logic error");
|
|
NSOrderedSet *visibleIndexPaths = [NSOrderedSet orderedSetWithArray:[self indexPathsForRange:_visibleRange]];
|
|
NSOrderedSet *oldWorkingIndexPaths = ASCopySetMinusSet(_workingIndexPaths, visibleIndexPaths);
|
|
NSOrderedSet *newWorkingIndexPaths = ASCopySetMinusSet([NSOrderedSet orderedSetWithArray:[self indexPathsForRange:newWorkingRange]], visibleIndexPaths);
|
|
|
|
// update bookkeeping for visible nodes; these will be removed from the working range later in -configureContentView::
|
|
[_workingIndexPaths minusOrderedSet:visibleIndexPaths];
|
|
|
|
// evict nodes that have left the working range (i.e., those that are in the old working range but not the new one)
|
|
NSOrderedSet *removedIndexPaths = ASCopySetMinusSet(oldWorkingIndexPaths, newWorkingIndexPaths);
|
|
for (NSIndexPath *indexPath in removedIndexPaths) {
|
|
ASCellNode *node = [self sizedNodeForIndexPath:indexPath];
|
|
ASDisplayNodeAssert(node, @"an unsized node should never have entered the working range");
|
|
[self removeNodeFromWorkingView:node];
|
|
}
|
|
|
|
// add nodes that have entered the working range (i.e., those that are in the new working range but not the old one)
|
|
NSOrderedSet *addedIndexPaths = ASCopySetMinusSet(newWorkingIndexPaths, oldWorkingIndexPaths);
|
|
for (NSIndexPath *indexPath in addedIndexPaths) {
|
|
// if a node in the working range is still sizing, the sizing logic will add it to the working range for us later
|
|
ASCellNode *node = [self sizedNodeForIndexPath:indexPath];
|
|
if (node) {
|
|
[self moveNodeToWorkingView:node];
|
|
} else {
|
|
ASDisplayNodeAssert(_sizedNodeCount != _totalNodeCount, @"logic error");
|
|
}
|
|
}
|
|
|
|
_workingRange = newWorkingRange;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Async sizing.
|
|
|
|
- (ASCellNode *)sizedNodeForIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
if ([self indexForIndexPath:indexPath] >= _sizedNodeCount) {
|
|
// this node hasn't been sized yet
|
|
return nil;
|
|
}
|
|
|
|
// work around applebug: a UIMutableIndexPath with row r and section s is not considered equal to an NSIndexPath with
|
|
// row r and section s, so we cannot use the provided indexPath directly as a dictionary index.
|
|
ASCellNode *sizedNode = _nodes[[NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section]];
|
|
ASDisplayNodeAssert(sizedNode, @"this node should be sized but doesn't even exist");
|
|
ASDisplayNodeAssert([sizedNode.asyncdisplaykit_indexPath isEqual:indexPath], @"this node has the wrong index path");
|
|
[sizedNode recursiveSetPreventOrCancelDisplay:NO];
|
|
return sizedNode;
|
|
}
|
|
|
|
- (void)sizeNextBlock
|
|
{
|
|
// concurrently size as many nodes as the CPU allows
|
|
static const NSInteger blockSize = [[NSProcessInfo processInfo] processorCount];
|
|
NSRange sizingRange = NSMakeRange(_sizedNodeCount, MIN(blockSize, _totalNodeCount - _sizedNodeCount));
|
|
|
|
// manage sizing on a throwaway background queue; we'll be blocking it
|
|
dispatch_async(dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT), ^{
|
|
dispatch_group_t group = dispatch_group_create();
|
|
|
|
NSArray *indexPaths = [self indexPathsForRange:sizingRange];
|
|
for (NSIndexPath *indexPath in indexPaths) {
|
|
ASCellNode *node = [_delegate rangeController:self nodeForIndexPath:indexPath];
|
|
node.asyncdisplaykit_indexPath = indexPath;
|
|
_nodes[indexPath] = node;
|
|
|
|
dispatch_group_async(group, [ASRangeController sizingQueue], ^{
|
|
[node measure:[_delegate rangeController:self constrainedSizeForNodeAtIndexPath:indexPath]];
|
|
node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height);
|
|
});
|
|
}
|
|
|
|
// wait for all sizing to finish, then bounce back to main
|
|
// TODO consider using a semaphore here -- we currently don't size nodes while updating the working range
|
|
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// update sized node information
|
|
_sizedNodeCount = NSMaxRange(sizingRange);
|
|
for (NSIndexPath *indexPath in indexPaths) {
|
|
ASCellNode *node = _nodes[indexPath];
|
|
_nodeSizes[[self indexForIndexPath:indexPath]] = [NSValue valueWithCGSize:node.calculatedSize];
|
|
}
|
|
ASDisplayNodeAssert(_nodeSizes.count == _sizedNodeCount, @"logic error");
|
|
|
|
// update the working range
|
|
if (ASRangeIsValid(_visibleRange)) {
|
|
[self recalculateWorkingRange];
|
|
}
|
|
|
|
// delegateify
|
|
[_delegate rangeController:self didSizeNodesWithIndexPaths:indexPaths];
|
|
|
|
// kick off the next block
|
|
if (_sizedNodeCount < _totalNodeCount) {
|
|
[self performSelector:@selector(sizeNextBlock) withObject:NULL afterDelay:0];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Editing.
|
|
|
|
static BOOL ASIndexPathsAreSequential(NSIndexPath *first, NSIndexPath *second)
|
|
{
|
|
BOOL row = (second.row == first.row + 1 && second.section == first.section);
|
|
BOOL section = (second.row == 0 && second.section == first.section + 1);
|
|
return row || section;
|
|
}
|
|
|
|
- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths
|
|
{
|
|
// sanity-check input
|
|
// TODO this is proof-of-concept-quality, expand validation when fleshing out update / editing support
|
|
NSIndexPath *lastNode = (_totalNodeCount > 0) ? [self indexPathForIndex:_totalNodeCount - 1] : nil;
|
|
BOOL indexPathsAreValid = ((lastNode && ASIndexPathsAreSequential(lastNode, [indexPaths firstObject])) ||
|
|
[[indexPaths firstObject] isEqual:[NSIndexPath indexPathForRow:0 inSection:0]]);
|
|
if (!indexPaths || !indexPaths.count || !indexPathsAreValid) {
|
|
ASDisplayNodeAssert(NO, @"invalid argument");
|
|
return;
|
|
}
|
|
|
|
// update all the things
|
|
void (^updateBlock)() = ^{
|
|
BOOL isSizing = (_sizedNodeCount < _totalNodeCount);
|
|
NSInteger expectedTotalNodeCount = _totalNodeCount + indexPaths.count;
|
|
|
|
[self recalculateDataSourceCounts];
|
|
ASDisplayNodeAssert(_totalNodeCount == expectedTotalNodeCount, @"data source error");
|
|
|
|
if (!isSizing) {
|
|
// the last sizing pass completely finished, start a new one
|
|
[self sizeNextBlock];
|
|
}
|
|
};
|
|
|
|
// trampoline to main if necessary, we don't have locks on _sectionCounts / _sectionOffsets / _totalNodeCount
|
|
if (![NSThread isMainThread]) {
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
updateBlock();
|
|
});
|
|
} else {
|
|
updateBlock();
|
|
}
|
|
}
|
|
|
|
|
|
@end
|