From 0b14d420337270618cdc71f7e764f95e5741c5c4 Mon Sep 17 00:00:00 2001 From: Jack Flintermann Date: Thu, 25 Jun 2015 16:36:32 -0400 Subject: [PATCH 1/9] add UIResponder methods to ASDisplayNode --- AsyncDisplayKit/ASDisplayNode.h | 9 +++++++++ AsyncDisplayKit/ASDisplayNode.mm | 24 +++++++++++++++++++++++ AsyncDisplayKit/Details/_ASDisplayView.mm | 8 ++++++++ 3 files changed, 41 insertions(+) diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index c963439c65..17ec215cee 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -468,6 +468,15 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); - (void)setNeedsDisplay; // Marks the view as needing display. Convenience for use whether view is created or not, or from a background thread. - (void)setNeedsLayout; // Marks the view as needing layout. Convenience for use whether view is created or not, or from a background thread. +// UIResponder methods +// By default these fall through to the underlying view, but can be overridden. +- (BOOL)canBecomeFirstResponder; // default==NO +- (BOOL)becomeFirstResponder; // default==NO (no-op) +- (BOOL)canResignFirstResponder; // default==YES +- (BOOL)resignFirstResponder; // default==YES (no-op) +- (BOOL)isFirstResponder; +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender; + @property (atomic, retain) id contents; // default=nil @property (atomic, assign) BOOL clipsToBounds; // default==NO @property (atomic, getter=isOpaque) BOOL opaque; // default==YES diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index c1a269e559..10a173d84a 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1697,6 +1697,30 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, } +- (BOOL)canBecomeFirstResponder { + return NO; +} + +- (BOOL)becomeFirstResponder { + return [self.view becomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder { + return YES; +} + +- (BOOL)resignFirstResponder { + return [self.view resignFirstResponder]; +} + +- (BOOL)isFirstResponder { + return [self.view isFirstResponder]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + return [self.view canPerformAction:action withSender:sender]; +} + @end @implementation ASDisplayNode (Debugging) diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm index 1bebdb9815..8b5300269d 100644 --- a/AsyncDisplayKit/Details/_ASDisplayView.mm +++ b/AsyncDisplayKit/Details/_ASDisplayView.mm @@ -243,6 +243,14 @@ [_node tintColorDidChange]; } +- (BOOL)canBecomeFirstResponder { + return [_node canBecomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder { + return [_node canResignFirstResponder]; +} + - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { // We forward responder-chain actions to our node if we can't handle them ourselves. See -targetForAction:withSender:. From 540eeec79b8b3646a0675bd7da9508a263260867 Mon Sep 17 00:00:00 2001 From: Jack Flintermann Date: Sat, 27 Jun 2015 01:48:28 -0400 Subject: [PATCH 2/9] move UIresponder methods out of UIView bridging category --- AsyncDisplayKit/ASDisplayNode.h | 19 ++++++++++--------- AsyncDisplayKit/ASDisplayNode.mm | 23 ++++++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 17ec215cee..fbb97c53f5 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -432,6 +432,16 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); */ - (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node; +/** @name UIResponder methods */ + +// By default these fall through to the underlying view, but can be overridden. +- (BOOL)canBecomeFirstResponder; // default==NO +- (BOOL)becomeFirstResponder; // default==NO (no-op) +- (BOOL)canResignFirstResponder; // default==YES +- (BOOL)resignFirstResponder; // default==NO (no-op) +- (BOOL)isFirstResponder; +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender; + @end @@ -468,15 +478,6 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); - (void)setNeedsDisplay; // Marks the view as needing display. Convenience for use whether view is created or not, or from a background thread. - (void)setNeedsLayout; // Marks the view as needing layout. Convenience for use whether view is created or not, or from a background thread. -// UIResponder methods -// By default these fall through to the underlying view, but can be overridden. -- (BOOL)canBecomeFirstResponder; // default==NO -- (BOOL)becomeFirstResponder; // default==NO (no-op) -- (BOOL)canResignFirstResponder; // default==YES -- (BOOL)resignFirstResponder; // default==YES (no-op) -- (BOOL)isFirstResponder; -- (BOOL)canPerformAction:(SEL)action withSender:(id)sender; - @property (atomic, retain) id contents; // default=nil @property (atomic, assign) BOOL clipsToBounds; // default==NO @property (atomic, getter=isOpaque) BOOL opaque; // default==YES diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 10a173d84a..c3cea9454d 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1701,24 +1701,29 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, return NO; } -- (BOOL)becomeFirstResponder { - return [self.view becomeFirstResponder]; -} - - (BOOL)canResignFirstResponder { return YES; } -- (BOOL)resignFirstResponder { - return [self.view resignFirstResponder]; +- (BOOL)isFirstResponder { + ASDisplayNodeAssertMainThread(); + return _view != nil && [_view isFirstResponder]; } -- (BOOL)isFirstResponder { - return [self.view isFirstResponder]; +// Note: this implicitly loads the view if it hasn't been loaded yet. +- (BOOL)becomeFirstResponder { + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self canBecomeFirstResponder] && [self.view becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder { + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self canResignFirstResponder] && [_view resignFirstResponder]; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { - return [self.view canPerformAction:action withSender:sender]; + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self.view canPerformAction:action withSender:sender]; } @end From 8a200078bd6a3a096b8c876972b417d040f54c8a Mon Sep 17 00:00:00 2001 From: Jack Flintermann Date: Sat, 27 Jun 2015 01:48:55 -0400 Subject: [PATCH 3/9] make ASEditableTextField properly subclass responder methods --- AsyncDisplayKit/ASEditableTextNode.h | 4 ++-- AsyncDisplayKit/ASEditableTextNode.mm | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/AsyncDisplayKit/ASEditableTextNode.h b/AsyncDisplayKit/ASEditableTextNode.h index 11cfaf8405..92811e2457 100644 --- a/AsyncDisplayKit/ASEditableTextNode.h +++ b/AsyncDisplayKit/ASEditableTextNode.h @@ -57,10 +57,10 @@ - (BOOL)isFirstResponder; //! @abstract Makes the receiver's text view the first responder. -- (void)becomeFirstResponder; +- (BOOL)becomeFirstResponder; //! @abstract Resigns the receiver's text view from first-responder status, if it has it. -- (void)resignFirstResponder; +- (BOOL)resignFirstResponder; #pragma mark - Geometry /** diff --git a/AsyncDisplayKit/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index 0b757d1752..b0d2fa1852 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -352,16 +352,26 @@ return [_textKitComponents.textView isFirstResponder]; } -- (void)becomeFirstResponder -{ - ASDN::MutexLocker l(_textKitLock); - [_textKitComponents.textView becomeFirstResponder]; +- (BOOL)canBecomeFirstResponder { + ASDN::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canBecomeFirstResponder]; } -- (void)resignFirstResponder +- (BOOL)becomeFirstResponder { ASDN::MutexLocker l(_textKitLock); - [_textKitComponents.textView resignFirstResponder]; + return [_textKitComponents.textView becomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder { + ASDN::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canResignFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + ASDN::MutexLocker l(_textKitLock); + return [_textKitComponents.textView resignFirstResponder]; } #pragma mark - UITextView Delegate From 5889c7019e85fca7f18686f0e9e2a23d4755d930 Mon Sep 17 00:00:00 2001 From: Jack Flintermann Date: Sat, 27 Jun 2015 02:37:18 -0400 Subject: [PATCH 4/9] basic unit tests --- AsyncDisplayKitTests/ASDisplayNodeTests.m | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index fb40acadef..4ce0c49bcf 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -71,6 +71,9 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @property (atomic, copy) CGSize(^calculateSizeBlock)(ASTestDisplayNode *node, CGSize size); @end +@interface ASTestResponderNode : ASTestDisplayNode +@end + @implementation ASTestDisplayNode - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize @@ -91,9 +94,57 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @interface UIDisplayNodeTestView : UIView @end +@interface UIResponderNodeTestView : _ASDisplayView +@property(nonatomic) BOOL isFirstResponder; +@end + @implementation UIDisplayNodeTestView @end +@interface ASTestWindow : UIWindow +@end + +@implementation ASTestWindow + +- (id)firstResponder { + return self.subviews.firstObject; +} + +@end + +@implementation ASTestResponderNode + ++ (Class)viewClass { + return [UIResponderNodeTestView class]; +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +@end + +@implementation UIResponderNodeTestView + +- (BOOL)becomeFirstResponder { + self.isFirstResponder = YES; + return YES; +} + +- (BOOL)canResignFirstResponder { + return YES; +} + +- (BOOL)resignFirstResponder { + if (self.isFirstResponder) { + self.isFirstResponder = NO; + return YES; + } + return NO; +} + +@end + @interface ASDisplayNodeTests : XCTestCase @end @@ -102,6 +153,25 @@ for (ASDisplayNode *n in @[ nodes ]) {\ dispatch_queue_t queue; } +- (void)testOverriddenFirstResponderBehavior { + ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); +} + +- (void)testDefaultFirstResponderBehavior { + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + XCTAssertFalse([node canBecomeFirstResponder]); + XCTAssertFalse([node becomeFirstResponder]); +} + +- (void)testLayerBackedFirstResponderBehavior { + ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; + node.layerBacked = YES; + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertFalse([node becomeFirstResponder]); +} + - (void)setUp { [super setUp]; From 57465c7fd346904e5251e5e0376c73ed471d948f Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 28 Jun 2015 18:03:45 -0700 Subject: [PATCH 5/9] Overhaul ASDataController and extensively test ASTableView. This diff resolves all known consistency issues with ASTableView and ASCollectionView. It includes significantly more aggressive thrash-testing in ASTableViewStressTest, which now passes on a variety of device and simulator configurations. It also updates the unit tests run on every commit to ensure any regression is caught quickly. A few of the salient changes in this diff: - ASTableView now uses Rene's ASCollectionViewLayoutController, and actually uses a UICollectionViewFlowLayout without any UICollectionView. This resolves an issue where ASFlowLayoutController was generating slightly out-of-bounds indicies when programmatically scrolling past the end of the table content. Because the custom implementation is likely faster, I will revisit this later with profiling and possibly returning to the custom impl. - There is now a second copy of the _nodes array maintained by ASDataController. It shares the same node instances, but this does add some overhead to manipulating the arrays. I've filed a task to follow up with optimization, as there are several great opportunities to make it faster. However, I don't believe the overhead is a significant issue, and it does guarantee correctness in even the toughest app usage scenarios. - ASDataController no longer supports calling its delegate /before/ edit operations. No other class was relying on this behavior, and it would be unusual for an app developer to use ASDataController directly. However, it is possible that someone with a custom view that integrates with ASDataController and ASRangeController could be affected by this. - Further cleanup of organization, naming, additional comments, reduced code length wherever possible. Overall, significantly more accessible to a new reader. --- AsyncDisplayKit/ASTableView.mm | 23 +- .../ASCollectionViewLayoutController.h | 1 + .../ASCollectionViewLayoutController.mm | 64 +- AsyncDisplayKit/Details/ASDataController.h | 12 +- AsyncDisplayKit/Details/ASDataController.mm | 554 +++++++++--------- .../Details/ASFlowLayoutController.mm | 4 +- .../Details/ASMultidimensionalArrayUtils.h | 2 +- AsyncDisplayKit/Details/ASRangeController.h | 22 - AsyncDisplayKit/Details/ASRangeController.mm | 35 +- AsyncDisplayKitTests/ASTableViewTests.m | 61 +- .../xcshareddata/xcschemes/Sample.xcscheme | 2 +- .../Sample/ViewController.m | 110 +++- 12 files changed, 479 insertions(+), 411 deletions(-) diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 2085b6f543..12fa595119 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -10,7 +10,7 @@ #import "ASAssert.h" #import "ASDataController.h" -#import "ASFlowLayoutController.h" +#import "ASCollectionViewLayoutController.h" #import "ASLayoutController.h" #import "ASRangeController.h" #import "ASDisplayNodeInternal.h" @@ -117,7 +117,7 @@ static BOOL _isInterceptedSelector(SEL sel) _ASTableViewProxy *_proxyDelegate; ASDataController *_dataController; - ASFlowLayoutController *_layoutController; + ASCollectionViewLayoutController *_layoutController; ASRangeController *_rangeController; @@ -159,7 +159,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (void)configureWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled { - _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + _layoutController = [[ASCollectionViewLayoutController alloc] initWithScrollView:self collectionViewLayout:flowLayout]; _rangeController = [[ASRangeController alloc] init]; _rangeController.layoutController = _layoutController; @@ -412,20 +413,12 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { { CGPoint scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; ASScrollDirection direction = ASScrollDirectionNone; - if (_layoutController.layoutDirection == ASFlowLayoutDirectionHorizontal) { - if (scrollVelocity.x > 0) { - direction = ASScrollDirectionRight; - } else if (scrollVelocity.x < 0) { - direction = ASScrollDirectionLeft; - } + if (scrollVelocity.y > 0) { + direction = ASScrollDirectionDown; } else { - if (scrollVelocity.y > 0) { - direction = ASScrollDirectionDown; - } else { - direction = ASScrollDirectionUp; - } + direction = ASScrollDirectionUp; } - + return direction; } diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h index 9aa25db0a9..3c33ae189a 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h @@ -14,5 +14,6 @@ @interface ASCollectionViewLayoutController : ASAbstractLayoutController - (instancetype)initWithCollectionView:(ASCollectionView *)collectionView; +- (instancetype)initWithScrollView:(UIScrollView *)scrollView collectionViewLayout:(UICollectionViewLayout *)layout; @end diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm index 15240b3c87..c83a0ac2bd 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm @@ -21,7 +21,8 @@ struct ASDirectionalScreenfulBuffer { typedef struct ASDirectionalScreenfulBuffer ASDirectionalScreenfulBuffer; ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDirection scrollDirection, - ASRangeTuningParameters rangeTuningParameters) { + ASRangeTuningParameters rangeTuningParameters) +{ ASDirectionalScreenfulBuffer horizontalBuffer = {0, 0}; BOOL movingRight = ASScrollDirectionContainsRight(scrollDirection); horizontalBuffer.positiveDirection = movingRight ? rangeTuningParameters.leadingBufferScreenfuls : @@ -32,7 +33,8 @@ ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDire } ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferVertical(ASScrollDirection scrollDirection, - ASRangeTuningParameters rangeTuningParameters) { + ASRangeTuningParameters rangeTuningParameters) +{ ASDirectionalScreenfulBuffer verticalBuffer = {0, 0}; BOOL movingDown = ASScrollDirectionContainsDown(scrollDirection); verticalBuffer.positiveDirection = movingDown ? rangeTuningParameters.leadingBufferScreenfuls : @@ -52,44 +54,64 @@ typedef struct ASRangeGeometry ASRangeGeometry; #pragma mark - #pragma mark ASCollectionViewLayoutController -@interface ASCollectionViewLayoutController () { - ASCollectionView * __weak _collectionView; +@interface ASCollectionViewLayoutController () +{ + UIScrollView * __weak _scrollView; + UICollectionViewLayout * __strong _collectionViewLayout; std::vector _updateRangeBoundsIndexedByRangeType; + ASScrollDirection _scrollableDirections; } @end @implementation ASCollectionViewLayoutController -- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView { +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView +{ if (!(self = [super init])) { return nil; } - _collectionView = collectionView; + + _scrollableDirections = [collectionView scrollableDirections]; + _scrollView = collectionView; + _collectionViewLayout = [collectionView collectionViewLayout]; _updateRangeBoundsIndexedByRangeType = std::vector(ASLayoutRangeTypeCount); return self; } +- (instancetype)initWithScrollView:(UIScrollView *)scrollView collectionViewLayout:(UICollectionViewLayout *)layout +{ + if (!(self = [super init])) { + return nil; + } + + _scrollableDirections = ASScrollDirectionVerticalDirections; + _scrollView = scrollView; + _collectionViewLayout = layout; + _updateRangeBoundsIndexedByRangeType = std::vector(ASLayoutRangeTypeCount); + return self; +} + + #pragma mark - #pragma mark Index Paths in Range - (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection viewportSize:(CGSize)viewportSize - rangeType:(ASLayoutRangeType)rangeType { + rangeType:(ASLayoutRangeType)rangeType +{ ASRangeGeometry rangeGeometry = [self rangeGeometryWithScrollDirection:scrollDirection - collectionView:_collectionView rangeTuningParameters:[self tuningParametersForRangeType:rangeType]]; _updateRangeBoundsIndexedByRangeType[rangeType] = rangeGeometry.updateBounds; - return [self indexPathsForItemsWithinRangeBounds:rangeGeometry.rangeBounds collectionView:_collectionView]; + return [self indexPathsForItemsWithinRangeBounds:rangeGeometry.rangeBounds]; } - (ASRangeGeometry)rangeGeometryWithScrollDirection:(ASScrollDirection)scrollDirection - collectionView:(ASCollectionView *)collectionView - rangeTuningParameters:(ASRangeTuningParameters)rangeTuningParameters { - CGRect rangeBounds = collectionView.bounds; - CGRect updateBounds = collectionView.bounds; - ASScrollDirection scrollableDirections = [collectionView scrollableDirections]; + rangeTuningParameters:(ASRangeTuningParameters)rangeTuningParameters +{ + CGRect rangeBounds = _scrollView.bounds; + CGRect updateBounds = _scrollView.bounds; - BOOL canScrollHorizontally = ASScrollDirectionContainsHorizontalDirection(scrollableDirections); + BOOL canScrollHorizontally = ASScrollDirectionContainsHorizontalDirection(_scrollableDirections); if (canScrollHorizontally) { ASDirectionalScreenfulBuffer horizontalBuffer = ASDirectionalScreenfulBufferHorizontal(scrollDirection, rangeTuningParameters); @@ -102,7 +124,7 @@ typedef struct ASRangeGeometry ASRangeGeometry; MIN(horizontalBuffer.positiveDirection * 0.5, 0.95)); } - BOOL canScrollVertically = ASScrollDirectionContainsVerticalDirection(scrollableDirections); + BOOL canScrollVertically = ASScrollDirectionContainsVerticalDirection(_scrollableDirections); if (canScrollVertically) { ASDirectionalScreenfulBuffer verticalBuffer = ASDirectionalScreenfulBufferVertical(scrollDirection, rangeTuningParameters); @@ -118,9 +140,10 @@ typedef struct ASRangeGeometry ASRangeGeometry; return {rangeBounds, updateBounds}; } -- (NSSet *)indexPathsForItemsWithinRangeBounds:(CGRect)rangeBounds collectionView:(ASCollectionView *)collectionView { +- (NSSet *)indexPathsForItemsWithinRangeBounds:(CGRect)rangeBounds +{ NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; - NSArray *layoutAttributes = [collectionView.collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; + NSArray *layoutAttributes = [_collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; for (UICollectionViewLayoutAttributes *la in layoutAttributes) { [indexPathSet addObject:la.indexPath]; } @@ -132,13 +155,14 @@ typedef struct ASRangeGeometry ASRangeGeometry; - (BOOL)shouldUpdateForVisibleIndexPaths:(NSArray *)indexPaths viewportSize:(CGSize)viewportSize - rangeType:(ASLayoutRangeType)rangeType { + rangeType:(ASLayoutRangeType)rangeType +{ CGRect updateRangeBounds = _updateRangeBoundsIndexedByRangeType[rangeType]; if (CGRectIsEmpty(updateRangeBounds)) { return YES; } - CGRect currentBounds = _collectionView.bounds; + CGRect currentBounds = _scrollView.bounds; if (CGRectIsEmpty(currentBounds)) { currentBounds = CGRectMake(0, 0, viewportSize.width, viewportSize.height); } diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index a9aa7e55ac..3254655fcb 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -70,25 +70,21 @@ typedef NSUInteger ASDataControllerAnimationOptions; /** Called for insertion of elements. */ -- (void)dataController:(ASDataController *)dataController willInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** Called for deletion of elements. */ -- (void)dataController:(ASDataController *)dataController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** Called for insertion of sections. */ -- (void)dataController:(ASDataController *)dataController willInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** Called for deletion of sections. */ -- (void)dataController:(ASDataController *)dataController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; @end @@ -117,7 +113,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; * Designated iniailizer. * * @param asyncDataFetchingEnabled Enable the data fetching in async mode. - + * * @discussion If enabled, we will fetch data through `dataController:nodeAtIndexPath:` and `dataController:rowsInSection:` in background thread. * Otherwise, the methods will be invoked synchronically in calling thread. Enabling data fetching in async mode could avoid blocking main thread * while allocating cell on main thread, which is frequently reported issue for handling large scale data. On another hand, the application code @@ -127,7 +123,11 @@ typedef NSUInteger ASDataControllerAnimationOptions; */ - (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled; -/** @name Initial loading */ +/** @name Initial loading + * + * @discussion This method allows choosing an animation style for the first load of content. It is typically used just once, + * for example in viewWillAppear:, to specify an animation option for the information already present in the asyncDataSource. + */ - (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index bfb56d3b67..25b6518e21 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -21,16 +21,16 @@ const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; static void *kASSizingQueueContext = &kASSizingQueueContext; @interface ASDataController () { - NSMutableArray *_nodes; - NSMutableArray *_pendingBlocks; + NSMutableArray *_completedNodes; // Main thread only. External data access can immediately query this. + NSMutableArray *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes. + + NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking. + NSOperationQueue *_editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. + BOOL _asyncDataFetchingEnabled; - BOOL _delegateWillInsertNodes; BOOL _delegateDidInsertNodes; - BOOL _delegateWillDeleteNodes; BOOL _delegateDidDeleteNodes; - BOOL _delegateWillInsertSections; BOOL _delegateDidInsertSections; - BOOL _delegateWillDeleteSections; BOOL _delegateDidDeleteSections; } @@ -48,8 +48,16 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return nil; } - _nodes = [NSMutableArray array]; - _pendingBlocks = [NSMutableArray array]; + _completedNodes = [NSMutableArray array]; + _editingNodes = [NSMutableArray array]; + + _pendingEditCommandBlocks = [NSMutableArray array]; + + _editingTransactionQueue = [[NSOperationQueue alloc] init]; + _editingTransactionQueue.qualityOfService = NSQualityOfServiceUserInitiated; + _editingTransactionQueue.maxConcurrentOperationCount = 1; // Serial queue + _editingTransactionQueue.name = @"org.AsyncDisplayKit.ASDataController.editingTransactionQueue"; + _batchUpdateCounter = 0; _asyncDataFetchingEnabled = asyncDataFetchingEnabled; @@ -65,18 +73,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; _delegate = delegate; // Interrogate our delegate to understand its capabilities, optimizing away expensive respondsToSelector: calls later. - _delegateWillInsertNodes = [_delegate respondsToSelector:@selector(dataController:willInsertNodes:atIndexPaths:withAnimationOptions:)]; _delegateDidInsertNodes = [_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:withAnimationOptions:)]; - _delegateWillDeleteNodes = [_delegate respondsToSelector:@selector(dataController:willDeleteNodesAtIndexPaths:withAnimationOptions:)]; _delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:withAnimationOptions:)]; - _delegateWillInsertSections = [_delegate respondsToSelector:@selector(dataController:willInsertSections:atIndexSet:withAnimationOptions:)]; _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)]; - _delegateWillDeleteSections = [_delegate respondsToSelector:@selector(dataController:willDeleteSectionsAtIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; } -#pragma mark - Queue Management - + (NSUInteger)parallelProcessorCount { static NSUInteger parallelProcessorCount; @@ -89,64 +91,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return parallelProcessorCount; } -+ (dispatch_queue_t)sizingQueue -{ - static dispatch_queue_t sizingQueue = NULL; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sizingQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDataController.sizingQueue", DISPATCH_QUEUE_SERIAL); - dispatch_queue_set_specific(sizingQueue, kASSizingQueueContext, kASSizingQueueContext, NULL); - dispatch_set_target_queue(sizingQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); - }); - - return sizingQueue; -} - -+ (BOOL)executingOnSizingQueue -{ - return kASSizingQueueContext == dispatch_get_specific(kASSizingQueueContext); -} - -- (void)asyncUpdateDataWithBlock:(dispatch_block_t)block { - dispatch_async(dispatch_get_main_queue(), ^{ - if (_batchUpdateCounter) { - [_pendingBlocks addObject:block]; - } else { - block(); - } - }); -} - -- (void)syncUpdateDataWithBlock:(dispatch_block_t)block { - dispatch_sync(dispatch_get_main_queue(), ^{ - if (_batchUpdateCounter) { - [_pendingBlocks addObject:block]; - } else { - block(); - } - }); -} - -- (void)accessDataSourceWithBlock:(dispatch_block_t)block { - if (_asyncDataFetchingEnabled) { - [_dataSource dataControllerLockDataSource]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - block(); - [_dataSource dataControllerUnlockDataSource]; - }); - } else { - [_dataSource dataControllerLockDataSource]; - block(); - [_dataSource dataControllerUnlockDataSource]; - } -} - #pragma mark - Cell Layout -- (void)_layoutNodes:(NSArray *)nodes - atIndexPaths:(NSArray *)indexPaths -withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)_layoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"Cell node layout must be initiated from edit transaction queue"); + if (!nodes.count) { return; } @@ -172,25 +122,14 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions }); } - dispatch_block_t block = ^{ - dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); - - [self asyncUpdateDataWithBlock:^{ - // Insert finished nodes into data storage - [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; - }; + // Block the _editingTransactionQueue from executing a new edit transaction until layout is done & _editingNodes array is updated. + dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); - if ([ASDataController executingOnSizingQueue]) { - block(); - } else { - dispatch_async([ASDataController sizingQueue], block); - } + // Insert finished nodes into data storage + [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; } -- (void)_batchLayoutNodes:(NSArray *)nodes - atIndexPaths:(NSArray *)indexPaths - withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; @@ -204,132 +143,187 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions } } -#pragma mark - Internal Data Editing +#pragma mark - Internal Data Querying + Editing - (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { if (indexPaths.count == 0) return; - if (_delegateWillInsertNodes) - [_delegate dataController:self willInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_nodes, indexPaths, nodes); - if (_delegateDidInsertNodes) - [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths, nodes); + + // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. + NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes); + + ASDisplayNodePerformBlockOnMainThread(^{ + _completedNodes = completedNodes; + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }); } - (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { if (indexPaths.count == 0) return; - if (_delegateWillDeleteNodes) - [_delegate dataController:self willDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); - if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); + + ASDisplayNodePerformBlockOnMainThread(^{ + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); + if (_delegateDidDeleteNodes) + [_delegate dataController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }); } -- (void)_insertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { if (indexSet.count == 0) return; - if (_delegateWillInsertSections) - [_delegate dataController:self willInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; - [_nodes insertObjects:sections atIndexes:indexSet]; - if (_delegateDidInsertSections) - [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + [_editingNodes insertObjects:sections atIndexes:indexSet]; + + // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. + NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); + + ASDisplayNodePerformBlockOnMainThread(^{ + [_completedNodes insertObjects:sectionsForCompleted atIndexes:indexSet]; + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + }); } - (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { if (indexSet.count == 0) return; - if (_delegateWillDeleteSections) - [_delegate dataController:self willDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - [_nodes removeObjectsAtIndexes:indexSet]; - if (_delegateDidDeleteSections) - [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + [_editingNodes removeObjectsAtIndexes:indexSet]; + ASDisplayNodePerformBlockOnMainThread(^{ + [_completedNodes removeObjectsAtIndexes:indexSet]; + if (_delegateDidDeleteSections) + [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + }); } #pragma mark - Initial Load & Full Reload (External API) -- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - NSMutableArray *indexPaths = [NSMutableArray array]; - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; +- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [self accessDataSourceWithBlock:^{ + NSMutableArray *indexPaths = [NSMutableArray array]; + NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; - // insert sections - [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; + // insert sections + [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; - for (NSUInteger i = 0; i < sectionNum; i++) { - NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; + for (NSUInteger i = 0; i < sectionNum; i++) { + NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; - for (NSUInteger j = 0; j < rowNum; j++) { - [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; + for (NSUInteger j = 0; j < rowNum; j++) { + [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; + } } - } - - // insert elements - [self insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + // insert elements + [self insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; }]; } - (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^)())completion { - [self accessDataSourceWithBlock:^{ - // Fetching data in calling thread - NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; - - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; - for (NSUInteger i = 0; i < sectionNum; i++) { - NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [self accessDataSourceWithBlock:^{ + NSUInteger sectionCount = [_dataSource dataControllerNumberOfSections:self]; + NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedIndexPaths = [NSMutableArray array]; + [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; - for (NSUInteger j = 0; j < rowNum; j++) { - NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j]; - [updatedIndexPaths addObject:indexPath]; - [updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; - } - } - - dispatch_async([ASDataController sizingQueue], ^{ - [self syncUpdateDataWithBlock:^{ + [_editingTransactionQueue addOperationWithBlock:^{ // Remove everything that existed before the reload, now that we're ready to insert replacements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_nodes); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _nodes.count)]; - [self deleteSections:indexSet withAnimationOptions:animationOptions]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _editingNodes.count)]; + [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; // Insert each section - NSMutableArray *sections = [[NSMutableArray alloc] initWithCapacity:sectionNum]; - for (int i = 0; i < sectionNum; i++) { + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; + for (int i = 0; i < sectionCount; i++) { [sections addObject:[[NSMutableArray alloc] init]]; } - [self _insertSections:sections atIndexSet:[[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:animationOptions]; + [self _insertSections:sections atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] withAnimationOptions:animationOptions]; + + [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; + + if (completion) { + dispatch_async(dispatch_get_main_queue(), completion); + } }]; - - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; - - if (completion) { - dispatch_async(dispatch_get_main_queue(), completion); - } - }); + }]; }]; } +#pragma mark - Data Source Access (Calling _dataSource) + +- (void)accessDataSourceWithBlock:(dispatch_block_t)block +{ + if (_asyncDataFetchingEnabled) { + [_dataSource dataControllerLockDataSource]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + block(); + [_dataSource dataControllerUnlockDataSource]; + }); + } else { + [_dataSource dataControllerLockDataSource]; + block(); + [_dataSource dataControllerUnlockDataSource]; + } +} + +- (void)_populateFromDataSourceWithSectionIndexSet:(NSIndexSet *)indexSet mutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths +{ + [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:idx]; + + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = 0; i < rowNum; i++) { + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [indexPaths addObject:indexPath]; + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; + } + }]; +} + +- (void)_populateFromEntireDataSourceWithMutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths +{ + NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; + for (NSUInteger i = 0; i < sectionNum; i++) { + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; + + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; + for (NSUInteger j = 0; j < rowNum; j++) { + NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j]; + [indexPaths addObject:indexPath]; + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; + } + } +} + + #pragma mark - Batching (External API) - (void)beginUpdates { - dispatch_async([[self class] sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ - _batchUpdateCounter++; - }]; - }); + // Begin queuing up edit calls that happen on the main thread. + // This will prevent further operations from being scheduled on _editingTransactionQueue. + // It's fine if there is an in-flight operation on _editingTransactionQueue, + // as once the command queue is unpaused, each edit command will wait for the _editingTransactionQueue to be flushed. + _batchUpdateCounter++; } - (void)endUpdates @@ -339,119 +333,110 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)endUpdatesWithCompletion:(void (^)(BOOL))completion { - dispatch_async([[self class] sizingQueue], ^{ - dispatch_async(dispatch_get_main_queue(), ^{ - _batchUpdateCounter--; + _batchUpdateCounter--; - if (!_batchUpdateCounter) { - [_delegate dataControllerBeginUpdates:self]; - [_pendingBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { - block(); - }]; - [_pendingBlocks removeAllObjects]; - [_delegate dataControllerEndUpdates:self completion:completion]; - } - }); - }); + if (_batchUpdateCounter == 0) { + [_delegate dataControllerBeginUpdates:self]; + // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. + // Each subsequent command in the queue will also wait on the full asynchronous completion of the prior command's edit transaction. + [_pendingEditCommandBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { + block(); + }]; + [_pendingEditCommandBlocks removeAllObjects]; + + [_delegate dataControllerEndUpdates:self completion:completion]; + } +} + +- (void)performEditCommandWithBlock:(void (^)(void))block +{ + // This method needs to block the thread and synchronously perform the operation if we are not + // queuing commands for begin/endUpdates. If we are queuing, it needs to return immediately. + if (_batchUpdateCounter == 0) { + block(); + } else { + [_pendingEditCommandBlocks addObject:block]; + } } #pragma mark - Section Editing (External API) - (void)insertSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - __block int nodeTotalCnt = 0; - NSMutableArray *nodeCounts = [NSMutableArray arrayWithCapacity:indexSet.count]; - [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSUInteger cnt = [_dataSource dataController:self rowsInSection:idx]; - [nodeCounts addObject:@(cnt)]; - nodeTotalCnt += cnt; - }]; - - NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:nodeTotalCnt]; - NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:nodeTotalCnt]; - - __block NSUInteger idx = 0; - [indexSet enumerateIndexesUsingBlock:^(NSUInteger sectionIdx, BOOL *stop) { - NSUInteger cnt = [nodeCounts[idx++] unsignedIntegerValue]; - - for (int i = 0; i < cnt; i++) { - NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sectionIdx]; - [indexPaths addObject:indexPath]; - - ASCellNode *node = [_dataSource dataController:self nodeAtIndexPath:indexPath]; - [nodes addObject:node]; - } - }]; - - dispatch_async([[self class] sizingQueue], ^{ - [self syncUpdateDataWithBlock:^{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [self accessDataSourceWithBlock:^{ + NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedIndexPaths = [NSMutableArray array]; + [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + + [_editingTransactionQueue addOperationWithBlock:^{ NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; for (NSUInteger i = 0; i < indexSet.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } + [self _insertSections:sectionArray atIndexSet:indexSet withAnimationOptions:animationOptions]; + [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; - - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }); + }]; }]; } - (void)deleteSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - dispatch_async([[self class] sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [_editingTransactionQueue addOperationWithBlock:^{ // remove elements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, indexSet); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }]; - }); + }]; } - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - // We need to keep data query on data source in the calling thread. - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; - NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:idx]; + [self accessDataSourceWithBlock:^{ + NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedIndexPaths = [NSMutableArray array]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; - NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; - for (NSUInteger i = 0; i < rowNum; i++) { - NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; - [updatedIndexPaths addObject:indexPath]; - [updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; - } - }]; - - // Dispatch to sizing queue in order to guarantee that any in-progress sizing operations from prior edits have completed. - // For example, if an initial -reloadData call is quickly followed by -reloadSections, sizing the initial set may not be done - // at this time. Thus _nodes could be empty and crash in ASIndexPathsForMultidimensional[...] - dispatch_async([ASDataController sizingQueue], ^{ - [self syncUpdateDataWithBlock:^{ - // remove elements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, sections); + // Dispatch to sizing queue in order to guarantee that any in-progress sizing operations from prior edits have completed. + // For example, if an initial -reloadData call is quickly followed by -reloadSections, sizing the initial set may not be done + // at this time. Thus _editingNodes could be empty and crash in ASIndexPathsForMultidimensional[...] + + [_editingTransactionQueue addOperationWithBlock:^{ + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + + // reinsert the elements + [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; - - // reinsert the elements - [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; - }); + }]; }]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - dispatch_async([ASDataController sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [_editingTransactionQueue addOperationWithBlock:^{ // remove elements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; // update the section of indexpaths @@ -464,62 +449,77 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions // Don't re-calculate size for moving [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; - }); + }]; } #pragma mark - Row Editing (External API) - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - // sort indexPath to avoid messing up the index when inserting in several batches - NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - for (NSUInteger i = 0; i < sortedIndexPaths.count; i++) { - [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:sortedIndexPaths[i]]]; - } - - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [self accessDataSourceWithBlock:^{ + // sort indexPath to avoid messing up the index when inserting in several batches + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + for (NSUInteger i = 0; i < sortedIndexPaths.count; i++) { + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:sortedIndexPaths[i]]]; + } + + [_editingTransactionQueue addOperationWithBlock:^{ + [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; + }]; }]; } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - // sort indexPath in order to avoid messing up the index when deleting - NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + // sort indexPath in order to avoid messing up the index when deleting + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - dispatch_async([ASDataController sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ + [_editingTransactionQueue addOperationWithBlock:^{ [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; }]; - }); + }]; } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. - [self accessDataSourceWithBlock:^{ - NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; - }]; - - dispatch_async([ASDataController sizingQueue], ^{ - [self syncUpdateDataWithBlock:^{ - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. + [self accessDataSourceWithBlock:^{ + NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; }]; - - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }); + + [_editingTransactionQueue addOperationWithBlock:^{ + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; + }]; }]; } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - dispatch_async([ASDataController sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [NSArray arrayWithObject:indexPath]); + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [_editingTransactionQueue addOperationWithBlock:^{ + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]); NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -527,7 +527,7 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions NSArray *newIndexPaths = [NSArray arrayWithObject:newIndexPath]; [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; }]; - }); + }]; } #pragma mark - Data Querying (External API) @@ -535,19 +535,19 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (NSUInteger)numberOfSections { ASDisplayNodeAssertMainThread(); - return [_nodes count]; + return [_completedNodes count]; } - (NSUInteger)numberOfRowsInSection:(NSUInteger)section { ASDisplayNodeAssertMainThread(); - return [_nodes[section] count]; + return [_completedNodes[section] count]; } - (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); - return _nodes[indexPath.section][indexPath.row]; + return _completedNodes[indexPath.section][indexPath.row]; } - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths @@ -557,19 +557,19 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions // Make sure that any asynchronous layout operations have finished so that those nodes are present. // Otherwise a failure case could be: // - Reload section 2, deleting all current nodes in that section. - // - New nodes are created and sizing is triggered, but they are not yet added to _nodes. + // - New nodes are created and sizing is triggered, but they are not yet added to _completedNodes. // - This method is called and includes an indexPath in section 2. - // - Unless we wait for the layout group to finish, we will crash with array out of bounds looking for the index in _nodes. - // FIXME: Seralization is required here. Diff in progress to resolve. - - return ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]); + // - Unless we wait for the layout group to finish, we will crash with array out of bounds looking for the index in _completedNodes. + + return ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]); } #pragma mark - Dealloc -- (void)dealloc { +- (void)dealloc +{ ASDisplayNodeAssertMainThread(); - [_nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { + [_completedNodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { if (node.isNodeLoaded) { if (node.layerBacked) { diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/AsyncDisplayKit/Details/ASFlowLayoutController.mm index bb5370d3bb..5bac784679 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.mm +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -77,8 +77,7 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; } - (void)deleteSectionsAtIndexSet:(NSIndexSet *)indexSet { - [indexSet enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) - { + [indexSet enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) { _nodeSizes.erase(_nodeSizes.begin() +idx); }]; } @@ -148,6 +147,7 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; [indexPathSet addObject:[NSIndexPath indexPathForRow:startIter.second inSection:startIter.first]]; startIter.second++; + // Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized. while (startIter.second == _nodeSizes[startIter.first].size() && startIter.first < _nodeSizes.size()) { startIter.second = 0; startIter.first++; diff --git a/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h index b7fbbd8c3f..d5058a8622 100644 --- a/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h +++ b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h @@ -44,7 +44,7 @@ extern NSArray *ASFindElementsInMultidimensionalArrayAtIndexPaths(NSMutableArray extern NSArray *ASIndexPathsForMultidimensionalArrayAtIndexSet(NSArray *MultidimensionalArray, NSIndexSet *indexSet); /** - * Reteurn all the index paths of mutable multidimensional array, in ascending order. + * Return all the index paths of mutable multidimensional array, in ascending order. */ extern NSArray *ASIndexPathsForMultidimensionalArray(NSArray *MultidimensionalArray); diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index f59b151f2e..cdfb0568b4 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -107,26 +107,4 @@ */ - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; -@optional - -/** - * Called before nodes insertion. - */ -- (void)rangeController:(ASRangeController *)rangeController willInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - -/** - * Called before nodes deletion. - */ -- (void)rangeController:(ASRangeController *)rangeController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - -/** - * Called before section insertion. - */ -- (void)rangeController:(ASRangeController *)rangeController willInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - -/** - * Called before section deletion. - */ -- (void)rangeController:(ASRangeController *)rangeController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - @end diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 2ac2593da2..1a6afed732 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -111,6 +111,7 @@ NSMutableSet *removedIndexPaths = _rangeIsValid ? [[_rangeTypeIndexPaths objectForKey:rangeKey] mutableCopy] : [NSMutableSet set]; [removedIndexPaths minusSet:indexPaths]; [removedIndexPaths minusSet:visibleNodePathsSet]; + if (removedIndexPaths.count) { NSArray *removedNodes = [_delegate rangeController:self nodesAtIndexPaths:[removedIndexPaths allObjects]]; [removedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { @@ -129,7 +130,7 @@ if ([self shouldSkipVisibleNodesForRangeType:rangeType]) { [addedIndexPaths minusSet:visibleNodePathsSet]; } - + if (addedIndexPaths.count) { NSArray *addedNodes = [_delegate rangeController:self nodesAtIndexPaths:[addedIndexPaths allObjects]]; [addedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { @@ -181,14 +182,6 @@ }); } -- (void)dataController:(ASDataController *)dataController willInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - ASDisplayNodePerformBlockOnMainThread(^{ - if ([_delegate respondsToSelector:@selector(rangeController:willInsertNodesAtIndexPaths:withAnimationOptions:)]) { - [_delegate rangeController:self willInsertNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - } - }); -} - - (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path"); @@ -206,14 +199,6 @@ }); } -- (void)dataController:(ASDataController *)dataController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - ASDisplayNodePerformBlockOnMainThread(^{ - if ([_delegate respondsToSelector:@selector(rangeController:willDeleteNodesAtIndexPaths:withAnimationOptions:)]) { - [_delegate rangeController:self willDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - } - }); -} - - (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodePerformBlockOnMainThread(^{ if ([_layoutController respondsToSelector:@selector(deleteNodesAtIndexPaths:)]) { @@ -224,14 +209,6 @@ }); } -- (void)dataController:(ASDataController *)dataController willInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - ASDisplayNodePerformBlockOnMainThread(^{ - if ([_delegate respondsToSelector:@selector(rangeController:willInsertSectionsAtIndexSet:withAnimationOptions:)]) { - [_delegate rangeController:self willInsertSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - } - }); -} - - (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections"); @@ -254,14 +231,6 @@ }); } -- (void)dataController:(ASDataController *)dataController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - ASDisplayNodePerformBlockOnMainThread(^{ - if ([_delegate respondsToSelector:@selector(rangeController:willDeleteSectionsAtIndexSet:withAnimationOptions:)]) { - [_delegate rangeController:self willDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - } - }); -} - - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodePerformBlockOnMainThread(^{ if ([_layoutController respondsToSelector:@selector(deleteSectionsAtIndexSet:)]) { diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index 84f7f29eee..08531ce131 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -113,6 +113,33 @@ XCTAssertTrue(tableViewDidDealloc, @"unexpected table view lifetime:%@", tableView); } +- (NSIndexSet *)randomIndexSet +{ + NSInteger randA = arc4random_uniform(NumberOfSections - 1); + NSInteger randB = arc4random_uniform(NumberOfSections - 1); + + return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))]; +} + +- (NSArray *)randomIndexPathsExisting:(BOOL)existing +{ + NSMutableArray *indexPaths = [NSMutableArray array]; + [[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSUInteger rowNum = NumberOfRowsPerSection; + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = (existing ? 0 : rowNum); i < (existing ? rowNum : rowNum * 2); i++) { + // Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows + if (existing && arc4random_uniform(2) == 0) { + continue; + } + + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [indexPaths addObject:indexPath]; + } + }]; + return indexPaths; +} + - (void)testReloadData { // Keep the viewport moderately sized so that new cells are loaded on scrolling @@ -127,22 +154,34 @@ [tableView reloadData]; - [tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1,2)] withRowAnimation:UITableViewRowAnimationNone]; - - // FIXME: Early return because we can't currently pass this test :). Diff is in progress to resolve. - return; - for (int i = 0; i < NumberOfReloadIterations; ++i) { - NSInteger randA = arc4random_uniform(NumberOfSections - 1); - NSInteger randB = arc4random_uniform(NumberOfSections - 1); + UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); - [tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))] withRowAnimation:UITableViewRowAnimationNone]; + BOOL animatedScroll = (arc4random_uniform(1) == 0 ? YES : NO); + BOOL reloadRowsInsteadOfSections = (arc4random_uniform(1) == 0 ? YES : NO); + BOOL letRunloopProceed = (arc4random_uniform(1) == 0 ? YES : NO); + BOOL useBeginEndUpdates = (arc4random_uniform(2) == 0 ? YES : NO); + + if (useBeginEndUpdates) { + [tableView beginUpdates]; + } - BOOL animated = (arc4random_uniform(1) == 0 ? YES : NO); + if (reloadRowsInsteadOfSections) { + [tableView reloadRowsAtIndexPaths:[self randomIndexPathsExisting:YES] withRowAnimation:rowAnimation]; + } else { + [tableView reloadSections:[self randomIndexSet] withRowAnimation:rowAnimation]; + } - [tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animated]; + [tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animatedScroll]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + if (letRunloopProceed) { + // Run other stuff on the main queue for between 2ms and 1000ms. + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1 / (1 + arc4random_uniform(500)))]]; + } + + if (useBeginEndUpdates) { + [tableView endUpdates]; + } } } diff --git a/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index 1e14aa0329..5c91bfc64d 100644 --- a/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme +++ b/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -1,6 +1,6 @@ { ASTableView *_tableView; + NSMutableArray *_sections; // Contains arrays of indexPaths representing rows } @end @@ -28,18 +29,24 @@ @implementation ViewController -#pragma mark - -#pragma mark UIViewController. - - (instancetype)init { if (!(self = [super init])) return nil; _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain asyncDataFetching:YES]; - _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator _tableView.asyncDataSource = self; _tableView.asyncDelegate = self; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + _sections = [NSMutableArray arrayWithCapacity:NumberOfSections]; + for (int i = 0; i < NumberOfSections; i++) { + NSMutableArray *rowsArray = [NSMutableArray arrayWithCapacity:NumberOfRowsPerSection]; + for (int j = 0; j < NumberOfRowsPerSection; j++) { + [rowsArray addObject:[NSIndexPath indexPathForRow:j inSection:i]]; + } + [_sections addObject:rowsArray]; + } return self; } @@ -63,42 +70,99 @@ [self thrashTableView]; } +- (NSIndexSet *)randomIndexSet +{ + u_int32_t upperBound = (u_int32_t)_sections.count - 1; + u_int32_t randA = arc4random_uniform(upperBound); + u_int32_t randB = arc4random_uniform(upperBound); + + return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))]; +} + +- (NSArray *)randomIndexPathsExisting:(BOOL)existing +{ + NSMutableArray *indexPaths = [NSMutableArray array]; + [[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSUInteger rowNum = [self tableView:_tableView numberOfRowsInSection:idx]; + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = (existing ? 0 : rowNum); i < (existing ? rowNum : rowNum * 2); i++) { + // Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows + if (existing && arc4random_uniform(2) == 0) { + continue; + } + + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [indexPaths addObject:indexPath]; + } + }]; + return indexPaths; +} + - (void)thrashTableView { - // Keep the viewport moderately sized so that new cells are loaded on scrolling - ASTableView *tableView = [[ASTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 500) - style:UITableViewStylePlain - asyncDataFetching:NO]; + _tableView.asyncDelegate = self; + _tableView.asyncDataSource = self; - tableView.asyncDelegate = self; - tableView.asyncDataSource = self; - - [tableView reloadData]; - - [tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1,2)] withRowAnimation:UITableViewRowAnimationNone]; + [_tableView reloadData]; + NSArray *indexPathsAddedAndRemoved = nil; + for (int i = 0; i < NumberOfReloadIterations; ++i) { - NSInteger randA = arc4random_uniform(NumberOfSections - 1); - NSInteger randB = arc4random_uniform(NumberOfSections - 1); + UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); + + BOOL animatedScroll = (arc4random_uniform(1) == 0 ? YES : NO); + BOOL reloadRowsInsteadOfSections = (arc4random_uniform(1) == 0 ? YES : NO); + BOOL letRunloopProceed = (arc4random_uniform(1) == 0 ? YES : NO); + BOOL addIndexPaths = (arc4random_uniform(1) == 0 ? YES : NO); + BOOL useBeginEndUpdates = (arc4random_uniform(2) == 0 ? YES : NO); - [tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))] withRowAnimation:UITableViewRowAnimationNone]; + if (useBeginEndUpdates) { + [_tableView beginUpdates]; + } - BOOL animated = (arc4random_uniform(1) == 0 ? YES : NO); + if (reloadRowsInsteadOfSections) { + [_tableView reloadRowsAtIndexPaths:[self randomIndexPathsExisting:YES] withRowAnimation:rowAnimation]; + } else { + [_tableView reloadSections:[self randomIndexSet] withRowAnimation:rowAnimation]; + } - [tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animated]; + if (addIndexPaths && !indexPathsAddedAndRemoved) { + indexPathsAddedAndRemoved = [self randomIndexPathsExisting:NO]; + for (NSIndexPath *indexPath in indexPathsAddedAndRemoved) { + [_sections[indexPath.section] addObject:indexPath]; + } + [_tableView insertRowsAtIndexPaths:indexPathsAddedAndRemoved withRowAnimation:rowAnimation]; + } - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + [_tableView setContentOffset:CGPointMake(0, arc4random_uniform(_tableView.contentSize.height - _tableView.bounds.size.height)) animated:animatedScroll]; + + if (letRunloopProceed) { + // Run other stuff on the main queue for between 2ms and 1000ms. + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1 / (1 + arc4random_uniform(500)))]]; + + if (indexPathsAddedAndRemoved) { + for (NSIndexPath *indexPath in indexPathsAddedAndRemoved) { + [_sections[indexPath.section] removeObjectIdenticalTo:indexPath]; + } + [_tableView deleteRowsAtIndexPaths:indexPathsAddedAndRemoved withRowAnimation:rowAnimation]; + indexPathsAddedAndRemoved = nil; + } + } + + if (useBeginEndUpdates) { + [_tableView endUpdates]; + } } } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return NumberOfSections; + return _sections.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return NumberOfRowsPerSection; + return [(NSArray *)[_sections objectAtIndex:section] count]; } - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath From 4963a887173a351963cb65c567fad19586fce4a7 Mon Sep 17 00:00:00 2001 From: Ethan Nagel Date: Mon, 29 Jun 2015 21:22:55 -0700 Subject: [PATCH 6/9] Propagate ASCellNode's clipsToBounds value to the __ASTableViewCell. This helps work around a bug we are seeing in some rare cases (selected background view overlaps other cells if size of ASCellNode has changed.) --- AsyncDisplayKit/ASCellNode.m | 1 + AsyncDisplayKit/ASTableView.mm | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index c9a6955c6d..f202a86f42 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -25,6 +25,7 @@ // use UITableViewCell defaults _selectionStyle = UITableViewCellSelectionStyleDefault; + self.clipsToBounds = YES; return self; } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 2085b6f543..e3b0b9ba53 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -389,6 +389,11 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { 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; } From 8fa092fb77d262235231d931ca8a438e6d55616a Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 4 Jul 2015 20:22:04 -0700 Subject: [PATCH 7/9] Complete overhaul of ASFlowLayoutController. Introduced ASIndexPath for efficient handling of index paths in C++ vectors, while maintaining the readability of ".section" and ".row" instead of ".first" and ".second" inside of complicated business logic. Confirmed that the working range calls are firing appropriately during ASTableViewStressTest, including the deallocation of the rich text placeholders provided by ASTextNode. --- AsyncDisplayKit/ASDisplayNode.mm | 1 + AsyncDisplayKit/ASTableView.mm | 9 +- .../ASCollectionViewLayoutController.h | 1 - .../ASCollectionViewLayoutController.mm | 14 -- AsyncDisplayKit/Details/ASDataController.h | 7 +- AsyncDisplayKit/Details/ASDataController.mm | 15 +- .../Details/ASFlowLayoutController.h | 10 +- .../Details/ASFlowLayoutController.mm | 238 +++++++++--------- AsyncDisplayKit/Details/ASIndexPath.h | 84 +++++++ AsyncDisplayKit/Details/ASRangeController.mm | 12 - 10 files changed, 224 insertions(+), 167 deletions(-) create mode 100644 AsyncDisplayKit/Details/ASIndexPath.h diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 212c5c76f1..15f0cfcc34 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1337,6 +1337,7 @@ static NSInteger incrementIfFound(NSInteger i) { { self.layer.contents = nil; _placeholderLayer.contents = nil; + _placeholderImage = nil; } - (void)recursivelyClearContents diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 12fa595119..6473eab376 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -117,7 +117,7 @@ static BOOL _isInterceptedSelector(SEL sel) _ASTableViewProxy *_proxyDelegate; ASDataController *_dataController; - ASCollectionViewLayoutController *_layoutController; + ASFlowLayoutController *_layoutController; ASRangeController *_rangeController; @@ -159,9 +159,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (void)configureWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled { - UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; - _layoutController = [[ASCollectionViewLayoutController alloc] initWithScrollView:self collectionViewLayout:flowLayout]; - + _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; + _rangeController = [[ASRangeController alloc] init]; _rangeController.layoutController = _layoutController; _rangeController.delegate = self; @@ -169,6 +168,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { _dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; _dataController.dataSource = self; _dataController.delegate = _rangeController; + + _layoutController.dataSource = _dataController; _asyncDataFetchingEnabled = asyncDataFetchingEnabled; _asyncDataSourceLocked = NO; diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h index 3c33ae189a..9aa25db0a9 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.h @@ -14,6 +14,5 @@ @interface ASCollectionViewLayoutController : ASAbstractLayoutController - (instancetype)initWithCollectionView:(ASCollectionView *)collectionView; -- (instancetype)initWithScrollView:(UIScrollView *)scrollView collectionViewLayout:(UICollectionViewLayout *)layout; @end diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm index c83a0ac2bd..8655102738 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm @@ -78,20 +78,6 @@ typedef struct ASRangeGeometry ASRangeGeometry; return self; } -- (instancetype)initWithScrollView:(UIScrollView *)scrollView collectionViewLayout:(UICollectionViewLayout *)layout -{ - if (!(self = [super init])) { - return nil; - } - - _scrollableDirections = ASScrollDirectionVerticalDirections; - _scrollView = scrollView; - _collectionViewLayout = layout; - _updateRangeBoundsIndexedByRangeType = std::vector(ASLayoutRangeTypeCount); - return self; -} - - #pragma mark - #pragma mark Index Paths in Range diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 3254655fcb..08dfd4fbf2 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -8,7 +8,7 @@ #import #import - +#import "ASFlowLayoutController.h" @class ASCellNode; @class ASDataController; @@ -97,7 +97,8 @@ typedef NSUInteger ASDataControllerAnimationOptions; * will be updated asynchronously. The dataSource must be updated to reflect the changes before these methods has been called. * For each data updatin, the corresponding methods in delegate will be called. */ -@interface ASDataController : ASDealloc2MainObject +@protocol ASFlowLayoutControllerDataSource; +@interface ASDataController : ASDealloc2MainObject /** Data source for fetching data info. @@ -167,4 +168,6 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths; +- (NSArray *)completedNodes; // This provides efficient access to the entire _completedNodes multidimensional array. + @end diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 25b6518e21..c3d1a916c6 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -54,7 +54,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; _pendingEditCommandBlocks = [NSMutableArray array]; _editingTransactionQueue = [[NSOperationQueue alloc] init]; - _editingTransactionQueue.qualityOfService = NSQualityOfServiceUserInitiated; _editingTransactionQueue.maxConcurrentOperationCount = 1; // Serial queue _editingTransactionQueue.name = @"org.AsyncDisplayKit.ASDataController.editingTransactionQueue"; @@ -553,17 +552,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); - - // Make sure that any asynchronous layout operations have finished so that those nodes are present. - // Otherwise a failure case could be: - // - Reload section 2, deleting all current nodes in that section. - // - New nodes are created and sizing is triggered, but they are not yet added to _completedNodes. - // - This method is called and includes an indexPath in section 2. - // - Unless we wait for the layout group to finish, we will crash with array out of bounds looking for the index in _completedNodes. - return ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]); } +- (NSArray *)completedNodes +{ + ASDisplayNodeAssertMainThread(); + return _completedNodes; +} + #pragma mark - Dealloc - (void)dealloc diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.h b/AsyncDisplayKit/Details/ASFlowLayoutController.h index 0c682594cb..01da06d124 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.h +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.h @@ -15,12 +15,20 @@ typedef NS_ENUM(NSUInteger, ASFlowLayoutDirection) { ASFlowLayoutDirectionHorizontal, }; +@protocol ASFlowLayoutControllerDataSource + +- (NSArray *)completedNodes; // This provides access to ASDataController's _completedNodes multidimensional array. + +@end + /** - * The controller for flow layout. + * An optimized flow layout controller that supports only vertical or horizontal scrolling, not simultaneously two-dimensional scrolling. + * It is used for all ASTableViews, and may be used with ASCollectionView. */ @interface ASFlowLayoutController : ASAbstractLayoutController @property (nonatomic, readonly, assign) ASFlowLayoutDirection layoutDirection; +@property (nonatomic) id dataSource; - (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection; diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/AsyncDisplayKit/Details/ASFlowLayoutController.mm index 5bac784679..52b9e6e880 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.mm +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -7,110 +7,66 @@ */ #import "ASFlowLayoutController.h" +#import "ASAssert.h" +#import "ASDisplayNode.h" +#import "ASIndexPath.h" #include #include #include -#import "ASAssert.h" - static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; -@interface ASFlowLayoutController() { - std::vector > _nodeSizes; - - std::pair _visibleRangeStartPos; - std::pair _visibleRangeEndPos; - - std::vector> _rangeStartPos; - std::vector> _rangeEndPos; +@interface ASFlowLayoutController() +{ + ASIndexPathRange _visibleRange; + std::vector _rangesByType; // All ASLayoutRangeTypes besides visible. } @end @implementation ASFlowLayoutController -- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection { +- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection +{ if (!(self = [super init])) { return nil; } - _layoutDirection = layoutDirection; - + _rangesByType = std::vector(ASLayoutRangeTypeCount); return self; } -#pragma mark - Editing - -- (void)insertNodesAtIndexPaths:(NSArray *)indexPaths withSizes:(NSArray *)nodeSizes -{ - ASDisplayNodeAssert(indexPaths.count == nodeSizes.count, @"Inconsistent index paths and node size"); - - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - std::vector &v = _nodeSizes[indexPath.section]; - v.insert(v.begin() + indexPath.row, [(NSValue *)nodeSizes[idx] CGSizeValue]); - }]; -} - -- (void)deleteNodesAtIndexPaths:(NSArray *)indexPaths -{ - [indexPaths enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - std::vector &v = _nodeSizes[indexPath.section]; - v.erase(v.begin() + indexPath.row); - }]; -} - -- (void)insertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet -{ - __block int cnt = 0; - [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSArray *nodes = sections[cnt++]; - std::vector v; - v.reserve(nodes.count); - - for (int i = 0; i < nodes.count; i++) { - v.push_back([nodes[i] CGSizeValue]); - } - - _nodeSizes.insert(_nodeSizes.begin() + idx, v); - }]; -} - -- (void)deleteSectionsAtIndexSet:(NSIndexSet *)indexSet { - [indexSet enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) { - _nodeSizes.erase(_nodeSizes.begin() +idx); - }]; -} - #pragma mark - Visible Indices - (BOOL)shouldUpdateForVisibleIndexPaths:(NSArray *)indexPaths viewportSize:(CGSize)viewportSize rangeType:(ASLayoutRangeType)rangeType { - if (!indexPaths.count) { + if (!indexPaths.count || rangeType >= _rangesByType.size()) { return NO; } - std::pair rangeStartPos, rangeEndPos; - - if (rangeType < _rangeStartPos.size() && rangeType < _rangeEndPos.size()) { - rangeStartPos = _rangeStartPos[rangeType]; - rangeEndPos = _rangeEndPos[rangeType]; - } - - std::pair startPos, endPos; - ASFindIndexPathRange(indexPaths, startPos, endPos); - - if (rangeStartPos >= startPos || rangeEndPos <= endPos) { + ASIndexPathRange existingRange = _rangesByType[rangeType]; + ASIndexPathRange newRange = [self indexPathRangeForIndexPaths:indexPaths]; + + ASIndexPath maximumStart = ASIndexPathMaximum(existingRange.start, newRange.start); + ASIndexPath minimumEnd = ASIndexPathMinimum(existingRange.end, newRange.end); + + if (ASIndexPathEqualToIndexPath(maximumStart, existingRange.start) || ASIndexPathEqualToIndexPath(minimumEnd, existingRange.end)) { return YES; } - return ASFlowLayoutDistance(startPos, _visibleRangeStartPos, _nodeSizes) > ASFlowLayoutDistance(_visibleRangeStartPos, rangeStartPos, _nodeSizes) * kASFlowLayoutControllerRefreshingThreshold || - ASFlowLayoutDistance(endPos, _visibleRangeEndPos, _nodeSizes) > ASFlowLayoutDistance(_visibleRangeEndPos, rangeEndPos, _nodeSizes) * kASFlowLayoutControllerRefreshingThreshold; + NSInteger newStartDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.start, newRange.start)]; + NSInteger existingStartDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.start, existingRange.start)] * kASFlowLayoutControllerRefreshingThreshold; + + NSInteger newEndDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.end, newRange.end)]; + NSInteger existingEndDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.end, existingRange.end)] * kASFlowLayoutControllerRefreshingThreshold; + + return (newStartDelta > existingStartDelta) || (newEndDelta > existingEndDelta); } - (void)setVisibleNodeIndexPaths:(NSArray *)indexPaths { - ASFindIndexPathRange(indexPaths, _visibleRangeStartPos, _visibleRangeEndPos); + _visibleRange = [self indexPathRangeForIndexPaths:indexPaths]; } /** @@ -138,100 +94,134 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; CGFloat backScreens = scrollDirection == leadingDirection ? tuningParameters.leadingBufferScreenfuls : tuningParameters.trailingBufferScreenfuls; CGFloat frontScreens = scrollDirection == leadingDirection ? tuningParameters.trailingBufferScreenfuls : tuningParameters.leadingBufferScreenfuls; - std::pair startIter = ASFindIndexForRange(_nodeSizes, _visibleRangeStartPos, - backScreens * viewportScreenMetric, _layoutDirection); - std::pair endIter = ASFindIndexForRange(_nodeSizes, _visibleRangeEndPos, frontScreens * viewportScreenMetric, _layoutDirection); + + ASIndexPath startPath = [self findIndexPathAtDistance:(-backScreens * viewportScreenMetric) fromIndexPath:_visibleRange.start]; + ASIndexPath endPath = [self findIndexPathAtDistance:(frontScreens * viewportScreenMetric) fromIndexPath:_visibleRange.end]; + ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never begin at a further position than endPath"); + NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; - while (startIter != endIter) { - [indexPathSet addObject:[NSIndexPath indexPathForRow:startIter.second inSection:startIter.first]]; - startIter.second++; + NSArray *completedNodes = [_dataSource completedNodes]; + + while (!ASIndexPathEqualToIndexPath(startPath, endPath)) { + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:startPath]]; + startPath.row++; // Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized. - while (startIter.second == _nodeSizes[startIter.first].size() && startIter.first < _nodeSizes.size()) { - startIter.second = 0; - startIter.first++; + while (startPath.row >= [(NSArray *)completedNodes[startPath.section] count] && startPath.section < completedNodes.count - 1) { + startPath.row = 0; + startPath.section++; + ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never reach a further section than endPath"); } } - [indexPathSet addObject:[NSIndexPath indexPathForRow:endIter.second inSection:endIter.first]]; + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:endPath]]; return indexPathSet; } #pragma mark - Utility -static void ASFindIndexPathRange(NSArray *indexPaths, std::pair &startPos, std::pair &endPos) - +- (ASIndexPathRange)indexPathRangeForIndexPaths:(NSArray *)indexPaths { - NSIndexPath *initialIndexPath = [indexPaths firstObject]; - startPos = endPos = {initialIndexPath.section, initialIndexPath.row}; + // Set up an initial value so the MIN and MAX can work in the enumeration. + __block ASIndexPath currentIndexPath = [[indexPaths firstObject] ASIndexPathValue]; + __block ASIndexPathRange range; + range.start = currentIndexPath; + range.end = currentIndexPath; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - std::pair p(indexPath.section, indexPath.row); - startPos = MIN(startPos, p); - endPos = MAX(endPos, p); + currentIndexPath = [indexPath ASIndexPathValue]; + range.start = ASIndexPathMinimum(range.start, currentIndexPath); + range.end = ASIndexPathMaximum(range.end, currentIndexPath); }]; + return range; } -static const std::pair ASFindIndexForRange(const std::vector> &nodes, - const std::pair &pos, - CGFloat range, - ASFlowLayoutDirection layoutDirection) +- (ASIndexPath)findIndexPathAtDistance:(CGFloat)distance fromIndexPath:(ASIndexPath)start { - std::pair cur = pos, pre = pos; + // "end" is the index path we'll advance until we have gone far enough from "start" to reach "distance" + ASIndexPath end = start; + // "previous" will store one iteration before "end", in case we go too far and need to reset "end" to be "previous" + ASIndexPath previous = start; - if (range < 0.0 && cur.first >= 0 && cur.first < nodes.size() && cur.second >= 0 && cur.second < nodes[cur.first].size()) { - // search backward - while (range < 0.0 && cur.first >= 0 && cur.second >= 0) { - pre = cur; - CGSize size = nodes[cur.first][cur.second]; - range += layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; - cur.second--; - while (cur.second < 0 && cur.first > 0) { - cur.second = (int)nodes[--cur.first].size() - 1; + NSArray *completedNodes = [_dataSource completedNodes]; + NSUInteger numberOfSections = [completedNodes count]; + NSUInteger numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; + + // If "distance" is negative, advance "end" backwards across rows and sections. + // Otherwise, advance forward. In either case, bring "distance" closer to zero by the dimension of each row passed. + if (distance < 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) { + while (distance < 0.0 && end.section >= 0 && end.row >= 0) { + previous = end; + ASDisplayNode *node = completedNodes[end.section][end.row]; + CGSize size = node.calculatedSize; + distance += (_layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height); + end.row--; + // If we've gone to a negative row, set to the last row of the previous section. While loop is required to handle empty sections. + while (end.row < 0 && end.section > 0) { + end.section--; + numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; + end.row = numberOfRowsInSection - 1; } } - if (cur.second < 0) { - cur = pre; + if (end.row < 0) { + end = previous; } } else { - // search forward - while (range > 0.0 && cur.first >= 0 && cur.first < nodes.size() && cur.second >= 0 && cur.second < nodes[cur.first].size()) { - pre = cur; - CGSize size = nodes[cur.first][cur.second]; - range -= layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; + while (distance > 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) { + previous = end; + ASDisplayNode *node = completedNodes[end.section][end.row]; + CGSize size = node.calculatedSize; + distance -= _layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; - cur.second++; - while (cur.second == nodes[cur.first].size() && cur.first < (int)nodes.size() - 1) { - cur.second = 0; - cur.first++; + end.row++; + // If we've gone beyond the section, reset to the beginning of the next section. While loop is required to handle empty sections. + while (end.row >= numberOfRowsInSection && end.section < numberOfSections - 1) { + end.row = 0; + end.section++; + numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; } } - if (cur.second == nodes[cur.first].size()) { - cur = pre; + if (end.row >= numberOfRowsInSection) { + end = previous; } } - return cur; + return end; } -static int ASFlowLayoutDistance(const std::pair &start, const std::pair &end, const std::vector> &nodes) +- (NSInteger)flowLayoutDistanceForRange:(ASIndexPathRange)range { - if (start == end) { + // This method should only be called with the range in proper order (start comes before end). + ASDisplayNodeAssert(ASIndexPathEqualToIndexPath(ASIndexPathMinimum(range.start, range.end), range.start), @"flowLayoutDistanceForRange: called with invalid range"); + + if (ASIndexPathEqualToIndexPath(range.start, range.end)) { return 0; - } else if (start > end) { - return - ASFlowLayoutDistance(end, start, nodes); } + + NSInteger totalRowCount = 0; + NSUInteger numberOfRowsInSection = 0; + NSArray *completedNodes = [_dataSource completedNodes]; - int res = 0; - - for (int i = start.first; i <= end.first; i++) { - res += (i == end.first ? end.second + 1 : nodes[i].size()) - (i == start.first ? start.second : 0); + for (NSInteger section = range.start.section; section <= range.end.section; section++) { + numberOfRowsInSection = [(NSArray *)completedNodes[section] count]; + totalRowCount += numberOfRowsInSection; + + if (section == range.start.section) { + // For the start section, make sure we don't count the rows before the start row. + totalRowCount -= range.start.row; + } else if (section == range.end.section) { + // For the start section, make sure we don't count the rows after the end row. + totalRowCount -= (numberOfRowsInSection - (range.end.row + 1)); + } } - - return res; + + ASDisplayNodeAssert(totalRowCount >= 0, @"totalRowCount in flowLayoutDistanceForRange: should not be negative"); + return totalRowCount; } @end diff --git a/AsyncDisplayKit/Details/ASIndexPath.h b/AsyncDisplayKit/Details/ASIndexPath.h new file mode 100644 index 0000000000..9eed7dd247 --- /dev/null +++ b/AsyncDisplayKit/Details/ASIndexPath.h @@ -0,0 +1,84 @@ +// +// ASIndexPath.h +// Pods +// +// Created by Scott Goodson on 7/4/15. +// +// A much more efficient way to handle index paths than NSIndexPath. +// For best results, use C++ vectors; NSValue wrapping with Cocoa collections +// would make NSIndexPath a much better choice. +// + +typedef struct { + NSInteger section; + NSInteger row; +} ASIndexPath; + +typedef struct { + ASIndexPath start; + ASIndexPath end; +} ASIndexPathRange; + +ASIndexPath ASIndexPathMake(NSInteger section, NSInteger row) +{ + ASIndexPath indexPath; + indexPath.section = section; + indexPath.row = row; + return indexPath; +} + +BOOL ASIndexPathEqualToIndexPath(ASIndexPath first, ASIndexPath second) +{ + return (first.section == second.section && first.row == second.row); +} + +ASIndexPath ASIndexPathMinimum(ASIndexPath first, ASIndexPath second) +{ + if (first.section < second.section) { + return first; + } else if (first.section > second.section) { + return second; + } else { + return (first.row < second.row ? first : second); + } +} + +ASIndexPath ASIndexPathMaximum(ASIndexPath first, ASIndexPath second) +{ + if (first.section > second.section) { + return first; + } else if (first.section < second.section) { + return second; + } else { + return (first.row > second.row ? first : second); + } +} + +ASIndexPathRange ASIndexPathRangeMake(ASIndexPath first, ASIndexPath second) +{ + ASIndexPathRange range; + range.start = ASIndexPathMinimum(first, second); + range.end = ASIndexPathMaximum(first, second); + return range; +} + +BOOL ASIndexPathRangeEqualToIndexPathRange(ASIndexPathRange first, ASIndexPathRange second) +{ + return ASIndexPathEqualToIndexPath(first.start, second.start) && ASIndexPathEqualToIndexPath(first.end, second.end); +} + +@interface NSIndexPath (ASIndexPathAdditions) ++ (NSIndexPath *)indexPathWithASIndexPath:(ASIndexPath)indexPath; +- (ASIndexPath)ASIndexPathValue; +@end + +@implementation NSIndexPath (ASIndexPathAdditions) ++ (NSIndexPath *)indexPathWithASIndexPath:(ASIndexPath)indexPath +{ + return [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];; +} +- (ASIndexPath)ASIndexPathValue +{ + return ASIndexPathMake(self.section, self.row); +} +@end diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 1a6afed732..f41d9d291c 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -191,9 +191,6 @@ }]; ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(insertNodesAtIndexPaths:withSizes:)]) { - [_layoutController insertNodesAtIndexPaths:indexPaths withSizes:nodeSizes]; - } _rangeIsValid = NO; [_delegate rangeController:self didInsertNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; }); @@ -201,9 +198,6 @@ - (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(deleteNodesAtIndexPaths:)]) { - [_layoutController deleteNodesAtIndexPaths:indexPaths]; - } _rangeIsValid = NO; [_delegate rangeController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; }); @@ -223,9 +217,6 @@ }]; ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(insertSections:atIndexSet:)]) { - [_layoutController insertSections:sectionNodeSizes atIndexSet:indexSet]; - } _rangeIsValid = NO; [_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }); @@ -233,9 +224,6 @@ - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(deleteSectionsAtIndexSet:)]) { - [_layoutController deleteSectionsAtIndexSet:indexSet]; - } _rangeIsValid = NO; [_delegate rangeController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }); From 6220d65747ac20521d2d7a8741d4ba657fa36291 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 4 Jul 2015 20:54:04 -0700 Subject: [PATCH 8/9] Property declaration tweak for odd Travis build failure (only on the Tests build). --- AsyncDisplayKit/Details/ASFlowLayoutController.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.h b/AsyncDisplayKit/Details/ASFlowLayoutController.h index 01da06d124..9b61a37b7d 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.h +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.h @@ -28,7 +28,7 @@ typedef NS_ENUM(NSUInteger, ASFlowLayoutDirection) { @interface ASFlowLayoutController : ASAbstractLayoutController @property (nonatomic, readonly, assign) ASFlowLayoutDirection layoutDirection; -@property (nonatomic) id dataSource; +@property (nonatomic, readwrite, weak) id dataSource; - (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection; From 3afcf33206432710430e74fc60b034d542463b86 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 5 Jul 2015 13:19:24 -0700 Subject: [PATCH 9/9] Update podspec to 1.2.2 --- AsyncDisplayKit.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index da347b579b..546001e6e7 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |spec| spec.name = 'AsyncDisplayKit' - spec.version = '1.2.1' + spec.version = '1.2.2' spec.license = { :type => 'BSD' } spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com', 'Ryan Nystrom' => 'rnystrom@fb.com' } spec.summary = 'Smooth asynchronous user interfaces for iOS apps.' - spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.2.1' } + spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.2.2' } spec.documentation_url = 'http://asyncdisplaykit.org/appledoc/'