From 67055eecff8dd80babf33862d8be1ae53b881030 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 13 Jun 2016 17:03:36 -0700 Subject: [PATCH 01/51] Fix default tuning parameter for range mode ASLayoutRangeModeMinimum and range type ASLayoutRangeTypeFetchData --- AsyncDisplayKit/Details/ASAbstractLayoutController.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/Details/ASAbstractLayoutController.mm b/AsyncDisplayKit/Details/ASAbstractLayoutController.mm index 4120fad6..91121083 100644 --- a/AsyncDisplayKit/Details/ASAbstractLayoutController.mm +++ b/AsyncDisplayKit/Details/ASAbstractLayoutController.mm @@ -49,8 +49,8 @@ extern BOOL ASRangeTuningParametersEqualToRangeTuningParameters(ASRangeTuningPar .trailingBufferScreenfuls = 0.25 }; _tuningParameters[ASLayoutRangeModeMinimum][ASLayoutRangeTypeFetchData] = { - .leadingBufferScreenfuls = 0.25, - .trailingBufferScreenfuls = 0.5 + .leadingBufferScreenfuls = 0.5, + .trailingBufferScreenfuls = 0.25 }; _tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypeDisplay] = { From f5460a44e0c1e9d67378ac5ae896f4e073c44713 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 16 Jun 2016 17:10:09 -0700 Subject: [PATCH 02/51] [ASDisplayNode] Reduce locking in removeFromSupernode --- AsyncDisplayKit/ASDisplayNode.mm | 45 ++++++++++++++------------------ 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 5bf9966c..30383ee8 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1515,40 +1515,35 @@ static NSInteger incrementIfFound(NSInteger i) { [subnode __setSupernode:nil]; } +// NOTE: You must not called this method while holding the receiver's property lock. This may cause deadlocks. - (void)removeFromSupernode { - ASDisplayNodeAssertThreadAffinity(self); - BOOL shouldRemoveFromSuperviewOrSuperlayer = NO; + _propertyLock.lock(); + __weak ASDisplayNode *supernode = _supernode; + __weak UIView *view = _view; + __weak CALayer *layer = _layer; + BOOL layerBacked = _flags.layerBacked; + _propertyLock.unlock(); - { - ASDN::MutexLocker l(_propertyLock); - if (!_supernode) - return; + if (supernode == nil) { + return; + } + [supernode _removeSubnode:self]; + + if (self.nodeLoaded && supernode.nodeLoaded) { // Check to ensure that our view or layer is actually inside of our supernode; otherwise, don't remove it. // Though _ASDisplayView decouples the supernode if it is inserted inside another view hierarchy, this is // more difficult to guarantee with _ASDisplayLayer because CoreAnimation doesn't have a -didMoveToSuperlayer. - - if (self.nodeLoaded && _supernode.nodeLoaded) { - if (_flags.layerBacked || _supernode.layerBacked) { - shouldRemoveFromSuperviewOrSuperlayer = (_layer.superlayer == _supernode.layer); - } else { - shouldRemoveFromSuperviewOrSuperlayer = (_view.superview == _supernode.view); - } - } - } - - // Do this before removing the view from the hierarchy, as the node will clear its supernode pointer when its view is removed from the hierarchy. - // This call may result in the object being destroyed. - [_supernode _removeSubnode:self]; - - if (shouldRemoveFromSuperviewOrSuperlayer) { ASPerformBlockOnMainThread(^{ - ASDN::MutexLocker l(_propertyLock); - if (_flags.layerBacked) { - [_layer removeFromSuperlayer]; + if (layerBacked || supernode.layerBacked) { + if (layer.superlayer == supernode.layer) { + [layer removeFromSuperlayer]; + } } else { - [_view removeFromSuperview]; + if (view.superview == supernode.view) { + [view removeFromSuperview]; + } } }); } From 15b6f2e2811348fa94ee8d5e8dc131913df662ae Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 16 Jun 2016 17:10:33 -0700 Subject: [PATCH 03/51] [ASLayoutTransition] Optimize add/remove subnode methods --- AsyncDisplayKit/Private/ASLayoutTransition.mm | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/AsyncDisplayKit/Private/ASLayoutTransition.mm b/AsyncDisplayKit/Private/ASLayoutTransition.mm index 67709cc3..560ac613 100644 --- a/AsyncDisplayKit/Private/ASLayoutTransition.mm +++ b/AsyncDisplayKit/Private/ASLayoutTransition.mm @@ -48,9 +48,12 @@ { ASDN::MutexLocker l(_propertyLock); [self calculateSubnodeOperationsIfNeeded]; - for (NSUInteger i = 0; i < [_insertedSubnodes count]; i++) { + + NSUInteger i = 0; + for (ASDisplayNode *node in _insertedSubnodes) { NSUInteger p = _insertedSubnodePositions[i]; - [_node insertSubnode:_insertedSubnodes[i] atIndex:p]; + [_node insertSubnode:node atIndex:p]; + i += 1; } } @@ -58,8 +61,8 @@ { ASDN::MutexLocker l(_propertyLock); [self calculateSubnodeOperationsIfNeeded]; - for (NSUInteger i = 0; i < [_removedSubnodes count]; i++) { - [_removedSubnodes[i] removeFromSupernode]; + for (ASDisplayNode *subnode in _removedSubnodes) { + [subnode removeFromSupernode]; } } From 0002d333f0edfc907b72c9656f0169c0d62bf87f Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 16 Jun 2016 17:59:50 -0700 Subject: [PATCH 04/51] [ASDisplayNodeTests] Add some removeFromSupernode tests --- AsyncDisplayKitTests/ASDisplayNodeTests.m | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index ed8ee616..c85b7d31 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -1638,6 +1638,57 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point [c release]; } +- (void)testRemoveFromViewBackedLoadedSupernode +{ + DeclareNodeNamed(a); + DeclareNodeNamed(b); + [b addSubnode:a]; + [a view]; + [b view]; + XCTAssertNodesLoaded(a, b); + XCTAssertEqual(a.supernode, b); + XCTAssertEqual(a.view.superview, b.view); + + [a removeFromSupernode]; + XCTAssertNil(a.supernode); + XCTAssertNil(a.view.superview); +} + +- (void)testRemoveFromLayerBackedLoadedSupernode +{ + DeclareNodeNamed(a); + a.layerBacked = YES; + DeclareNodeNamed(b); + b.layerBacked = YES; + [b addSubnode:a]; + [a layer]; + [b layer]; + XCTAssertNodesLoaded(a, b); + XCTAssertEqual(a.supernode, b); + XCTAssertEqual(a.layer.superlayer, b.layer); + + [a removeFromSupernode]; + XCTAssertNil(a.supernode); + XCTAssertNil(a.layer.superlayer); +} + +- (void)testRemoveLayerBackedFromViewBackedLoadedSupernode +{ + DeclareNodeNamed(a); + a.layerBacked = YES; + DeclareNodeNamed(b); + [b addSubnode:a]; + [a layer]; + [b view]; + XCTAssertNodesLoaded(a, b); + XCTAssertEqual(a.supernode, b); + XCTAssertEqual(a.layer.superlayer, b.layer); + + [a removeFromSupernode]; + XCTAssertNil(a.supernode); + XCTAssertNil(a.layer.superlayer); +} + - (void)testSubnodeAddedBeforeLoadingExternalView { UIView *view = [[UIDisplayNodeTestView alloc] init]; From 42aa52b4076257fc50faafeac15c86783ae275a7 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 17 Jun 2016 09:51:09 -0700 Subject: [PATCH 05/51] [ASDisplayNode] Put the thread affinity assertion back --- AsyncDisplayKit/ASDisplayNode.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 30383ee8..9609d183 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1518,6 +1518,7 @@ static NSInteger incrementIfFound(NSInteger i) { // NOTE: You must not called this method while holding the receiver's property lock. This may cause deadlocks. - (void)removeFromSupernode { + ASDisplayNodeAssertThreadAffinity(self); _propertyLock.lock(); __weak ASDisplayNode *supernode = _supernode; __weak UIView *view = _view; From 46421f868437371255f5fc0a7f337b0980888956 Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Fri, 17 Jun 2016 10:17:52 -0700 Subject: [PATCH 06/51] [ASCollectionView] support UICollectionViewLayoutAttributes --- AsyncDisplayKit/ASCellNode.h | 2 ++ AsyncDisplayKit/ASCellNode.mm | 4 ++++ AsyncDisplayKit/ASCollectionView.mm | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/AsyncDisplayKit/ASCellNode.h b/AsyncDisplayKit/ASCellNode.h index edad6c41..3cdae7de 100644 --- a/AsyncDisplayKit/ASCellNode.h +++ b/AsyncDisplayKit/ASCellNode.h @@ -99,6 +99,8 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { - (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; - (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes; + /** * @abstract Initializes a cell with a given view controller block. * diff --git a/AsyncDisplayKit/ASCellNode.mm b/AsyncDisplayKit/ASCellNode.mm index b2bc7344..cfd00907 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -211,6 +211,10 @@ #pragma clang diagnostic pop +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ +} + - (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame { // To be overriden by subclasses diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 821e21dd..54dfe8a4 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -58,6 +58,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _node.highlighted = highlighted; } +- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes +{ + [_node applyLayoutAttributes:layoutAttributes]; +} + @end #pragma mark - From eb057ca380a78965f84965b405efbc0324c2b40b Mon Sep 17 00:00:00 2001 From: Vadim Spivak Date: Fri, 17 Jun 2016 13:52:27 -0700 Subject: [PATCH 07/51] Editable text node should include insets when calculating size --- AsyncDisplayKit/ASEditableTextNode.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index 48035a8d..fda9fd6e 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -192,7 +192,9 @@ { ASTextKitComponents *displayedComponents = [self isDisplayingPlaceholder] ? _placeholderTextKitComponents : _textKitComponents; CGSize textSize = [displayedComponents sizeForConstrainedWidth:constrainedSize.width]; - return CGSizeMake(fminf(ceilf(textSize.width), constrainedSize.width), fminf(ceilf(textSize.height), constrainedSize.height)); + CGFloat width = ceilf(textSize.width + _textContainerInset.left + _textContainerInset.right); + CGFloat height = ceilf(textSize.height + _textContainerInset.top + _textContainerInset.bottom); + return CGSizeMake(fminf(width, constrainedSize.width), fminf(height, constrainedSize.height)); } - (void)layout From da27d36b4abfce74a190337e7236116b037ab713 Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Sat, 18 Jun 2016 12:54:55 -0700 Subject: [PATCH 08/51] Add comments --- AsyncDisplayKit/ASCellNode.h | 5 +++++ AsyncDisplayKit/ASCellNode.mm | 1 + 2 files changed, 6 insertions(+) diff --git a/AsyncDisplayKit/ASCellNode.h b/AsyncDisplayKit/ASCellNode.h index 3cdae7de..e70d67ef 100644 --- a/AsyncDisplayKit/ASCellNode.h +++ b/AsyncDisplayKit/ASCellNode.h @@ -99,6 +99,11 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { - (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; - (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; +/** + * Called by the system when ASCellNode is used with an ASCollectionNode. It will not be called by ASTableNode. + * When the UICollectionViewLayout object returns a new UICollectionViewLayoutAttributes object, the corresponding ASCellNode will be updated. + * See UICollectionViewCell's applyLayoutAttributes: for a full description. +*/ - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes; /** diff --git a/AsyncDisplayKit/ASCellNode.mm b/AsyncDisplayKit/ASCellNode.mm index cfd00907..1bc660c4 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -213,6 +213,7 @@ - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { + // To be overriden by subclasses } - (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame From ead9590d36433a78b57e1bd264934de2aa43f177 Mon Sep 17 00:00:00 2001 From: Ethan Nagel Date: Sun, 19 Jun 2016 18:52:32 -0700 Subject: [PATCH 09/51] Default animation run loop -> CommonModes --- AsyncDisplayKit/ASImageNode+AnimatedImage.mm | 2 +- AsyncDisplayKit/ASImageNode.h | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm index 5899764b..7df743c3 100644 --- a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm +++ b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm @@ -22,7 +22,7 @@ #import "ASInternalHelpers.h" #import "ASWeakProxy.h" -NSString *const ASAnimatedImageDefaultRunLoopMode = NSDefaultRunLoopMode; +NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; @implementation ASImageNode (AnimatedImage) diff --git a/AsyncDisplayKit/ASImageNode.h b/AsyncDisplayKit/ASImageNode.h index 003ed43c..ef603cfd 100644 --- a/AsyncDisplayKit/ASImageNode.h +++ b/AsyncDisplayKit/ASImageNode.h @@ -144,9 +144,11 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image); @property (atomic, assign) BOOL animatedImagePaused; /** - * @abstract The runloop mode used to animte th image. + * @abstract The runloop mode used to animate the image. * - * @discussion Defaults to NSDefaultRunLoopMode. Another commonly used mode is NSRunLoopCommonModes. + * @discussion Defaults to NSRunLoopCommonModes. Another commonly used mode is NSDefaultRunLoopMode. + * Setting NSDefaultRunLoopMode will cause animation to pause while scrolling (if the ASImageNode is + * in a scroll view), which may improve scroll performance in some use cases. */ @property (atomic, strong) NSString *animatedImageRunLoopMode; From 2c6e810e26316bf36640af735e5a1625b0259a2f Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 20 Jun 2016 07:50:40 -0700 Subject: [PATCH 10/51] Fix not handling imageNode:didLoadImage: from ASNetworkImageNodeDelegate as optional --- AsyncDisplayKit/ASNetworkImageNode.mm | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 279f1bca..fbfea442 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -44,9 +44,11 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; CGFloat _currentImageQuality; CGFloat _renderedImageQuality; + // TODO: Move this to flags BOOL _delegateSupportsDidStartFetchingData; BOOL _delegateSupportsDidFailWithError; - BOOL _delegateSupportsImageNodeDidFinishDecoding; + BOOL _delegateSupportsDidFinishDecoding; + BOOL _delegateSupportsDidLoadImage; BOOL _shouldRenderProgressImages; @@ -214,7 +216,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _delegateSupportsDidStartFetchingData = [delegate respondsToSelector:@selector(imageNodeDidStartFetchingData:)]; _delegateSupportsDidFailWithError = [delegate respondsToSelector:@selector(imageNode:didFailWithError:)]; - _delegateSupportsImageNodeDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)]; + _delegateSupportsDidFinishDecoding = [delegate respondsToSelector:@selector(imageNodeDidFinishDecoding:)]; + _delegateSupportsDidLoadImage = [delegate respondsToSelector:@selector(imageNode:didLoadImage:)]; } - (id)delegate @@ -494,7 +497,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ self.currentImageQuality = 1.0; }); - [_delegate imageNode:self didLoadImage:self.image]; + if (_delegateSupportsDidLoadImage) { + [_delegate imageNode:self didLoadImage:self.image]; + } }); } } else { @@ -529,7 +534,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; strongSelf->_cacheUUID = nil; if (imageContainer != nil) { - [strongSelf->_delegate imageNode:strongSelf didLoadImage:strongSelf.image]; + if (strongSelf->_delegateSupportsDidLoadImage) { + [strongSelf->_delegate imageNode:strongSelf didLoadImage:strongSelf.image]; + } } else if (error && strongSelf->_delegateSupportsDidFailWithError) { [strongSelf->_delegate imageNode:strongSelf didFailWithError:error]; @@ -581,7 +588,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; [super displayDidFinish]; ASDN::MutexLocker l(_lock); - if (_delegateSupportsImageNodeDidFinishDecoding && self.layer.contents != nil) { + if (_delegateSupportsDidFinishDecoding && self.layer.contents != nil) { /* We store the image quality in _currentImageQuality whenever _image is set. On the following displayDidFinish, we'll know that _currentImageQuality is the quality of the image that has just finished rendering. In order for this to be accurate, we need to be sure we are on main thread when we set _currentImageQuality. Otherwise, it is possible for _currentImageQuality From 23736c3cf7dd8ea4de7c1d09fc0f0a48934c5932 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 19 Jun 2016 14:01:07 -0700 Subject: [PATCH 11/51] Fix wrapped UIKit components are disappearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I looked into the internals of UIImageView a bit. I would recommend to not fixing this in a universal way. UIImageView is specifically optimized for performance and does not use the usual way to provide the contents of the CALayer that backs the UIImageView. Furthermore we cannot trigger a recreation of the contents of the UIImageView layer as if it get’s cleared and we call setNeedsDisplay on it it goes trough the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately UIImageView does not do it, it actually does not implement some of the methods at all, so nothing will show up. It’s getting better, by calling setNeedsDisplay on an UIImageView layer it actually clears the contents and nothing is visible anymore. So we have to be careful to not calling that too. Unfortunately I didn’t find a way yet to trigger a recreation of the UIImageView layers content except calling the private _updateState method. That said it’s different for layers of other UIKit components like UILabel for example. Clearing the contents of a UILabel layer and calling setNeedsDisplay on the layer usually recreates the contents of the it and it will show up again. This commit prevents to clear the contents of a layer for all wrapped UIKit and instead only NOT clear the content if the node wraps a UIImageView otherwise we should clear the contents of the layer to reclaim memory as usual. --- AsyncDisplayKit/ASDisplayNode.mm | 35 +++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 9609d183..8db127a9 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1791,7 +1791,26 @@ static NSInteger incrementIfFound(NSInteger i) { // Helper method to summarize whether or not the node run through the display process - (BOOL)__implementsDisplay { - return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.shouldRasterizeDescendants || _flags.implementsInstanceDrawRect || _flags.implementsInstanceImageDisplay; + return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.shouldRasterizeDescendants || + _flags.implementsInstanceDrawRect || _flags.implementsInstanceImageDisplay; +} + +- (BOOL)__canClearContentsOfLayer +{ + // The layer contents should not be cleared in case the node is wrapping a UIImageView.UIImageView is specifically + // optimized for performance and does not use the usual way to provide the contents of the CALayer via the + // CALayerDelegate method that backs the UIImageView. + return !_flags.synchronous || _viewClass != [UIImageView class]; +} + +- (BOOL)__canCallNeedsDisplayOfLayer +{ + // Prevent calling setNeedsDisplay on a layer that backs a UIImageView. Usually calling setNeedsDisplay on a CALayer + // triggers a recreation of the contents of layer unfortunately calling it on a CALayer that backs a UIImageView + // it goes trough the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately + // UIImageView does not do recreate the layer contents the usual way, it actually does not implement some of the + // methods at all instead it throws away the contents of the layer and nothing will show up. + return _flags.synchronous && _viewClass != [UIImageView class]; } - (BOOL)placeholderShouldPersist @@ -1841,6 +1860,13 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) // (even a runloop observer at a late call order will not stop the next frame from compositing, showing placeholders). ASDisplayNode *node = [layer asyncdisplaykit_node]; + + if ([node __canCallNeedsDisplayOfLayer]) { + // Layers for UIKit components that are wrapped wtihin a node needs to be set to be displayed as the contents of + // the layer get's cleared and would not be recreated otherwise + [layer setNeedsDisplay]; + } + if ([node __implementsDisplay]) { // For layers that do get displayed here, this immediately kicks off the work on the concurrent -[_ASDisplayLayer displayQueue]. // At the same time, it creates an associated _ASAsyncTransaction, which we can use to block on display completion. See ASDisplayNode+AsyncDisplay.mm. @@ -2088,8 +2114,11 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)clearContents { - // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. - _layer.contents = nil; + if ([self __canClearContentsOfLayer]) { + // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. + _layer.contents = nil; + } + _placeholderLayer.contents = nil; _placeholderImage = nil; } From eef22074a45e7c695d235fecb65109ed35c3629c Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 20 Jun 2016 10:25:09 -0700 Subject: [PATCH 12/51] Add caching for checking if ASDisplayNode can clear it contents or setNeedsDisplay: can be called on the layer --- AsyncDisplayKit/ASDisplayNode.mm | 29 +++++++++---------- .../Private/ASDisplayNodeInternal.h | 14 +++++++++ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 8db127a9..f27b2ad7 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -273,6 +273,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _preferredFrameSize = CGSizeZero; _environmentState = ASEnvironmentStateMakeDefault(); + + _flags.canClearContentsOfLayer = YES; + _flags.canCallNeedsDisplayOfLayer = NO; } - (instancetype)init @@ -440,6 +443,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } view = [[_viewClass alloc] init]; } + + // Update flags related to special handling of UIImageView layers. More details on the flags + BOOL isUIIMageViewViewClass = [view isKindOfClass:[UIImageView class]]; + _flags.canClearContentsOfLayer = !isUIIMageViewViewClass; + _flags.canCallNeedsDisplayOfLayer = (_flags.synchronous && !isUIIMageViewViewClass); return view; } @@ -1788,29 +1796,18 @@ static NSInteger incrementIfFound(NSInteger i) { } } -// Helper method to summarize whether or not the node run through the display process +/// Helper method to summarize whether or not the node run through the display process - (BOOL)__implementsDisplay { return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.shouldRasterizeDescendants || _flags.implementsInstanceDrawRect || _flags.implementsInstanceImageDisplay; } -- (BOOL)__canClearContentsOfLayer -{ - // The layer contents should not be cleared in case the node is wrapping a UIImageView.UIImageView is specifically - // optimized for performance and does not use the usual way to provide the contents of the CALayer via the - // CALayerDelegate method that backs the UIImageView. - return !_flags.synchronous || _viewClass != [UIImageView class]; -} - +// Helper method to determine if it's save to call setNeedsDisplay on a layer without throwing away the content. +// For details look at the comment on the canCallNeedsDisplayOfLayer flag - (BOOL)__canCallNeedsDisplayOfLayer { - // Prevent calling setNeedsDisplay on a layer that backs a UIImageView. Usually calling setNeedsDisplay on a CALayer - // triggers a recreation of the contents of layer unfortunately calling it on a CALayer that backs a UIImageView - // it goes trough the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately - // UIImageView does not do recreate the layer contents the usual way, it actually does not implement some of the - // methods at all instead it throws away the contents of the layer and nothing will show up. - return _flags.synchronous && _viewClass != [UIImageView class]; + return _flags.canCallNeedsDisplayOfLayer; } - (BOOL)placeholderShouldPersist @@ -2114,7 +2111,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)clearContents { - if ([self __canClearContentsOfLayer]) { + if (_flags.canClearContentsOfLayer) { // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. _layer.contents = nil; } diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index e29a9e8c..ee3da6b0 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -75,6 +75,20 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo unsigned displaySuspended:1; unsigned shouldAnimateSizeChanges:1; unsigned hasCustomDrawingPriority:1; + + // Wrapped view handling + + // The layer contents should not be cleared in case the node is wrapping a UIImageView.UIImageView is specifically + // optimized for performance and does not use the usual way to provide the contents of the CALayer via the + // CALayerDelegate method that backs the UIImageView. + unsigned canClearContentsOfLayer:1; + + // Prevent calling setNeedsDisplay on a layer that backs a UIImageView. Usually calling setNeedsDisplay on a CALayer + // triggers a recreation of the contents of layer unfortunately calling it on a CALayer that backs a UIImageView + // it goes trough the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately + // UIImageView does not do recreate the layer contents the usual way, it actually does not implement some of the + // methods at all instead it throws away the contents of the layer and nothing will show up. + unsigned canCallNeedsDisplayOfLayer:1; // whether custom drawing is enabled unsigned implementsInstanceDrawRect:1; From e230c690555f1f759b9b10f716383a3512e9455d Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 20 Jun 2016 13:34:14 -0700 Subject: [PATCH 13/51] Improve settings flags --- AsyncDisplayKit/ASDisplayNode.mm | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index f27b2ad7..e38731e7 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -445,9 +445,13 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } // Update flags related to special handling of UIImageView layers. More details on the flags - BOOL isUIIMageViewViewClass = [view isKindOfClass:[UIImageView class]]; - _flags.canClearContentsOfLayer = !isUIIMageViewViewClass; - _flags.canCallNeedsDisplayOfLayer = (_flags.synchronous && !isUIIMageViewViewClass); + if (_flags.synchronous) { + if ([view isKindOfClass:[UIImageView class]]) { + _flags.canClearContentsOfLayer = NO; + } else { + _flags.canCallNeedsDisplayOfLayer = YES; + } + } return view; } From 22fa715682f5faf25b1bb1e750c665205ab51ced Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 15 Jun 2016 16:51:58 -0700 Subject: [PATCH 14/51] Move lock from ASDN::Mutex to std::mutex in ASTextKitContext --- AsyncDisplayKit/TextKit/ASTextKitContext.mm | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.mm b/AsyncDisplayKit/TextKit/ASTextKitContext.mm index 73f4b92e..7410ed9f 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.mm @@ -9,14 +9,14 @@ // #import "ASTextKitContext.h" -#import "ASThread.h" - #import "ASLayoutManager.h" +#import + @implementation ASTextKitContext { // All TextKit operations (even non-mutative ones) must be executed serially. - ASDN::Mutex _textKitMutex; + std::mutex _textKitMutex; NSLayoutManager *_layoutManager; NSTextStorage *_textStorage; @@ -35,8 +35,8 @@ { if (self = [super init]) { // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. - static ASDN::Mutex __staticMutex; - ASDN::MutexLocker l(__staticMutex); + static std::mutex __static_mutex; + std::lock_guard l(__static_mutex); // Create the TextKit component stack with our default configuration. if (textStorageCreationBlock) { _textStorage = textStorageCreationBlock(attributedString); @@ -60,13 +60,13 @@ - (CGSize)constrainedSize { - ASDN::MutexLocker l(_textKitMutex); + std::lock_guard l(_textKitMutex); return _textContainer.size; } - (void)setConstrainedSize:(CGSize)constrainedSize { - ASDN::MutexLocker l(_textKitMutex); + std::lock_guard l(_textKitMutex); _textContainer.size = constrainedSize; } @@ -74,7 +74,7 @@ NSTextStorage *, NSTextContainer *))block { - ASDN::MutexLocker l(_textKitMutex); + std::lock_guard l(_textKitMutex); block(_layoutManager, _textStorage, _textContainer); } From 3d72a6b7e56df899df81b35ca255e8080f6340e8 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 15 Jun 2016 16:53:15 -0700 Subject: [PATCH 15/51] Cleanup ASTextNode and add locking For now we use a big recursive lock. This needs to be revisited as we revisit the whole ASDK locking strategy. --- AsyncDisplayKit/ASTextNode.mm | 219 +++++++++++++++++++++++----------- 1 file changed, 151 insertions(+), 68 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index fedcfaa0..a7d00a99 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -11,22 +11,19 @@ #import "ASTextNode.h" #import "ASTextNode+Beta.h" +#include + #import -#import #import #import #import #import #import "ASTextKitCoreTextAdditions.h" -#import "ASTextKitComponents.h" -#import "ASTextKitFontSizeAdjuster.h" -#import "ASTextKitRenderer.h" #import "ASTextKitRenderer+Positioning.h" #import "ASTextKitShadower.h" #import "ASInternalHelpers.h" -#import "ASEqualityHelpers.h" #import "ASLayout.h" static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15; @@ -38,15 +35,13 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation @interface ASTextNodeDrawParameters : NSObject @property (nonatomic, assign, readonly) CGRect bounds; - @property (nonatomic, strong, readonly) UIColor *backgroundColor; @end @implementation ASTextNodeDrawParameters -- (instancetype)initWithBounds:(CGRect)bounds - backgroundColor:(UIColor *)backgroundColor +- (instancetype)initWithBounds:(CGRect)bounds backgroundColor:(UIColor *)backgroundColor { if (self = [super init]) { _bounds = bounds; @@ -76,7 +71,7 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation NSRange _highlightRange; ASHighlightOverlayLayer *_activeHighlightLayer; - ASDN::Mutex _rendererLock; + std::recursive_mutex _textLock; CGSize _constrainedSize; @@ -160,6 +155,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (NSString *)description { + std::lock_guard l(_textLock); + NSString *plainString = [[_attributedText string] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; NSString *truncationString = [_composedTruncationText string]; if (plainString.length > 50) @@ -195,7 +192,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)didLoad { [super didLoad]; - + // If we are view-backed and the delegate cares, support the long-press callback. SEL longPressCallback = @selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:); if (!self.isLayerBacked && [self.delegate respondsToSelector:longPressCallback]) { @@ -227,7 +224,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds { - ASDN::MutexLocker l(_rendererLock); + std::lock_guard l(_textLock); + if (_renderer == nil) { CGSize constrainedSize = _constrainedSize.width != -INFINITY ? _constrainedSize : bounds.size; _renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes] @@ -250,9 +248,28 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; }; } +- (void)_invalidateRendererIfNeeded +{ + [self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size]; +} + +- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize +{ + if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { + // Our bounds of frame have changed to a size that is not identical to our constraining size, + // so our previous layout information is invalid, and TextKit may draw at the + // incorrect origin. + { + std::lock_guard l(_textLock); + _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); + } + [self _invalidateRenderer]; + } +} + - (void)_invalidateRenderer { - ASDN::MutexLocker l(_rendererLock); + std::lock_guard l(_textLock); if (_renderer) { // Destruction of the layout managers/containers/text storage is quite @@ -267,27 +284,13 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } -- (void)_invalidateRendererIfNeeded -{ - [self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size]; -} - -- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize -{ - if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { - // Our bounds of frame have changed to a size that is not identical to our constraining size, - // so our previous layout information is invalid, and TextKit may draw at the - // incorrect origin. - _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); - [self _invalidateRenderer]; - } -} - #pragma mark - Layout and Sizing - (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize { - if (!_renderer) { + std::lock_guard l(_textLock); + + if (_renderer == nil) { return YES; } @@ -322,6 +325,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; [super calculatedLayoutDidChange]; ASLayout *layout = self.calculatedLayout; + + std::lock_guard l(_textLock); if (layout != nil) { _constrainedSize = layout.size; _renderer.constrainedSize = layout.size; @@ -333,6 +338,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); + std::lock_guard l(_textLock); + _constrainedSize = constrainedSize; // Instead of invalidating the renderer, in case this is a new call with a different constrained size, @@ -341,7 +348,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; [self setNeedsDisplay]; - CGSize size = [[self _renderer] size]; + CGSize size = [self _renderer].size; if (_attributedText.length > 0) { CGFloat screenScale = ASScreenScale(); self.ascender = round([[_attributedText attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL] ascender] * screenScale)/screenScale; @@ -359,6 +366,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)setAttributedText:(NSAttributedString *)attributedText { + std::lock_guard l(_textLock); + if (attributedText == nil) { attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; } @@ -387,17 +396,19 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; [self invalidateCalculatedLayout]; [self setNeedsDisplay]; - + + + // Accessiblity self.accessibilityLabel = _attributedText.string; - - // We're an accessibility element by default if there is a string. - self.isAccessibilityElement = _attributedText.length != 0; + self.isAccessibilityElement = (_attributedText.length != 0); // We're an accessibility element by default if there is a string. } #pragma mark - Text Layout - (void)setExclusionPaths:(NSArray *)exclusionPaths { + std::lock_guard l(_textLock); + if (ASObjectIsEqual(exclusionPaths, _exclusionPaths)) { return; } @@ -410,6 +421,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (NSArray *)exclusionPaths { + std::lock_guard l(_textLock); + return _exclusionPaths; } @@ -417,6 +430,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)drawRect:(CGRect)bounds withParameters:(ASTextNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing { + std::lock_guard l(_textLock); + CGContextRef context = UIGraphicsGetCurrentContext(); ASDisplayNodeAssert(context, @"This is no good without a context."); @@ -437,7 +452,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } // Draw shadow - [[renderer shadower] setShadowInContext:context]; + [renderer.shadower setShadowInContext:context]; // Draw text bounds.origin = textOrigin; @@ -470,6 +485,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting { + ASDisplayNodeAssertMainThread(); + ASTextKitRenderer *renderer = [self _renderer]; NSRange visibleRange = renderer.firstVisibleRange; NSAttributedString *attributedString = _attributedText; @@ -562,6 +579,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { + ASDisplayNodeAssertMainThread(); + if (gestureRecognizer == _longPressGestureRecognizer) { // Don't allow long press on truncation message if ([self _pendingTruncationTap]) { @@ -593,6 +612,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (NSRange)highlightRange { + ASDisplayNodeAssertMainThread(); + return _highlightRange; } @@ -603,6 +624,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated { + ASDisplayNodeAssertMainThread(); + [self _setHighlightRange:highlightRange forAttributeName:nil value:nil animated:animated]; } @@ -672,7 +695,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; for (NSValue *rectValue in highlightRects) { UIEdgeInsets shadowPadding = _renderer.shadower.shadowPadding; - CGRect rendererRect = [[self class] _adjustRendererRect:rectValue.CGRectValue forShadowPadding:shadowPadding]; + CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding); CGRect highlightedRect = [self.layer convertRect:rendererRect toLayer:highlightTargetLayer]; // We set our overlay layer's frame to the bounds of the highlight target layer. @@ -709,6 +732,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)_clearHighlightIfNecessary { + ASDisplayNodeAssertMainThread(); + if ([self _pendingLinkTap] || [self _pendingTruncationTap]) { [self setHighlightRange:NSMakeRange(0, 0) animated:YES]; } @@ -726,29 +751,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Text rects -+ (CGRect)_adjustRendererRect:(CGRect)rendererRect forShadowPadding:(UIEdgeInsets)shadowPadding -{ +static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UIEdgeInsets shadowPadding) { rendererRect.origin.x -= shadowPadding.left; rendererRect.origin.y -= shadowPadding.top; return rendererRect; } -- (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRendererMeasureOption)measureOption -{ - NSArray *rects = [[self _renderer] rectsForTextRange:textRange measureOption:measureOption]; - NSMutableArray *adjustedRects = [NSMutableArray array]; - - for (NSValue *rectValue in rects) { - CGRect rect = [rectValue CGRectValue]; - rect = [self.class _adjustRendererRect:rect forShadowPadding:self.shadowPadding]; - - NSValue *adjustedRectValue = [NSValue valueWithCGRect:rect]; - [adjustedRects addObject:adjustedRectValue]; - } - - return adjustedRects; -} - - (NSArray *)rectsForTextRange:(NSRange)textRange { return [self _rectsForTextRange:textRange measureOption:ASTextKitRendererMeasureOptionCapHeight]; @@ -759,22 +767,46 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return [self _rectsForTextRange:textRange measureOption:ASTextKitRendererMeasureOptionBlock]; } +- (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextKitRendererMeasureOption)measureOption +{ + std::lock_guard l(_textLock); + + NSArray *rects = [[self _renderer] rectsForTextRange:textRange measureOption:measureOption]; + NSMutableArray *adjustedRects = [NSMutableArray array]; + + for (NSValue *rectValue in rects) { + CGRect rect = [rectValue CGRectValue]; + rect = ASTextNodeAdjustRenderRectForShadowPadding(rect, self.shadowPadding); + + NSValue *adjustedRectValue = [NSValue valueWithCGRect:rect]; + [adjustedRects addObject:adjustedRectValue]; + } + + return adjustedRects; +} + - (CGRect)trailingRect { + std::lock_guard l(_textLock); + CGRect rect = [[self _renderer] trailingRect]; - return [self.class _adjustRendererRect:rect forShadowPadding:self.shadowPadding]; + return ASTextNodeAdjustRenderRectForShadowPadding(rect, self.shadowPadding); } - (CGRect)frameForTextRange:(NSRange)textRange { + std::lock_guard l(_textLock); + CGRect frame = [[self _renderer] frameForTextRange:textRange]; - return [self.class _adjustRendererRect:frame forShadowPadding:self.shadowPadding]; + return ASTextNodeAdjustRenderRectForShadowPadding(frame, self.shadowPadding); } #pragma mark - Placeholders - (void)setPlaceholderColor:(UIColor *)placeholderColor { + std::lock_guard l(_textLock); + _placeholderColor = placeholderColor; // prevent placeholders if we don't have a color @@ -790,6 +822,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return nil; } + std::lock_guard l(_textLock); + UIGraphicsBeginImageContext(size); [self.placeholderColor setFill]; @@ -816,8 +850,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Touch Handling --(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { + ASDisplayNodeAssertMainThread(); + if (!_passthroughNonlinkTouches) { return [super pointInside:point withEvent:event]; } @@ -846,9 +882,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - [super touchesBegan:touches withEvent:event]; - ASDisplayNodeAssertMainThread(); + [super touchesBegan:touches withEvent:event]; CGPoint point = [[touches anyObject] locationInView:self.view]; @@ -878,15 +913,17 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + ASDisplayNodeAssertMainThread(); [super touchesCancelled:touches withEvent:event]; - + [self _clearHighlightIfNecessary]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + ASDisplayNodeAssertMainThread(); [super touchesEnded:touches withEvent:event]; - + if ([self _pendingLinkTap] && [_delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { CGPoint point = [[touches anyObject] locationInView:self.view]; [_delegate textNode:self tappedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:point textRange:_highlightRange]; @@ -903,6 +940,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + ASDisplayNodeAssertMainThread(); [super touchesMoved:touches withEvent:event]; UITouch *touch = [touches anyObject]; @@ -928,6 +966,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer { + ASDisplayNodeAssertMainThread(); + // Respond to long-press when it begins, not when it ends. if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { if ([self.delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { @@ -939,11 +979,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (BOOL)_pendingLinkTap { + std::lock_guard l(_textLock); + return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && _delegate != nil; } - (BOOL)_pendingTruncationTap { + std::lock_guard l(_textLock); + return [_highlightedLinkAttributeName isEqualToString:ASTextNodeTruncationTokenAttributeName]; } @@ -951,11 +995,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (CGColorRef)shadowColor { + std::lock_guard l(_textLock); + return _shadowColor; } - (void)setShadowColor:(CGColorRef)shadowColor { + std::lock_guard l(_textLock); + if (_shadowColor != shadowColor) { if (shadowColor != NULL) { CGColorRetain(shadowColor); @@ -968,11 +1016,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (CGSize)shadowOffset { + std::lock_guard l(_textLock); + return _shadowOffset; } - (void)setShadowOffset:(CGSize)shadowOffset { + std::lock_guard l(_textLock); + if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) { _shadowOffset = shadowOffset; [self _invalidateRenderer]; @@ -982,11 +1034,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (CGFloat)shadowOpacity { + std::lock_guard l(_textLock); + return _shadowOpacity; } - (void)setShadowOpacity:(CGFloat)shadowOpacity { + std::lock_guard l(_textLock); + if (_shadowOpacity != shadowOpacity) { _shadowOpacity = shadowOpacity; [self _invalidateRenderer]; @@ -996,11 +1052,15 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (CGFloat)shadowRadius { + std::lock_guard l(_textLock); + return _shadowRadius; } - (void)setShadowRadius:(CGFloat)shadowRadius { + std::lock_guard l(_textLock); + if (_shadowRadius != shadowRadius) { _shadowRadius = shadowRadius; [self _invalidateRenderer]; @@ -1015,6 +1075,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (UIEdgeInsets)shadowPaddingWithRenderer:(ASTextKitRenderer *)renderer { + std::lock_guard l(_textLock); + return renderer.shadower.shadowPadding; } @@ -1032,6 +1094,8 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText { + std::lock_guard l(_textLock); + if (ASObjectIsEqual(_truncationAttributedText, truncationAttributedText)) { return; } @@ -1042,6 +1106,8 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage { + std::lock_guard l(_textLock); + if (ASObjectIsEqual(_additionalTruncationMessage, additionalTruncationMessage)) { return; } @@ -1052,6 +1118,8 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)setTruncationMode:(NSLineBreakMode)truncationMode { + std::lock_guard l(_textLock); + if (_truncationMode != truncationMode) { _truncationMode = truncationMode; [self _invalidateRenderer]; @@ -1061,12 +1129,16 @@ static NSAttributedString *DefaultTruncationAttributedString() - (BOOL)isTruncated { + std::lock_guard l(_textLock); + ASTextKitRenderer *renderer = [self _renderer]; return renderer.firstVisibleRange.length < _attributedText.length; } - (void)setPointSizeScaleFactors:(NSArray *)pointSizeScaleFactors { + std::lock_guard l(_textLock); + if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) { _pointSizeScaleFactors = pointSizeScaleFactors; [self _invalidateRenderer]; @@ -1075,15 +1147,19 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines { - if (_maximumNumberOfLines != maximumNumberOfLines) { - _maximumNumberOfLines = maximumNumberOfLines; - [self _invalidateRenderer]; - [self setNeedsDisplay]; - } + std::lock_guard l(_textLock); + + if (_maximumNumberOfLines != maximumNumberOfLines) { + _maximumNumberOfLines = maximumNumberOfLines; + [self _invalidateRenderer]; + [self setNeedsDisplay]; + } } - (NSUInteger)lineCount { + std::lock_guard l(_textLock); + return [[self _renderer] lineCount]; } @@ -1091,6 +1167,8 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)_updateComposedTruncationText { + std::lock_guard l(_textLock); + _composedTruncationText = [self _prepareTruncationStringForDrawing:[self _composedTruncationText]]; } @@ -1107,6 +1185,8 @@ static NSAttributedString *DefaultTruncationAttributedString() */ - (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange { + std::lock_guard l(_textLock); + // Check if we even have an additional truncation message. if (!_additionalTruncationMessage) { return NSMakeRange(NSNotFound, 0); @@ -1118,8 +1198,7 @@ static NSAttributedString *DefaultTruncationAttributedString() NSUInteger additionalTruncationMessageLength = _additionalTruncationMessage.length; // We get the location of the truncation token, then add the length of the // truncation attributed string +1 for the space between. - NSRange range = NSMakeRange(truncationTokenIndex + _truncationAttributedText.length + 1, additionalTruncationMessageLength); - return range; + return NSMakeRange(truncationTokenIndex + _truncationAttributedText.length + 1, additionalTruncationMessageLength); } /** @@ -1129,6 +1208,8 @@ static NSAttributedString *DefaultTruncationAttributedString() */ - (NSAttributedString *)_composedTruncationText { + std::lock_guard l(_textLock); + //If we have neither return the default if (!_additionalTruncationMessage && !_truncationAttributedText) { return _composedTruncationText; @@ -1157,6 +1238,8 @@ static NSAttributedString *DefaultTruncationAttributedString() */ - (NSAttributedString *)_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString { + std::lock_guard l(_textLock); + truncationString = ASCleanseAttributedStringOfCoreTextAttributes(truncationString); NSMutableAttributedString *truncationMutableString = [truncationString mutableCopy]; // Grab the attributes from the full string From f3a909324dd8a5faf8c87d7c575c45f31da1b34d Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Thu, 16 Jun 2016 11:43:48 -0700 Subject: [PATCH 16/51] Add lock to _rendererAttributes --- AsyncDisplayKit/ASTextNode.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index a7d00a99..595306d0 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -236,6 +236,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (ASTextKitAttributes)_rendererAttributes { + std::lock_guard l(_textLock); + return { .attributedString = _attributedText, .truncationAttributedString = _composedTruncationText, From 6bde8cdcf3a3b86d03a94b562f16726204a5a533 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Thu, 16 Jun 2016 15:59:33 -0700 Subject: [PATCH 17/51] Adress review comments - Add locking to _linkAttributeValueAtPoint:attributeName:range:inAdditionalTruncationMessage:forHighlighting: as we access the attributed text in there - Add locking to touchesBegan:withEvent: as we are accessing the [ASTextKitRenderer firstVisibleRange] - Add locking for highlightStyle - Move accessing delegate property access to instance variable access --- AsyncDisplayKit/ASTextNode.mm | 37 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 595306d0..aba0e7e8 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -68,6 +68,7 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation NSString *_highlightedLinkAttributeName; id _highlightedLinkAttributeValue; + ASTextNodeHighlightStyle _highlightStyle; NSRange _highlightRange; ASHighlightOverlayLayer *_activeHighlightLayer; @@ -195,7 +196,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // If we are view-backed and the delegate cares, support the long-press callback. SEL longPressCallback = @selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:); - if (!self.isLayerBacked && [self.delegate respondsToSelector:longPressCallback]) { + if (!self.isLayerBacked && [_delegate respondsToSelector:longPressCallback]) { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_handleLongPress:)]; _longPressGestureRecognizer.cancelsTouchesInView = self.longPressCancelsTouches; _longPressGestureRecognizer.delegate = self; @@ -258,7 +259,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize { if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { - // Our bounds of frame have changed to a size that is not identical to our constraining size, + // Our bounds have changed to a size that is not identical to our constraining size, // so our previous layout information is invalid, and TextKit may draw at the // incorrect origin. { @@ -489,6 +490,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; { ASDisplayNodeAssertMainThread(); + std::lock_guard l(_textLock); + ASTextKitRenderer *renderer = [self _renderer]; NSRange visibleRange = renderer.firstVisibleRange; NSAttributedString *attributedString = _attributedText; @@ -590,8 +593,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } // Ask our delegate if a long-press on an attribute is relevant - if ([self.delegate respondsToSelector:@selector(textNode:shouldLongPressLinkAttribute:value:atPoint:)]) { - return [self.delegate textNode:self + if ([_delegate respondsToSelector:@selector(textNode:shouldLongPressLinkAttribute:value:atPoint:)]) { + return [_delegate textNode:self shouldLongPressLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:[gestureRecognizer locationInView:self.view]]; @@ -612,6 +615,20 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Highlighting +- (ASTextNodeHighlightStyle)highlightStyle +{ + std::lock_guard l(_textLock); + + return _highlightStyle; +} + +- (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle +{ + std::lock_guard l(_textLock); + + _highlightStyle = highlightStyle; +} + - (NSRange)highlightRange { ASDisplayNodeAssertMainThread(); @@ -626,8 +643,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated { - ASDisplayNodeAssertMainThread(); - [self _setHighlightRange:highlightRange forAttributeName:nil value:nil animated:animated]; } @@ -885,6 +900,9 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); + + std::lock_guard l(_textLock); + [super touchesBegan:touches withEvent:event]; CGPoint point = [[touches anyObject] locationInView:self.view]; @@ -903,8 +921,7 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); if (inAdditionalTruncationMessage) { - ASTextKitRenderer *renderer = [self _renderer]; - NSRange visibleRange = renderer.firstVisibleRange; + NSRange visibleRange = [self _renderer].firstVisibleRange; NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange]; [self _setHighlightRange:truncationMessageRange forAttributeName:ASTextNodeTruncationTokenAttributeName value:nil animated:YES]; } else if (range.length && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) { @@ -972,9 +989,9 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI // Respond to long-press when it begins, not when it ends. if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { - if ([self.delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { + if ([_delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view]; - [self.delegate textNode:self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; + [_delegate textNode:self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; } } } From 9873f4bd7df1341e215e818f7d6f3d4f46813509 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Mon, 20 Jun 2016 16:40:10 -0700 Subject: [PATCH 18/51] Deprecate willDisplayNode:/didEndDisplayingNode: methods --- AsyncDisplayKit/ASCollectionView.h | 48 ++++++++++++++++++++--------- AsyncDisplayKit/ASCollectionView.mm | 6 ++++ AsyncDisplayKit/ASTableView.h | 48 ++++++++++++++++++++--------- AsyncDisplayKit/ASTableView.mm | 6 ++++ 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index fa39c62b..1ee140f1 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -430,19 +430,6 @@ NS_ASSUME_NONNULL_BEGIN @optional -- (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath; - -/** - * Informs the delegate that the collection view did remove the provided node from the view hierarchy. - * This may be caused by the node scrolling out of view, or by deleting the item - * or its containing section with @c deleteItemsAtIndexPaths: or @c deleteSections: . - * - * @param collectionView The sender. - * @param node The node which was removed from the view hierarchy. - * @param indexPath The index path at which the node was located before it was removed. - */ -- (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath; - /** * Receive a message that the collectionView is near the end of its data set and more data should be fetched if * necessary. @@ -475,10 +462,43 @@ NS_ASSUME_NONNULL_BEGIN * Informs the delegate that the collection view did remove the node which was previously * at the given index path from the view hierarchy. * - * This method is deprecated. Use @c collectionView:didEndDisplayingNode:forItemAtIndexPath: instead. + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. */ - (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNodeForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; +/** + * Informs the delegate that the collection view will add the node + * at the given index path to the view hierarchy. + * + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. + */ +- (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; + +/** + * Informs the delegate that the collection view did remove the provided node from the view hierarchy. + * This may be caused by the node scrolling out of view, or by deleting the item + * or its containing section with @c deleteItemsAtIndexPaths: or @c deleteSections: . + * + * @param collectionView The sender. + * @param node The node which was removed from the view hierarchy. + * @param indexPath The index path at which the node was located before it was removed. + * + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. + */ +- (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; + @end /** diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 821e21dd..e0321a15 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -605,9 +605,12 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASCellNode *cellNode = [cell node]; cellNode.scrollView = collectionView; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateCollectionViewWillDisplayNodeForItemAtIndexPath) { [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; } +#pragma clang diagnostic pop [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; @@ -625,10 +628,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASCellNode *cellNode = [cell node]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateCollectionViewDidEndDisplayingNodeForItemAtIndexPath) { ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; } +#pragma clang diagnostic pop if ([_cellsForVisibilityUpdates containsObject:cell]) { [_cellsForVisibilityUpdates removeObject:cell]; diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index 4a6d6780..a7971ee4 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -390,19 +390,6 @@ NS_ASSUME_NONNULL_BEGIN @optional -- (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath; - -/** - * Informs the delegate that the table view did remove the provided node from the view hierarchy. - * This may be caused by the node scrolling out of view, or by deleting the row - * or its containing section with @c deleteRowsAtIndexPaths:withRowAnimation: or @c deleteSections:withRowAnimation: . - * - * @param tableView The sender. - * @param node The node which was removed from the view hierarchy. - * @param indexPath The index path at which the node was located before the removal. - */ -- (void)tableView:(ASTableView *)tableView didEndDisplayingNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath; - /** * Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary. * @@ -430,14 +417,47 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)shouldBatchFetchForTableView:(ASTableView *)tableView; +/** + * Informs the delegate that the table view did remove the provided node from the view hierarchy. + * This may be caused by the node scrolling out of view, or by deleting the row + * or its containing section with @c deleteRowsAtIndexPaths:withRowAnimation: or @c deleteSections:withRowAnimation: . + * + * @param tableView The sender. + * @param node The node which was removed from the view hierarchy. + * @param indexPath The index path at which the node was located before the removal. + * + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. + */ +- (void)tableView:(ASTableView *)tableView didEndDisplayingNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; + /** * Informs the delegate that the table view did remove the node which was previously * at the given index path from the view hierarchy. * - * This method is deprecated. Use @c tableView:didEndDisplayingNode:forRowAtIndexPath: instead. + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. */ - (void)tableView:(ASTableView *)tableView didEndDisplayingNodeForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; +/** + * Informs the delegate that the table view will add the node + * at the given index path to the view hierarchy. + * + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + * + * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. + */ +- (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; + @end @protocol ASTableViewDelegate diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index b8a174bb..a0bfd98b 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -623,9 +623,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASCellNode *cellNode = [cell node]; cellNode.scrollView = tableView; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateTableViewWillDisplayNodeForRowAtIndexPath) { [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; } +#pragma clang diagnostic pop [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; @@ -648,10 +651,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateTableViewDidEndDisplayingNodeForRowAtIndexPath) { ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; } +#pragma clang diagnostic pop if ([_cellsForVisibilityUpdates containsObject:cell]) { [_cellsForVisibilityUpdates removeObject:cell]; From 1198f114f2f0bfb0154ae9218119c5d4b09db59b Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Mon, 20 Jun 2016 20:17:51 -0700 Subject: [PATCH 19/51] [ASDK Management] PR / Issue Checklists (#1752) * [ASDK Management] PR / Issue Checklists * Update ISSUE_TEMPLATE.md * Remove PR template, add GitHub rules, modify issue template --- .github/GITHUB_RULES.md | 61 +++++++++++++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE.md | 3 ++ 2 files changed, 64 insertions(+) create mode 100644 .github/GITHUB_RULES.md create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/GITHUB_RULES.md b/.github/GITHUB_RULES.md new file mode 100644 index 00000000..156168ac --- /dev/null +++ b/.github/GITHUB_RULES.md @@ -0,0 +1,61 @@ +### Contribute to ASDK's Friendly Reputation + +ASDK has earned its reputation as an exceptionally welcoming place for newbie & experienced developers alike through the extra time Scott takes to thank _everyone_ who posts a question, bug, feature request or PR, for their time and contribution to the project, no matter how large the contribution (or silly the question). + +###PR Reviewing + +Merge permissions granted to Scott Goodson (@appleguy), Michael Schneider (@maicki), Adlai Holler (@Adlai-Holler) + +**PR Type** | **Required Reviewers** +--- | --- +Documentation | Anyone +Bug Fix | 2 (external PR) or 1 (internal PR) of the following (Scott, Michael, Adlai, Levi) +Refactoring | 1-3 depending on size / author familiarity with feature +New API | Scott + component owner + 1 additional +Breaking API | Scott + component owner + 1 additional + +**Component** | **Experts For Reviewing** +--- | --- +ASTextNode + subclasses | Ricky / Oliver +ASImageNode + subclasses | Garrett / Scott / Michael +ASDataController / Table / Collection | Michael +ASRangeController | Scott +ASLayout | Huy +ASDisplayNode | Garret / Michael / Levi +ASVideoNode | #asvideonode channel + +###PR Merging + +BE CAUTIOUS, DON'T CAUSE A REGRESSION + +Try to include as much as possible: +- Description / Screenshots +- Motivation & Context +- Methods of testing / Sample app +- What type of change it is (bug fix, new feature, breaking change) +- Tag @hannahmbanana on any documentation needs* +- Title the PR with the component in brackets - e.g. "[ASTextNode] fix threading issues..." +- New files need to include the required Facebook licensing header info. +- For future viewers / potential contributors, try to describe why this PR is helpful / useful / awesome / makes an impact on the current or future community + +###What stays on GitHub vs goes to Ship? + +GitHub: +- active bugs +- active community discussions +- unresolved community questions +- open issue about slack channel +- open issue with list of “up-for-grabs” tasks to get involved + +Ship: +- feature requests +- documentation requests +- performance optimizations / refactoring + +Comment for moving to Ship: + +@\ The community is planning an exciting long term road map for the project and getting organized around how to deliver these feature requests. + +If you are interested in helping contribute to this component or any other, don’t hesitate to send us an email at AsyncDisplayKit@gmail.com or ping us on ASDK's Slack (#1582). If you would like to contribute for a few weeks, we can also add you to our Ship bug tracker so that you can see what everyone is working on and actively coordinate with us. + +As always, keep filing issues and submitting pull requests here on Github and we will only move things to the new tracker if they require long term coordination. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..f912bc6b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ + \ No newline at end of file From c32e2f838353499007ca273c176ed29b23c3da9a Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Mon, 20 Jun 2016 20:18:44 -0700 Subject: [PATCH 20/51] If the animated image is ready, start animating it as soon as it's set (#1724) --- AsyncDisplayKit/ASImageNode+AnimatedImage.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm index 7df743c3..e51305a2 100644 --- a/AsyncDisplayKit/ASImageNode+AnimatedImage.mm +++ b/AsyncDisplayKit/ASImageNode+AnimatedImage.mm @@ -45,6 +45,10 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; }; } + if (animatedImage.playbackReady) { + [self animatedImageFileReady]; + } + animatedImage.playbackReadyCallback = ^{ [weakSelf animatedImageFileReady]; }; From 92b412b73ff55b191cf2e9cd7681503be939816a Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Mon, 20 Jun 2016 21:44:48 -0700 Subject: [PATCH 21/51] [GitHub Mgmt] Updated Issue Checklist (#1787) * [ASDK Management] PR / Issue Checklists * Update ISSUE_TEMPLATE.md * Remove PR template, add GitHub rules, modify issue template * Fixed Issue Template --- .github/ISSUE_TEMPLATE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index f912bc6b..03194fca 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,4 @@ - \ No newline at end of file +// The more information you include, the faster we can help you out! +// Please include: a sample project or screenshots, code snippets +// AsyncDisplayKit version, and/or backtraces for any crashes (> bt all). +// Please delete these lines before posting. Thanks! \ No newline at end of file From 21d0f1e10d7892e12f58a400a09ab4e18230f137 Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Tue, 21 Jun 2016 01:02:40 -0700 Subject: [PATCH 22/51] [Travis CI] Build Optimizations (#1743) * [Travis CI] Build Optimizations * update cocoapods versionm * test sharing derived data directories * Hack to reduce travis run time * Remove test file * Add missing slashes --- .travis.yml | 9 ++++-- build.sh | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2cb8a5fa..2b7982d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: objective-c +cache: + - bundler + - cocoapods osx_image: xcode7.3 git: depth: 10 @@ -6,7 +9,7 @@ before_install: - brew update - brew outdated xctool || brew upgrade xctool - brew outdated carthage || brew upgrade carthage - - gem install cocoapods -v 0.38.2 + - gem install cocoapods -v 1.0.1 - gem install xcpretty - gem install xcpretty-travis-formatter # - gem install slather @@ -14,7 +17,9 @@ before_install: install: echo "<3" env: - MODE=tests - - MODE=examples + - MODE=examples-pt1 + - MODE=examples-pt2 + - MODE=examples-pt3 - MODE=life-without-cocoapods - MODE=framework script: ./build.sh $MODE diff --git a/build.sh b/build.sh index a32640bb..02a2ddb4 100755 --- a/build.sh +++ b/build.sh @@ -36,21 +36,102 @@ if [ "$MODE" = "tests" ]; then exit 0 fi -if [ "$MODE" = "examples" ]; then +if [ "$MODE" = "examples-pt1" ]; then echo "Verifying that all AsyncDisplayKit examples compile." - for example in examples/*/; do - echo "Building $example." + for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -6 | head); do + echo "Building (examples-pt1) $example." if [ -f "${example}/Podfile" ]; then echo "Using CocoaPods" pod install --project-directory=$example set -o pipefail && xcodebuild \ - -workspace "${example}Sample.xcworkspace" \ + -workspace "${example}/Sample.xcworkspace" \ -scheme Sample \ -sdk "$SDK" \ -destination "$PLATFORM" \ + -derivedDataPath ~/ \ + build | xcpretty $FORMATTER + elif [ -f "${example}/Cartfile" ]; then + echo "Using Carthage" + local_repo=`pwd` + current_branch=`git rev-parse --abbrev-ref HEAD` + cd $example + + echo "git \"file://${local_repo}\" \"${current_branch}\"" > "Cartfile" + carthage update --platform iOS + + set -o pipefail && xcodebuild \ + -project "Sample.xcodeproj" \ + -scheme Sample \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build | xcpretty $FORMATTER + + cd ../.. + fi + done + trap - EXIT + exit 0 +fi + +if [ "$MODE" = "examples-pt2" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + + for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -12 | tail -6 | head); do + echo "Building $example (examples-pt2)." + + if [ -f "${example}/Podfile" ]; then + echo "Using CocoaPods" + pod install --project-directory=$example + + set -o pipefail && xcodebuild \ + -workspace "${example}/Sample.xcworkspace" \ + -scheme Sample \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + -derivedDataPath ~/ \ + build | xcpretty $FORMATTER + elif [ -f "${example}/Cartfile" ]; then + echo "Using Carthage" + local_repo=`pwd` + current_branch=`git rev-parse --abbrev-ref HEAD` + cd $example + + echo "git \"file://${local_repo}\" \"${current_branch}\"" > "Cartfile" + carthage update --platform iOS + + set -o pipefail && xcodebuild \ + -project "Sample.xcodeproj" \ + -scheme Sample \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build | xcpretty $FORMATTER + + cd ../.. + fi + done + trap - EXIT + exit 0 +fi + +if [ "$MODE" = "examples-pt3" ]; then + echo "Verifying that all AsyncDisplayKit examples compile." + + for example in $((find ./examples -type d -maxdepth 1 \( ! -iname ".*" \)) | head -7 | head); do + echo "Building $example (examples-pt3)." + + if [ -f "${example}/Podfile" ]; then + echo "Using CocoaPods" + pod install --project-directory=$example + + set -o pipefail && xcodebuild \ + -workspace "${example}/Sample.xcworkspace" \ + -scheme Sample \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + -derivedDataPath ~/ \ build | xcpretty $FORMATTER elif [ -f "${example}/Cartfile" ]; then echo "Using Carthage" From 3fee0810d5a840e8ce9105da4ce2f8b02a902944 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 21 Jun 2016 11:36:54 +0100 Subject: [PATCH 23/51] [ASVideoNode] Ensure that both ASVideoNode and ASNetworkImageNode delegate methods are called for ASVideoNode observers --- AsyncDisplayKit/ASVideoNode.mm | 2 ++ AsyncDisplayKitTests/ASVideoNodeTests.m | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 8290e9da..379a7095 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -453,7 +453,9 @@ static NSString * const kStatus = @"status"; - (void)setDelegate:(id)delegate { + [super setDelegate:delegate]; _delegate = delegate; + if (_delegate == nil) { memset(&_delegateFlags, 0, sizeof(_delegateFlags)); } else { diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index d2ba042d..f7c4a6b0 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -15,7 +15,7 @@ #import #import -@interface ASVideoNodeTests : XCTestCase +@interface ASVideoNodeTests : XCTestCase { ASVideoNode *_videoNode; AVURLAsset *_firstAsset; @@ -25,10 +25,18 @@ } @end +@interface ASNetworkImageNode () { + @public __weak id _delegate; +} +@end + + @interface ASVideoNode () { ASDisplayNode *_playerNode; AVPlayer *_player; } + + @property (atomic, readwrite) ASInterfaceState interfaceState; @property (atomic, readonly) ASDisplayNode *spinner; @property (atomic, readwrite) ASDisplayNode *playerNode; @@ -405,4 +413,18 @@ XCTAssertNil(_videoNode.image); } +- (void)testDelegateProperlySetForClassHierarchy +{ + _videoNode.delegate = self; + + XCTAssertTrue([_videoNode.delegate conformsToProtocol:@protocol(ASVideoNodeDelegate)]); + XCTAssertTrue([_videoNode.delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]); + XCTAssertTrue([((ASNetworkImageNode*)_videoNode).delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]); + XCTAssertTrue([((ASNetworkImageNode*)_videoNode)->_delegate conformsToProtocol:@protocol(ASNetworkImageNodeDelegate)]); + + XCTAssertEqual(_videoNode.delegate, self); + XCTAssertEqual(((ASNetworkImageNode*)_videoNode).delegate, self); + XCTAssertEqual(((ASNetworkImageNode*)_videoNode)->_delegate, self); +} + @end From c14bbf6005b29043a024468d5b26dd3dc6bf2334 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 21 Jun 2016 09:16:54 -0700 Subject: [PATCH 24/51] Undeprecate the methods. Just inform users about the index path issue --- AsyncDisplayKit/ASCollectionView.h | 61 +++++++++++++++--------------- AsyncDisplayKit/ASTableView.h | 55 ++++++++++++--------------- 2 files changed, 55 insertions(+), 61 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index 1ee140f1..a2d5e2ec 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -430,6 +430,34 @@ NS_ASSUME_NONNULL_BEGIN @optional +/** + * Informs the delegate that the collection view will add the node + * at the given index path to the view hierarchy. + * + * @param collectionView The sender. + * @param indexPath The index path of the item that will be displayed. + * + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + */ +- (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Informs the delegate that the collection view did remove the provided node from the view hierarchy. + * This may be caused by the node scrolling out of view, or by deleting the item + * or its containing section with @c deleteItemsAtIndexPaths: or @c deleteSections: . + * + * @param collectionView The sender. + * @param node The node which was removed from the view hierarchy. + * @param indexPath The index path at which the node was located before it was removed. + * + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + */ +- (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath; + /** * Receive a message that the collectionView is near the end of its data set and more data should be fetched if * necessary. @@ -462,43 +490,14 @@ NS_ASSUME_NONNULL_BEGIN * Informs the delegate that the collection view did remove the node which was previously * at the given index path from the view hierarchy. * - * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path * passed into this method may not correspond to the same item in your data source * if your data source has been updated since the last edit was processed. * - * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. + * This method is deprecated. Use @c collectionView:didEndDisplayingNode:forItemAtIndexPath: instead. */ - (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNodeForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; -/** - * Informs the delegate that the collection view will add the node - * at the given index path to the view hierarchy. - * - * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path - * passed into this method may not correspond to the same item in your data source - * if your data source has been updated since the last edit was processed. - * - * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. - */ -- (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; - -/** - * Informs the delegate that the collection view did remove the provided node from the view hierarchy. - * This may be caused by the node scrolling out of view, or by deleting the item - * or its containing section with @c deleteItemsAtIndexPaths: or @c deleteSections: . - * - * @param collectionView The sender. - * @param node The node which was removed from the view hierarchy. - * @param indexPath The index path at which the node was located before it was removed. - * - * @warning AsyncDisplayKit processes collection view edits asynchronously. The index path - * passed into this method may not correspond to the same item in your data source - * if your data source has been updated since the last edit was processed. - * - * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. - */ -- (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; - @end /** diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index a7971ee4..4ab48771 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -390,6 +390,30 @@ NS_ASSUME_NONNULL_BEGIN @optional +/** + * Informs the delegate that the table view will add the node + * at the given index path to the view hierarchy. + * + * @param tableView The sender. + * @param indexPath The index path of the row that will be displayed. + * + * @warning AsyncDisplayKit processes table view edits asynchronously. The index path + * passed into this method may not correspond to the same item in your data source + * if your data source has been updated since the last edit was processed. + */ +- (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Informs the delegate that the table view did remove the provided node from the view hierarchy. + * This may be caused by the node scrolling out of view, or by deleting the row + * or its containing section with @c deleteRowsAtIndexPaths:withRowAnimation: or @c deleteSections:withRowAnimation: . + * + * @param tableView The sender. + * @param node The node which was removed from the view hierarchy. + * @param indexPath The index path at which the node was located before the removal. + */ +- (void)tableView:(ASTableView *)tableView didEndDisplayingNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath; + /** * Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary. * @@ -417,23 +441,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)shouldBatchFetchForTableView:(ASTableView *)tableView; -/** - * Informs the delegate that the table view did remove the provided node from the view hierarchy. - * This may be caused by the node scrolling out of view, or by deleting the row - * or its containing section with @c deleteRowsAtIndexPaths:withRowAnimation: or @c deleteSections:withRowAnimation: . - * - * @param tableView The sender. - * @param node The node which was removed from the view hierarchy. - * @param indexPath The index path at which the node was located before the removal. - * - * @warning AsyncDisplayKit processes table view edits asynchronously. The index path - * passed into this method may not correspond to the same item in your data source - * if your data source has been updated since the last edit was processed. - * - * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. - */ -- (void)tableView:(ASTableView *)tableView didEndDisplayingNode:(ASCellNode *)node forRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; - /** * Informs the delegate that the table view did remove the node which was previously * at the given index path from the view hierarchy. @@ -442,22 +449,10 @@ NS_ASSUME_NONNULL_BEGIN * passed into this method may not correspond to the same item in your data source * if your data source has been updated since the last edit was processed. * - * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. + * This method is deprecated. Use @c tableView:didEndDisplayingNode:forRowAtIndexPath: instead. */ - (void)tableView:(ASTableView *)tableView didEndDisplayingNodeForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; -/** - * Informs the delegate that the table view will add the node - * at the given index path to the view hierarchy. - * - * @warning AsyncDisplayKit processes table view edits asynchronously. The index path - * passed into this method may not correspond to the same item in your data source - * if your data source has been updated since the last edit was processed. - * - * This method is deprecated. Use @c visibleStateDidChange: on the cell node instead. - */ -- (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath ASDISPLAYNODE_DEPRECATED; - @end @protocol ASTableViewDelegate From fcb69382243fb37f3183c7ec3ef05bb2cd376808 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 21 Jun 2016 09:20:40 -0700 Subject: [PATCH 25/51] Revert deprecated method warning suppression --- AsyncDisplayKit/ASCollectionView.mm | 6 ------ AsyncDisplayKit/ASTableView.mm | 6 ------ 2 files changed, 12 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index e0321a15..821e21dd 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -605,12 +605,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASCellNode *cellNode = [cell node]; cellNode.scrollView = collectionView; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateCollectionViewWillDisplayNodeForItemAtIndexPath) { [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; } -#pragma clang diagnostic pop [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; @@ -628,13 +625,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASCellNode *cellNode = [cell node]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateCollectionViewDidEndDisplayingNodeForItemAtIndexPath) { ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; } -#pragma clang diagnostic pop if ([_cellsForVisibilityUpdates containsObject:cell]) { [_cellsForVisibilityUpdates removeObject:cell]; diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index a0bfd98b..b8a174bb 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -623,12 +623,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASCellNode *cellNode = [cell node]; cellNode.scrollView = tableView; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateTableViewWillDisplayNodeForRowAtIndexPath) { [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; } -#pragma clang diagnostic pop [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; @@ -651,13 +648,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" if (_asyncDelegateFlags.asyncDelegateTableViewDidEndDisplayingNodeForRowAtIndexPath) { ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; } -#pragma clang diagnostic pop if ([_cellsForVisibilityUpdates containsObject:cell]) { [_cellsForVisibilityUpdates removeObject:cell]; From 3384297c589695dc043142b26d2642e9ab85514c Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 19 Jun 2016 14:59:27 -0700 Subject: [PATCH 26/51] Move drawing parameters in ASTextNode and ASImageNode to structs --- AsyncDisplayKit/ASImageNode.mm | 140 ++++++++++++++++----------------- AsyncDisplayKit/ASTextNode.mm | 58 ++++++-------- 2 files changed, 95 insertions(+), 103 deletions(-) diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 0678a528..8609bf48 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -26,46 +26,18 @@ #import "ASInternalHelpers.h" #import "ASEqualityHelpers.h" -@interface _ASImageNodeDrawParameters : NSObject - -@property (nonatomic, retain) UIImage *image; -@property (nonatomic, assign) BOOL opaque; -@property (nonatomic, assign) CGRect bounds; -@property (nonatomic, assign) CGFloat contentsScale; -@property (nonatomic, strong) UIColor *backgroundColor; -@property (nonatomic, assign) UIViewContentMode contentMode; - -@end - -// TODO: eliminate explicit parameters with a set of keys copied from the node -@implementation _ASImageNodeDrawParameters - -- (instancetype)initWithImage:(UIImage *)image - bounds:(CGRect)bounds - opaque:(BOOL)opaque - contentsScale:(CGFloat)contentsScale - backgroundColor:(UIColor *)backgroundColor - contentMode:(UIViewContentMode)contentMode -{ - if (!(self = [self init])) - return nil; - - _image = image; - _opaque = opaque; - _bounds = bounds; - _contentsScale = contentsScale; - _backgroundColor = backgroundColor; - _contentMode = contentMode; - - return self; -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"<%@ : %p opaque:%@ bounds:%@ contentsScale:%.2f backgroundColor:%@ contentMode:%@>", [self class], self, @(self.opaque), NSStringFromCGRect(self.bounds), self.contentsScale, self.backgroundColor, ASDisplayNodeNSStringFromUIContentMode(self.contentMode)]; -} - -@end +struct ASImageNodeDrawParameters { + BOOL opaque; + CGRect bounds; + CGFloat contentsScale; + UIColor *backgroundColor; + UIViewContentMode contentMode; + BOOL cropEnabled; + BOOL forceUpscaling; + CGRect cropRect; + CGRect cropDisplayBounds; + asimagenode_modification_block_t imageModificationBlock; +}; @implementation ASImageNode { @@ -75,18 +47,22 @@ void (^_displayCompletionBlock)(BOOL canceled); ASDN::RecursiveMutex _imageLock; + // Drawing + ASImageNodeDrawParameters _drawParameter; + ASTextNode *_debugLabelNode; + // Cropping. BOOL _cropEnabled; // Defaults to YES. BOOL _forceUpscaling; //Defaults to NO. CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0) - CGRect _cropDisplayBounds; - - ASTextNode *_debugLabelNode; + CGRect _cropDisplayBounds; // Defaults to CGRectNull } @synthesize image = _image; @synthesize imageModificationBlock = _imageModificationBlock; +#pragma mark - NSObject + - (instancetype)init { if (!(self = [super init])) @@ -124,6 +100,8 @@ return nil; } +#pragma mark - Layout and Sizing + - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDN::MutexLocker l(_imageLock); @@ -136,6 +114,8 @@ return CGSizeZero; } +#pragma mark - Setter / Getter + - (void)setImage:(UIImage *)image { _imageLock.lock(); @@ -177,54 +157,72 @@ self.placeholderEnabled = placeholderColor != nil; } +#pragma mark - Drawing + - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { - return [[_ASImageNodeDrawParameters alloc] initWithImage:self.image - bounds:self.bounds - opaque:self.opaque - contentsScale:self.contentsScaleForDisplay - backgroundColor:self.backgroundColor - contentMode:self.contentMode]; + ASDN::MutexLocker l(_imageLock); + + _drawParameter = { + .bounds = self.bounds, + .opaque = self.opaque, + .contentsScale = _contentsScaleForDisplay, + .backgroundColor = self.backgroundColor, + .contentMode = self.contentMode, + .cropEnabled = _cropEnabled, + .forceUpscaling = _forceUpscaling, + .cropRect = _cropRect, + .cropDisplayBounds = _cropDisplayBounds, + .imageModificationBlock = _imageModificationBlock + }; + + return nil; } - (NSDictionary *)debugLabelAttributes { - return @{ NSFontAttributeName: [UIFont systemFontOfSize:15.0], - NSForegroundColorAttributeName: [UIColor redColor] }; + return @{ + NSFontAttributeName: [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor redColor] + }; } -- (UIImage *)displayWithParameters:(_ASImageNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +- (UIImage *)displayWithParameters:(id *)parameter isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled { - UIImage *image = parameters.image; - if (!image) { + UIImage *image = self.image; + if (image == nil) { return nil; } + CGRect drawParameterBounds = CGRectZero; BOOL forceUpscaling = NO; - BOOL cropEnabled = NO; - BOOL isOpaque = parameters.opaque; - UIColor *backgroundColor = parameters.backgroundColor; - UIViewContentMode contentMode = parameters.contentMode; + BOOL cropEnabled = YES; + BOOL isOpaque = NO; + UIColor *backgroundColor = nil; + UIViewContentMode contentMode = UIViewContentModeScaleAspectFill; CGFloat contentsScale = 0.0; CGRect cropDisplayBounds = CGRectZero; CGRect cropRect = CGRectZero; asimagenode_modification_block_t imageModificationBlock; - + + ASDN::MutexLocker l(_imageLock); { - ASDN::MutexLocker l(_imageLock); + ASImageNodeDrawParameters drawParameter = _drawParameter; - // FIXME: There is a small risk of these values changing between the main thread creation of drawParameters, and the execution of this method. - // We should package these up into the draw parameters object. Might be easiest to create a struct for the non-objects and make it one property. - cropEnabled = _cropEnabled; - forceUpscaling = _forceUpscaling; - contentsScale = _contentsScaleForDisplay; - cropDisplayBounds = _cropDisplayBounds; - cropRect = _cropRect; - imageModificationBlock = _imageModificationBlock; + drawParameterBounds = drawParameter.bounds; + forceUpscaling = drawParameter.forceUpscaling; + cropEnabled = drawParameter.cropEnabled; + isOpaque = drawParameter.opaque; + backgroundColor = drawParameter.backgroundColor; + contentMode = drawParameter.contentMode; + contentsScale = drawParameter.contentsScale; + cropDisplayBounds = drawParameter.cropDisplayBounds; + cropRect = drawParameter.cropRect; + imageModificationBlock = drawParameter.imageModificationBlock; } BOOL hasValidCropBounds = cropEnabled && !CGRectIsNull(cropDisplayBounds) && !CGRectIsEmpty(cropDisplayBounds); - CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : parameters.bounds); + CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds); ASDisplayNodeContextModifier preContextBlock = self.willDisplayNodeContentWithRenderingContext; ASDisplayNodeContextModifier postContextBlock = self.didDisplayNodeContentWithRenderingContext; @@ -359,7 +357,6 @@ } } -#pragma mark - - (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock { if (self.displaySuspended) { @@ -378,6 +375,7 @@ } #pragma mark - Cropping + - (BOOL)isCropEnabled { ASDN::MutexLocker l(_imageLock); @@ -462,6 +460,7 @@ } #pragma mark - Debug + - (void)layout { [super layout]; @@ -477,6 +476,7 @@ @end #pragma mark - Extras + extern asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor) { return ^(UIImage *originalImage) { diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index aba0e7e8..9fd9fafb 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -32,25 +32,10 @@ static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; -@interface ASTextNodeDrawParameters : NSObject - -@property (nonatomic, assign, readonly) CGRect bounds; -@property (nonatomic, strong, readonly) UIColor *backgroundColor; - -@end - -@implementation ASTextNodeDrawParameters - -- (instancetype)initWithBounds:(CGRect)bounds backgroundColor:(UIColor *)backgroundColor -{ - if (self = [super init]) { - _bounds = bounds; - _backgroundColor = backgroundColor; - } - return self; -} - -@end +struct ASTextNodeDrawParameter { + CGRect bounds; + UIColor *backgroundColor; +}; @interface ASTextNode () @@ -78,6 +63,8 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation ASTextKitRenderer *_renderer; + ASTextNodeDrawParameter _drawParameter; + UILongPressGestureRecognizer *_longPressGestureRecognizer; } @dynamic placeholderEnabled; @@ -431,27 +418,37 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Drawing -- (void)drawRect:(CGRect)bounds withParameters:(ASTextNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + _drawParameter = { + .backgroundColor = self.backgroundColor, + .bounds = self.bounds + }; + return nil; +} + +- (void)drawRect:(CGRect)bounds withParameters:(id )p isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; { std::lock_guard l(_textLock); + + ASTextNodeDrawParameter drawParameter = _drawParameter; + CGRect drawParameterBounds = drawParameter.bounds; + UIColor *backgroundColor = isRasterizing ? nil : drawParameter.backgroundColor; CGContextRef context = UIGraphicsGetCurrentContext(); ASDisplayNodeAssert(context, @"This is no good without a context."); CGContextSaveGState(context); - ASTextKitRenderer *renderer = [self _rendererWithBounds:parameters.bounds]; + ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds]; UIEdgeInsets shadowPadding = [self shadowPaddingWithRenderer:renderer]; - CGPoint boundsOrigin = parameters.bounds.origin; + CGPoint boundsOrigin = drawParameterBounds.origin; CGPoint textOrigin = CGPointMake(boundsOrigin.x - shadowPadding.left, boundsOrigin.y - shadowPadding.top); // Fill background - if (!isRasterizing) { - UIColor *backgroundColor = parameters.backgroundColor; - if (backgroundColor) { - [backgroundColor setFill]; - UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); - } + if (backgroundColor != nil) { + [backgroundColor setFill]; + UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); } // Draw shadow @@ -464,11 +461,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; CGContextRestoreGState(context); } -- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer -{ - return [[ASTextNodeDrawParameters alloc] initWithBounds:self.threadSafeBounds backgroundColor:self.backgroundColor]; -} - #pragma mark - Attributes - (id)linkAttributeValueAtPoint:(CGPoint)point From fc7cff333ece1ff4ed19ed158dcd66db1d5c609a Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 19 Jun 2016 14:59:39 -0700 Subject: [PATCH 27/51] Prevent subclassing of ASTextNode and ASImageNode --- AsyncDisplayKit/ASImageNode.mm | 10 ++++++++++ AsyncDisplayKit/ASTextNode.mm | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 8609bf48..024bf137 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -63,6 +63,16 @@ struct ASImageNodeDrawParameters { #pragma mark - NSObject ++ (void)initialize +{ + [super initialize]; + + if (self != [ASImageNode class]) { + // Prevent custom drawing in subclasses + ASDisplayNodeAssert(!ASSubclassOverridesClassSelector([ASImageNode class], self, @selector(displayWithParameters:isCancelled:)), @"Subclass %@ must not override displayWithParameters:isCancelled: method. Custom drawing in %@ subclass is not supported.", NSStringFromClass(self), NSStringFromClass([ASImageNode class])); + } +} + - (instancetype)init { if (!(self = [super init])) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 9fd9fafb..688da55c 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -71,6 +71,16 @@ struct ASTextNodeDrawParameter { #pragma mark - NSObject ++ (void)initialize +{ + [super initialize]; + + if (self != [ASTextNode class]) { + // Prevent custom drawing in subclasses + ASDisplayNodeAssert(!ASSubclassOverridesClassSelector([ASTextNode class], self, @selector(drawRect:withParameters:isCancelled:isRasterizing:)), @"Subclass %@ must not override drawRect:withParameters:isCancelled:isRasterizing: method. Custom drawing in %@ subclass is not supported.", NSStringFromClass(self), NSStringFromClass([ASTextNode class])); + } +} + static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (instancetype)init From e7f2edd183c57021beb335a25492e270cdb70ad7 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 20 Jun 2016 15:03:16 -0700 Subject: [PATCH 28/51] Add updating drawing parameter in ASTextNode Update drawing on demand if properties change and not on every drawing cycle. This should reduce the overhead to access properties from the view / layer for the drawing parameters. --- AsyncDisplayKit/ASTextNode.mm | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 688da55c..2efe1f5e 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -210,9 +210,16 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)setBounds:(CGRect)bounds { [super setBounds:bounds]; + [self updateDrawingParameter]; [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; } +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + [super setBackgroundColor:backgroundColor]; + [self updateDrawingParameter]; +} + #pragma mark - Renderer Management - (ASTextKitRenderer *)_renderer @@ -428,13 +435,14 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Drawing -- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +- (void)updateDrawingParameter { + std::lock_guard l(_textLock); + _drawParameter = { .backgroundColor = self.backgroundColor, .bounds = self.bounds }; - return nil; } - (void)drawRect:(CGRect)bounds withParameters:(id )p isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; From 74b9b6b49e2841855774a2977126162e04bf73aa Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 20 Jun 2016 15:21:52 -0700 Subject: [PATCH 29/51] Use threadSafeBounds instead of bounds to create drawing parameters --- AsyncDisplayKit/ASImageNode.mm | 2 +- AsyncDisplayKit/ASTextNode.mm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 024bf137..b669f533 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -174,7 +174,7 @@ struct ASImageNodeDrawParameters { ASDN::MutexLocker l(_imageLock); _drawParameter = { - .bounds = self.bounds, + .bounds = self.threadSafeBounds, .opaque = self.opaque, .contentsScale = _contentsScaleForDisplay, .backgroundColor = self.backgroundColor, diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 2efe1f5e..466a96ef 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -441,7 +441,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; _drawParameter = { .backgroundColor = self.backgroundColor, - .bounds = self.bounds + .bounds = self.threadSafeBounds }; } From 36e48cf340c2e667a7450e50ed80aefc41206a86 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 20 Jun 2016 16:41:42 -0700 Subject: [PATCH 30/51] Remove caching of _drawParameter and use bounds instead of threadSafeBounds --- AsyncDisplayKit/ASImageNode.mm | 2 +- AsyncDisplayKit/ASTextNode.mm | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index b669f533..024bf137 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -174,7 +174,7 @@ struct ASImageNodeDrawParameters { ASDN::MutexLocker l(_imageLock); _drawParameter = { - .bounds = self.threadSafeBounds, + .bounds = self.bounds, .opaque = self.opaque, .contentsScale = _contentsScaleForDisplay, .backgroundColor = self.backgroundColor, diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 466a96ef..95d44145 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -210,16 +210,9 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)setBounds:(CGRect)bounds { [super setBounds:bounds]; - [self updateDrawingParameter]; [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; } -- (void)setBackgroundColor:(UIColor *)backgroundColor -{ - [super setBackgroundColor:backgroundColor]; - [self updateDrawingParameter]; -} - #pragma mark - Renderer Management - (ASTextKitRenderer *)_renderer @@ -435,16 +428,18 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Drawing -- (void)updateDrawingParameter +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { std::lock_guard l(_textLock); _drawParameter = { .backgroundColor = self.backgroundColor, - .bounds = self.threadSafeBounds + .bounds = self.bounds }; + return nil; } + - (void)drawRect:(CGRect)bounds withParameters:(id )p isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; { std::lock_guard l(_textLock); From 2675204d5031fc776700bdfee8b3a05f133b2c8c Mon Sep 17 00:00:00 2001 From: ricky Date: Tue, 7 Jun 2016 10:07:59 -0700 Subject: [PATCH 31/51] [ASLayoutSpec.mm] Remove use of dictionary to hold children Converted the backing store of children to a std::vector. Subclass now have defined indexes instead of string keys to add children. --- .../Layout/ASBackgroundLayoutSpec.mm | 9 +-- AsyncDisplayKit/Layout/ASLayoutSpec.h | 16 ++--- AsyncDisplayKit/Layout/ASLayoutSpec.mm | 63 +++++++------------ AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm | 9 +-- 4 files changed, 41 insertions(+), 56 deletions(-) diff --git a/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm b/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm index e060aab0..00201a79 100644 --- a/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm @@ -14,7 +14,8 @@ #import "ASBaseDefines.h" #import "ASLayout.h" -static NSString * const kBackgroundChildKey = @"kBackgroundChildKey"; +static NSUInteger const kForegroundChildIndex = 0; +static NSUInteger const kBackgroundChildIndex = 1; @interface ASBackgroundLayoutSpec () @end @@ -28,7 +29,7 @@ static NSString * const kBackgroundChildKey = @"kBackgroundChildKey"; } ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); - [self setChild:child]; + [self setChild:child forIndex:kForegroundChildIndex]; self.background = background; return self; } @@ -63,12 +64,12 @@ static NSString * const kBackgroundChildKey = @"kBackgroundChildKey"; - (void)setBackground:(id)background { - [super setChild:background forIdentifier:kBackgroundChildKey]; + [super setChild:background forIndex:kBackgroundChildIndex]; } - (id)background { - return [super childForIdentifier:kBackgroundChildKey]; + return [super childForIndex:kBackgroundChildIndex]; } @end diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.h b/AsyncDisplayKit/Layout/ASLayoutSpec.h index 32e2d437..7d7e4bc5 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.h @@ -52,19 +52,19 @@ NS_ASSUME_NONNULL_BEGIN * * @param child A child to be added. * - * @param identifier An identifier associated with the child. + * @param index An index associated with the child. * * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the * responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, * only require a single child. * * For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) - * a subclass should use the setChild method to set the "primary" child. It can then use this method + * a subclass can use the setChild method to set the "primary" child. It should then use this method * to set any other required children. Ideally a subclass would hide this from the user, and use the - * setChild:forIdentifier: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild - * property that behind the scenes is calling setChild:forIdentifier:. + * setChild:forIndex: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild + * property that behind the scenes is calling setChild:forIndex:. */ -- (void)setChild:(id)child forIdentifier:(NSString *)identifier; +- (void)setChild:(id)child forIndex:(NSUInteger)index; /** * Adds childen to this layout spec. @@ -94,11 +94,11 @@ NS_ASSUME_NONNULL_BEGIN - (nullable id)child; /** - * Returns the child added to this layout spec using the given identifier. + * Returns the child added to this layout spec using the given index. * - * @param identifier An identifier associated withe the child. + * @param index An identifier associated withe the child. */ -- (nullable id)childForIdentifier:(NSString *)identifier; +- (nullable id)childForIndex:(NSUInteger)index; /** * Returns all children added to this layout spec. diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.mm b/AsyncDisplayKit/Layout/ASLayoutSpec.mm index e7eb57ba..db522daa 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.mm @@ -25,9 +25,7 @@ @interface ASLayoutSpec() { ASEnvironmentState _environmentState; ASDN::RecursiveMutex _propertyLock; - - NSArray *_children; - NSMutableDictionary *_childrenWithIdentifier; + std::vector> _children; } @end @@ -45,7 +43,6 @@ } _isMutable = YES; _environmentState = ASEnvironmentStateMakeDefault(); - _children = [NSArray array]; return self; } @@ -102,14 +99,6 @@ return child; } -- (NSMutableDictionary *)childrenWithIdentifier -{ - if (!_childrenWithIdentifier) { - _childrenWithIdentifier = [NSMutableDictionary dictionary]; - } - return _childrenWithIdentifier; -} - - (void)setParent:(id)parent { // FIXME: Locking should be evaluated here. _parent is not widely used yet, though. @@ -126,34 +115,29 @@ if (child) { id finalLayoutable = [self layoutableToAddFromLayoutable:child]; if (finalLayoutable) { - _children = @[finalLayoutable]; + _children.insert(_children.begin(), finalLayoutable); [self propagateUpLayoutable:finalLayoutable]; } - } else { - // remove the only child - _children = [NSArray array]; + } else if (_children.size() > 0) { + _children.erase(_children.begin()); } } -- (void)setChild:(id)child forIdentifier:(NSString *)identifier +- (void)setChild:(id)child forIndex:(NSUInteger)index { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); if (child) { id finalLayoutable = [self layoutableToAddFromLayoutable:child]; - self.childrenWithIdentifier[identifier] = finalLayoutable; - if (finalLayoutable) { - _children = [_children arrayByAddingObject:finalLayoutable]; + if (index > _children.size()) { + _children.insert(_children.end(), finalLayoutable); + } else { + _children.insert(_children.begin() + index, finalLayoutable); } } else { - id oldChild = self.childrenWithIdentifier[identifier]; - if (oldChild) { - self.childrenWithIdentifier[identifier] = nil; - NSMutableArray *mutableChildren = [_children mutableCopy]; - [mutableChildren removeObject:oldChild]; - _children = [mutableChildren copy]; + if (_children.begin() + index < _children.end()) { + _children.erase(_children.begin() + index); } } - // TODO: Should we propagate up the layoutable at it could happen that multiple children will propagated up their // layout options and one child will overwrite values from another child // [self propagateUpLayoutable:finalLayoutable]; @@ -163,32 +147,31 @@ { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); - std::vector> finalChildren; + _children.clear(); for (id child in children) { - finalChildren.push_back([self layoutableToAddFromLayoutable:child]); - } - - _children = nil; - if (finalChildren.size() > 0) { - _children = [NSArray arrayWithObjects:&finalChildren[0] count:finalChildren.size()]; - } else { - _children = [NSArray array]; + _children.push_back([self layoutableToAddFromLayoutable:child]); } } -- (id)childForIdentifier:(NSString *)identifier +- (id)childForIndex:(NSUInteger)index { - return self.childrenWithIdentifier[identifier]; + if (index < _children.size()) { + return _children[index]; + } + return nil; } - (id)child { - return [_children firstObject]; + if (_children.size() > 0) { + return _children[0]; + } + return nil; } - (NSArray *)children { - return _children; + return [NSArray arrayWithObjects:&_children[0] count:_children.size()]; } #pragma mark - ASEnvironment diff --git a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm index 35a686db..669a5ad9 100644 --- a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm @@ -14,7 +14,8 @@ #import "ASBaseDefines.h" #import "ASLayout.h" -static NSString * const kOverlayChildKey = @"kOverlayChildKey"; +static NSUInteger const kUnderlayChildIndex = 0; +static NSUInteger const kOverlayChildIndex = 1; @implementation ASOverlayLayoutSpec @@ -25,7 +26,7 @@ static NSString * const kOverlayChildKey = @"kOverlayChildKey"; } ASDisplayNodeAssertNotNil(child, @"Child that will be overlayed on shouldn't be nil"); self.overlay = overlay; - [self setChild:child]; + [self setChild:child forIndex:kUnderlayChildIndex]; return self; } @@ -36,12 +37,12 @@ static NSString * const kOverlayChildKey = @"kOverlayChildKey"; - (void)setOverlay:(id)overlay { - [super setChild:overlay forIdentifier:kOverlayChildKey]; + [super setChild:overlay forIndex:kOverlayChildIndex]; } - (id)overlay { - return [super childForIdentifier:kOverlayChildKey]; + return [super childForIndex:kOverlayChildIndex]; } /** From eb715e5836d673a5e139838bc7055406a870e528 Mon Sep 17 00:00:00 2001 From: ricky Date: Tue, 21 Jun 2016 14:04:48 -0700 Subject: [PATCH 32/51] hold children in a map instead of vector --- AsyncDisplayKit/Layout/ASLayoutSpec.mm | 37 +++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.mm b/AsyncDisplayKit/Layout/ASLayoutSpec.mm index db522daa..a366cc58 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.mm @@ -20,12 +20,15 @@ #import "ASTraitCollection.h" #import +#import #import +typedef std::map, std::less> ASChildMap; + @interface ASLayoutSpec() { ASEnvironmentState _environmentState; ASDN::RecursiveMutex _propertyLock; - std::vector> _children; + ASChildMap _children; } @end @@ -115,11 +118,11 @@ if (child) { id finalLayoutable = [self layoutableToAddFromLayoutable:child]; if (finalLayoutable) { - _children.insert(_children.begin(), finalLayoutable); + _children[0] = finalLayoutable; [self propagateUpLayoutable:finalLayoutable]; } } else if (_children.size() > 0) { - _children.erase(_children.begin()); + _children.erase(0); } } @@ -128,15 +131,9 @@ ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); if (child) { id finalLayoutable = [self layoutableToAddFromLayoutable:child]; - if (index > _children.size()) { - _children.insert(_children.end(), finalLayoutable); - } else { - _children.insert(_children.begin() + index, finalLayoutable); - } + _children[index] = finalLayoutable; } else { - if (_children.begin() + index < _children.end()) { - _children.erase(_children.begin() + index); - } + _children.erase(index); } // TODO: Should we propagate up the layoutable at it could happen that multiple children will propagated up their // layout options and one child will overwrite values from another child @@ -148,9 +145,9 @@ ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); _children.clear(); - for (id child in children) { - _children.push_back([self layoutableToAddFromLayoutable:child]); - } + [children enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + _children[idx] = obj; + }]; } - (id)childForIndex:(NSUInteger)index @@ -163,15 +160,17 @@ - (id)child { - if (_children.size() > 0) { - return _children[0]; - } - return nil; + return _children[0]; } - (NSArray *)children { - return [NSArray arrayWithObjects:&_children[0] count:_children.size()]; + std::vector children; + for (ASChildMap::iterator it = _children.begin(); it != _children.end(); ++it ) { + children.push_back(it->second); + } + + return [NSArray arrayWithObjects:&children[0] count:children.size()]; } #pragma mark - ASEnvironment From af98d23bf01fbb919492cb79a4e3d277749159b6 Mon Sep 17 00:00:00 2001 From: ricky Date: Tue, 21 Jun 2016 14:48:43 -0700 Subject: [PATCH 33/51] michael's comments --- AsyncDisplayKit/Layout/ASLayoutSpec.h | 7 +++---- AsyncDisplayKit/Layout/ASLayoutSpec.mm | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.h b/AsyncDisplayKit/Layout/ASLayoutSpec.h index 7d7e4bc5..97f9fe1c 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.h @@ -40,10 +40,9 @@ NS_ASSUME_NONNULL_BEGIN * only require a single child. * * For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) - * a subclass should use this method to set the "primary" child. It can then use setChild:forIdentifier: - * to set any other required children. Ideally a subclass would hide this from the user, and use the - * setChild:forIdentifier: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild - * property that behind the scenes is calling setChild:forIdentifier:. + * a subclass should use this method to set the "primary" child. This is actually the same as calling + * setChild:forIdentifier:0. All other children should be set by defining convenience methods + * that call setChild:forIdentifier behind the scenes. */ - (void)setChild:(id)child; diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.mm b/AsyncDisplayKit/Layout/ASLayoutSpec.mm index a366cc58..b0bd8ee8 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.mm @@ -121,7 +121,7 @@ typedef std::map, std::less> ASCh _children[0] = finalLayoutable; [self propagateUpLayoutable:finalLayoutable]; } - } else if (_children.size() > 0) { + } else { _children.erase(0); } } From fcf2db79f863c4d375b451c7656239f5468a6f54 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 21 Jun 2016 18:46:08 -0700 Subject: [PATCH 34/51] [ASTableViewThrashTests] Initial commit --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 + .../Private/ASMultidimensionalArrayUtils.mm | 2 +- AsyncDisplayKitTests/ASTableViewThrashTests.m | 391 ++++++++++++++++++ 3 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 AsyncDisplayKitTests/ASTableViewThrashTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 8ed10cca..62c3baff 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -545,6 +545,7 @@ CC3B208C1C3F7A5400798563 /* ASWeakSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20881C3F7A5400798563 /* ASWeakSet.m */; }; CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */; }; CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */; }; + CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */; }; CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; }; CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; }; @@ -935,6 +936,7 @@ CC3B20881C3F7A5400798563 /* ASWeakSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSet.m; sourceTree = ""; }; CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSetTests.m; sourceTree = ""; }; CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBridgedPropertiesTests.mm; sourceTree = ""; }; + CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewThrashTests.m; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = ""; }; CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = ""; }; @@ -1201,6 +1203,7 @@ 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */, + CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */, 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */, 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */, 254C6B531BF8FF2A003EC431 /* ASTextKitTests.mm */, @@ -2171,6 +2174,7 @@ 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */, 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */, + CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */, 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */, AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */, diff --git a/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm b/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm index 26529129..5c7e4373 100644 --- a/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm +++ b/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm @@ -41,7 +41,7 @@ static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray } } -static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray *res) +static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray *res) { if (![obj isKindOfClass:[NSArray class]]) { [res addObject:curIndexPath]; diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m new file mode 100644 index 00000000..79cf4ef0 --- /dev/null +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -0,0 +1,391 @@ +// +// ASTableViewThrashTests.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 6/21/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +@import XCTest; +#import + +typedef NS_ENUM(NSUInteger, ASThrashChangeType) { + ASThrashReplaceItem, + ASThrashReplaceSection, + ASThrashDeleteItem, + ASThrashDeleteSection, + ASThrashInsertItem, + ASThrashInsertSection +}; + +#define USE_UIKIT_REFERENCE 1 +#define kInitialSectionCount 6 +#define kInitialItemCount 6 + +#if USE_UIKIT_REFERENCE +#define kCellReuseID @"ASThrashTestCellReuseID" +#endif + +static NSString *ASThrashArrayDescription(NSArray *array) { + NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; + NSInteger i = 0; + for (id obj in array) { + [str appendFormat:@"\t[%ld]: \"%@\",\n", i, obj]; + i += 1; + } + [str appendString:@")"]; + return str; +} +@interface ASThrashTestItem: NSObject +#if USE_UIKIT_REFERENCE +/// This is used to identify the row with the table view (UIKit only). +@property (nonatomic, readonly) CGFloat rowHeight; +#endif +@end + +@implementation ASThrashTestItem + +- (instancetype)init { + self = [super init]; + if (self != nil) { +#if USE_UIKIT_REFERENCE + _rowHeight = arc4random_uniform(500); +#endif + } + return self; +} + ++ (NSArray *)itemsWithCount:(NSInteger)count { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; + for (NSInteger i = 0; i < count; i += 1) { + [result addObject:[[ASThrashTestItem alloc] init]]; + } + return result; +} + +- (NSString *)description { +#if USE_UIKIT_REFERENCE + return [NSString stringWithFormat:@"", (unsigned long)self.rowHeight]; +#else + return [NSString stringWithFormat:@"", self]; +#endif +} + +@end + +@interface ASThrashTestSection: NSObject +@property (nonatomic, strong, readonly) NSMutableArray *items; +/// This is used to identify the section with the table view. +@property (nonatomic, readonly) CGFloat headerHeight; +@end + +@implementation ASThrashTestSection + +- (instancetype)initWithCount:(NSInteger)count { + self = [super init]; + if (self != nil) { + _items = [NSMutableArray arrayWithCapacity:count]; + _headerHeight = arc4random_uniform(500) + 1; + for (NSInteger i = 0; i < count; i++) { + [_items addObject:[ASThrashTestItem new]]; + } + } + return self; +} + +- (instancetype)init { + return [self initWithCount:0]; +} + ++ (NSMutableArray *)sectionsWithCount:(NSInteger)count { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; + for (NSInteger i = 0; i < count; i += 1) { + [result addObject:[[ASThrashTestSection alloc] initWithCount:kInitialItemCount]]; + } + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", (unsigned long)self.headerHeight, (unsigned long)self.items.count]; +} + +@end + +#if !USE_UIKIT_REFERENCE +@interface ASThrashTestNode: ASCellNode +@property (nonatomic, strong) ASThrashTestItem *item; +@end + +@implementation ASThrashTestNode + +@end +#endif + +@interface ASThrashDataSource: NSObject +#if USE_UIKIT_REFERENCE + +#else + +#endif +@property (nonatomic, strong, readonly) NSMutableArray *data; +@end + + +@implementation ASThrashDataSource + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _data = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + } + return self; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.data[section].items.count; +} + + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return self.data.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { + return self.data[section].headerHeight; +} + +#if USE_UIKIT_REFERENCE + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + return [tableView dequeueReusableCellWithIdentifier:kCellReuseID forIndexPath:indexPath]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; + return item.rowHeight; +} + +#else + +- (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { + ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; + return ^{ + ASThrashTestNode *tableNode = [[ASThrashTestNode alloc] init]; + tableNode.item = item; + return tableNode; + }; +} + +#endif + +@end + + +@implementation NSIndexSet (ASThrashHelpers) + +- (NSArray *)indexPathsInSection:(NSInteger)section { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [result addObject:[NSIndexPath indexPathForItem:idx inSection:section]]; + }]; + return result; +} + +@end + +@interface ASTableViewThrashTests: XCTestCase +@end + +@implementation ASTableViewThrashTests { + CGRect screenBounds; + ASThrashDataSource *ds; + UIWindow *window; +#if USE_UIKIT_REFERENCE + UITableView *tableView; +#else + ASTableNode *tableNode; + ASTableView *tableView; +#endif + + NSInteger minimumItemCount; + NSInteger minimumSectionCount; + float fickleness; +} + +- (void)setUp { + minimumItemCount = 5; + minimumSectionCount = 3; + fickleness = 0.1; + window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ds = [[ASThrashDataSource alloc] init]; +#if USE_UIKIT_REFERENCE + tableView = [[UITableView alloc] initWithFrame:window.bounds style:UITableViewStyleGrouped]; + [window addSubview:tableView]; + tableView.dataSource = ds; + tableView.delegate = ds; + [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; + [window layoutIfNeeded]; +#else + tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + tableNode.frame = window.bounds; + [window addSubnode:tableNode]; + tableNode.dataSource = ds; + tableNode.delegate = ds; + [tableView reloadDataImmediately]; +#endif + +} + +- (void)testInitialDataRead { + [self verifyTableStateWithHierarchy]; +} + +- (void)testThrashingWildly { + for (NSInteger i = 0; i < 100; i++) { + [self _testThrashingWildly]; + } +} + +- (void)_testThrashingWildly { + NSLog(@"Old data: %@", ASThrashArrayDescription(ds.data)); + NSMutableArray *deletedItems = [NSMutableArray array]; + NSMutableArray *replacedItems = [NSMutableArray array]; + NSMutableArray *insertedItems = [NSMutableArray array]; + NSInteger i = 0; + + // Randomly reload some items + for (ASThrashTestSection *section in ds.data) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; + [replacedItems addObject:indexes]; + i += 1; + } + + // Randomly replace some sections + NSMutableIndexSet *replacedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; + NSArray *replacingSections = [ASThrashTestSection sectionsWithCount:replacedSections.count]; + [ds.data replaceObjectsAtIndexes:replacedSections withObjects:replacingSections]; + + // Randomly delete some items + i = 0; + for (ASThrashTestSection *section in ds.data) { + if (section.items.count >= minimumItemCount) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; + + /// Cannot reload & delete the same item. + [indexes removeIndexes:replacedItems[i]]; + + [section.items removeObjectsAtIndexes:indexes]; + [deletedItems addObject:indexes]; + } else { + [deletedItems addObject:[NSMutableIndexSet indexSet]]; + } + i += 1; + } + + // Randomly delete some sections + NSMutableIndexSet *deletedSections = nil; + if (ds.data.count >= minimumSectionCount) { + deletedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; + + // Cannot reload & delete the same section. + [deletedSections removeIndexes:replacedSections]; + } else { + deletedSections = [NSMutableIndexSet indexSet]; + } + [ds.data removeObjectsAtIndexes:deletedSections]; + + // Randomly insert some sections + NSMutableIndexSet *insertedSections = [self randomIndexesLessThan:(ds.data.count + 1) probability:fickleness insertMode:YES]; + NSArray *newSections = [ASThrashTestSection sectionsWithCount:insertedSections.count]; + [ds.data insertObjects:newSections atIndexes:insertedSections]; + + // Randomly insert some items + i = 0; + for (ASThrashTestSection *section in ds.data) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items insertObjects:newItems atIndexes:indexes]; + [insertedItems addObject:indexes]; + i += 1; + } + + NSLog(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); + + // TODO: Submit changes in random order, randomly chunked up + + [tableView beginUpdates]; + i = 0; + for (NSIndexSet *indexes in insertedItems) { + NSArray *indexPaths = [indexes indexPathsInSection:i]; + NSLog(@"Requested to insert rows: %@", indexPaths); + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + i += 1; + } + + [tableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationNone]; + + [tableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationNone]; + + i = 0; + for (NSIndexSet *indexes in deletedItems) { + NSArray *indexPaths = [indexes indexPathsInSection:i]; + NSLog(@"Requested to delete rows: %@", indexPaths); + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + i += 1; + } + + i = 0; + for (NSIndexSet *indexes in replacedItems) { + NSArray *indexPaths = [indexes indexPathsInSection:i]; + NSLog(@"Requested to reload rows: %@", indexPaths); + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + i += 1; + } + + [tableView endUpdates]; +#if !USE_UIKIT_REFERENCE + [tableView waitUntilAllUpdatesAreCommitted]; +#endif + [self verifyTableStateWithHierarchy]; +} + +/// `insertMode` means that for each index selected, the max goes up by one. +- (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode { + NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; + u_int32_t cutoff = probability * 100; + for (NSInteger i = 0; i < max; i++) { + if (arc4random_uniform(100) < cutoff) { + [indexes addIndex:i]; + if (insertMode) { + max += 1; + } + } + } + return indexes; +} + +#pragma mark Helpers + +- (void)verifyTableStateWithHierarchy { + NSArray *data = [ds data]; + XCTAssertEqual(data.count, tableView.numberOfSections); + for (NSInteger i = 0; i < tableView.numberOfSections; i++) { + XCTAssertEqual([tableView numberOfRowsInSection:i], data[i].items.count); + XCTAssertEqual([tableView rectForHeaderInSection:i].size.height, data[i].headerHeight); + + for (NSInteger j = 0; j < [tableView numberOfRowsInSection:i]; j++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i]; + ASThrashTestItem *item = data[i].items[j]; +#if USE_UIKIT_REFERENCE + XCTAssertEqual([tableView rectForRowAtIndexPath:indexPath].size.height, item.rowHeight); +#else + ASThrashTestNode *node = (ASThrashTestNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqual(node.item, item); +#endif + } + } +} + +@end From 64835c0db7abe5d7f7300cf252939637234f77c1 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 13:04:47 -0700 Subject: [PATCH 35/51] [ASThrashTesting] It's working! + Cleanup --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 111 ++++++++++-------- 1 file changed, 64 insertions(+), 47 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 79cf4ef0..df163627 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -18,14 +18,20 @@ typedef NS_ENUM(NSUInteger, ASThrashChangeType) { ASThrashInsertSection }; -#define USE_UIKIT_REFERENCE 1 -#define kInitialSectionCount 6 -#define kInitialItemCount 6 +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +#define USE_UIKIT_REFERENCE 0 + +#define kInitialSectionCount 20 +#define kInitialItemCount 20 #if USE_UIKIT_REFERENCE #define kCellReuseID @"ASThrashTestCellReuseID" #endif +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" static NSString *ASThrashArrayDescription(NSArray *array) { NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; NSInteger i = 0; @@ -36,6 +42,8 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [str appendString:@")"]; return str; } +#pragma clang diagnostic pop + @interface ASThrashTestItem: NSObject #if USE_UIKIT_REFERENCE /// This is used to identify the row with the table view (UIKit only). @@ -75,6 +83,7 @@ static NSString *ASThrashArrayDescription(NSArray *array) { @interface ASThrashTestSection: NSObject @property (nonatomic, strong, readonly) NSMutableArray *items; + /// This is used to identify the section with the table view. @property (nonatomic, readonly) CGFloat headerHeight; @end @@ -170,9 +179,9 @@ static NSString *ASThrashArrayDescription(NSArray *array) { - (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { ASThrashTestItem *item = self.data[indexPath.section].items[indexPath.item]; return ^{ - ASThrashTestNode *tableNode = [[ASThrashTestNode alloc] init]; - tableNode.item = item; - return tableNode; + ASThrashTestNode *node = [[ASThrashTestNode alloc] init]; + node.item = item; + return node; }; } @@ -197,13 +206,11 @@ static NSString *ASThrashArrayDescription(NSArray *array) { @end @implementation ASTableViewThrashTests { - CGRect screenBounds; ASThrashDataSource *ds; UIWindow *window; #if USE_UIKIT_REFERENCE UITableView *tableView; #else - ASTableNode *tableNode; ASTableView *tableView; #endif @@ -226,7 +233,8 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; [window layoutIfNeeded]; #else - tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + ASTableNode *tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + tableView = tableNode.view; tableNode.frame = window.bounds; [window addSubnode:tableNode]; tableNode.dataSource = ds; @@ -240,18 +248,25 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [self verifyTableStateWithHierarchy]; } -- (void)testThrashingWildly { +- (void)DISABLED_testThrashingWildly { for (NSInteger i = 0; i < 100; i++) { + [self setUp]; [self _testThrashingWildly]; + [self tearDown]; } } - (void)_testThrashingWildly { - NSLog(@"Old data: %@", ASThrashArrayDescription(ds.data)); + [self verifyTableStateWithHierarchy]; + LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); + + // NOTE: This is not a deep copy, so these sections will still have their + // item counts updated throughout the update. + NSArray *oldSections = [ds.data copy]; + NSMutableArray *deletedItems = [NSMutableArray array]; NSMutableArray *replacedItems = [NSMutableArray array]; NSMutableArray *insertedItems = [NSMutableArray array]; - NSInteger i = 0; // Randomly reload some items for (ASThrashTestSection *section in ds.data) { @@ -259,7 +274,6 @@ static NSString *ASThrashArrayDescription(NSArray *array) { NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; [replacedItems addObject:indexes]; - i += 1; } // Randomly replace some sections @@ -268,32 +282,39 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [ds.data replaceObjectsAtIndexes:replacedSections withObjects:replacingSections]; // Randomly delete some items - i = 0; - for (ASThrashTestSection *section in ds.data) { + [ds.data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { if (section.items.count >= minimumItemCount) { NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; /// Cannot reload & delete the same item. - [indexes removeIndexes:replacedItems[i]]; + [indexes removeIndexes:replacedItems[idx]]; [section.items removeObjectsAtIndexes:indexes]; [deletedItems addObject:indexes]; } else { [deletedItems addObject:[NSMutableIndexSet indexSet]]; } - i += 1; - } + }]; // Randomly delete some sections NSMutableIndexSet *deletedSections = nil; if (ds.data.count >= minimumSectionCount) { deletedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; - - // Cannot reload & delete the same section. - [deletedSections removeIndexes:replacedSections]; } else { deletedSections = [NSMutableIndexSet indexSet]; } + // Cannot replace & delete the same section. + [deletedSections removeIndexes:replacedSections]; + + // Cannot delete/replace item in deleted/replaced section + [deletedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [replacedItems[idx] removeAllIndexes]; + [deletedItems[idx] removeAllIndexes]; + }]; + [replacedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [replacedItems[idx] removeAllIndexes]; + [deletedItems[idx] removeAllIndexes]; + }]; [ds.data removeObjectsAtIndexes:deletedSections]; // Randomly insert some sections @@ -302,47 +323,43 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [ds.data insertObjects:newSections atIndexes:insertedSections]; // Randomly insert some items - i = 0; for (ASThrashTestSection *section in ds.data) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; - NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; - [section.items insertObjects:newItems atIndexes:indexes]; - [insertedItems addObject:indexes]; - i += 1; + // Only insert items into the old sections – not replaced/inserted sections. + if ([oldSections containsObject:section]) { + NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items insertObjects:newItems atIndexes:indexes]; + [insertedItems addObject:indexes]; + } else { + [insertedItems addObject:[NSMutableIndexSet indexSet]]; + } } - NSLog(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); + LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); // TODO: Submit changes in random order, randomly chunked up [tableView beginUpdates]; - i = 0; - for (NSIndexSet *indexes in insertedItems) { - NSArray *indexPaths = [indexes indexPathsInSection:i]; - NSLog(@"Requested to insert rows: %@", indexPaths); + [insertedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - i += 1; - } + }]; [tableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationNone]; [tableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationNone]; - i = 0; - for (NSIndexSet *indexes in deletedItems) { - NSArray *indexPaths = [indexes indexPathsInSection:i]; - NSLog(@"Requested to delete rows: %@", indexPaths); - [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - i += 1; - } + [tableView reloadSections:replacedSections withRowAnimation:UITableViewRowAnimationNone]; - i = 0; - for (NSIndexSet *indexes in replacedItems) { - NSArray *indexPaths = [indexes indexPathsInSection:i]; - NSLog(@"Requested to reload rows: %@", indexPaths); + [deletedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [replacedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - i += 1; - } + }]; [tableView endUpdates]; #if !USE_UIKIT_REFERENCE From 290897cb566ce6b47fc098ea3bfb164bb139bcc8 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 13:06:16 -0700 Subject: [PATCH 36/51] [ASThrashTesting] Little more cleanup --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index df163627..b24542d7 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -9,15 +9,6 @@ @import XCTest; #import -typedef NS_ENUM(NSUInteger, ASThrashChangeType) { - ASThrashReplaceItem, - ASThrashReplaceSection, - ASThrashDeleteItem, - ASThrashDeleteSection, - ASThrashInsertItem, - ASThrashInsertSection -}; - //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) @@ -63,7 +54,7 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return self; } -+ (NSArray *)itemsWithCount:(NSInteger)count { ++ (NSMutableArray *)itemsWithCount:(NSInteger)count { NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; for (NSInteger i = 0; i < count; i += 1) { [result addObject:[[ASThrashTestItem alloc] init]]; @@ -93,11 +84,8 @@ static NSString *ASThrashArrayDescription(NSArray *array) { - (instancetype)initWithCount:(NSInteger)count { self = [super init]; if (self != nil) { - _items = [NSMutableArray arrayWithCapacity:count]; + _items = [ASThrashTestItem itemsWithCount:count]; _headerHeight = arc4random_uniform(500) + 1; - for (NSInteger i = 0; i < count; i++) { - [_items addObject:[ASThrashTestItem new]]; - } } return self; } From 15e03d85cf4ce0fb8ebdd22ca687f203fae4d0a2 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 14:19:59 -0700 Subject: [PATCH 37/51] [ASThrashTesting] Move update into an archivable object so we can replay tests --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 431 ++++++++++++------ 1 file changed, 280 insertions(+), 151 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index b24542d7..dc1ddbbd 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -16,6 +16,9 @@ #define kInitialSectionCount 20 #define kInitialItemCount 20 +#define kMinimumItemCount 5 +#define kMinimumSectionCount 3 +#define kFickleness 0.1 #if USE_UIKIT_REFERENCE #define kCellReuseID @"ASThrashTestCellReuseID" @@ -35,25 +38,40 @@ static NSString *ASThrashArrayDescription(NSArray *array) { } #pragma clang diagnostic pop -@interface ASThrashTestItem: NSObject -#if USE_UIKIT_REFERENCE -/// This is used to identify the row with the table view (UIKit only). -@property (nonatomic, readonly) CGFloat rowHeight; -#endif +static volatile int32_t ASThrashTestItemNextID = 1; +@interface ASThrashTestItem: NSObject +@property (nonatomic, readonly) NSInteger itemID; + +- (CGFloat)rowHeight; @end @implementation ASThrashTestItem ++ (BOOL)supportsSecureCoding { + return YES; +} + - (instancetype)init { self = [super init]; if (self != nil) { -#if USE_UIKIT_REFERENCE - _rowHeight = arc4random_uniform(500); -#endif + _itemID = OSAtomicIncrement32(&ASThrashTestItemNextID); } return self; } +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + _itemID = [aDecoder decodeIntegerForKey:@"itemID"]; + NSAssert(_itemID > 0, @"Failed to decode %@", self); + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeInteger:_itemID forKey:@"itemID"]; +} + + (NSMutableArray *)itemsWithCount:(NSInteger)count { NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; for (NSInteger i = 0; i < count; i += 1) { @@ -62,38 +80,28 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return result; } +- (CGFloat)rowHeight { + return (self.itemID % 400) ?: 44; +} + + - (NSString *)description { -#if USE_UIKIT_REFERENCE - return [NSString stringWithFormat:@"", (unsigned long)self.rowHeight]; -#else - return [NSString stringWithFormat:@"", self]; -#endif + return [NSString stringWithFormat:@"", (unsigned long)_itemID]; } @end -@interface ASThrashTestSection: NSObject +@interface ASThrashTestSection: NSObject @property (nonatomic, strong, readonly) NSMutableArray *items; +@property (nonatomic, readonly) NSInteger sectionID; -/// This is used to identify the section with the table view. -@property (nonatomic, readonly) CGFloat headerHeight; +- (CGFloat)headerHeight; @end +static volatile int32_t ASThrashTestSectionNextID = 1; @implementation ASThrashTestSection -- (instancetype)initWithCount:(NSInteger)count { - self = [super init]; - if (self != nil) { - _items = [ASThrashTestItem itemsWithCount:count]; - _headerHeight = arc4random_uniform(500) + 1; - } - return self; -} - -- (instancetype)init { - return [self initWithCount:0]; -} - +/// Create an array of sections with the given count + (NSMutableArray *)sectionsWithCount:(NSInteger)count { NSMutableArray *result = [NSMutableArray arrayWithCapacity:count]; for (NSInteger i = 0; i < count; i += 1) { @@ -102,8 +110,59 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return result; } +- (instancetype)initWithCount:(NSInteger)count { + self = [super init]; + if (self != nil) { + _sectionID = OSAtomicIncrement32(&ASThrashTestSectionNextID); + _items = [ASThrashTestItem itemsWithCount:count]; + } + return self; +} + +- (instancetype)init { + return [self initWithCount:0]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + _items = [aDecoder decodeObjectOfClass:[NSArray class] forKey:@"items"]; + _sectionID = [aDecoder decodeIntegerForKey:@"sectionID"]; + NSAssert(_sectionID > 0, @"Failed to decode %@", self); + } + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:_items forKey:@"items"]; + [aCoder encodeInteger:_sectionID forKey:@"sectionID"]; +} + +- (CGFloat)headerHeight { + return self.sectionID % 400 ?: 44; +} + - (NSString *)description { - return [NSString stringWithFormat:@"", (unsigned long)self.headerHeight, (unsigned long)self.items.count]; + return [NSString stringWithFormat:@"
", (unsigned long)_sectionID, (unsigned long)self.items.count]; +} + +- (id)copyWithZone:(NSZone *)zone { + ASThrashTestSection *copy = [[ASThrashTestSection alloc] init]; + copy->_sectionID = _sectionID; + copy->_items = [_items mutableCopy]; + return copy; +} + +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[ASThrashTestSection class]]) { + return [(ASThrashTestSection *)object sectionID] == _sectionID; + } else { + return NO; + } } @end @@ -188,6 +247,195 @@ static NSString *ASThrashArrayDescription(NSArray *array) { return result; } +/// `insertMode` means that for each index selected, the max goes up by one. ++ (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode { + NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; + u_int32_t cutoff = probability * 100; + for (NSInteger i = 0; i < max; i++) { + if (arc4random_uniform(100) < cutoff) { + [indexes addIndex:i]; + if (insertMode) { + max += 1; + } + } + } + return indexes; +} + +@end + +static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; + +@interface ASThrashUpdate : NSObject +@property (nonatomic, strong, readonly) NSMutableArray *oldData; +@property (nonatomic, strong, readonly) NSMutableArray *data; +@property (nonatomic, strong, readonly) NSMutableIndexSet *deletedSectionIndexes; +@property (nonatomic, strong, readonly) NSMutableIndexSet *replacedSectionIndexes; +/// The sections used to replace the replaced sections. +@property (nonatomic, strong, readonly) NSMutableArray *replacingSections; +@property (nonatomic, strong, readonly) NSMutableIndexSet *insertedSectionIndexes; +@property (nonatomic, strong, readonly) NSMutableArray *insertedSections; +@property (nonatomic, strong, readonly) NSMutableArray *deletedItemIndexes; +@property (nonatomic, strong, readonly) NSMutableArray *replacedItemIndexes; +/// The items used to replace the replaced items. +@property (nonatomic, strong, readonly) NSMutableArray *replacingItems; +@property (nonatomic, strong, readonly) NSMutableArray *insertedItemIndexes; +@property (nonatomic, strong, readonly) NSMutableArray *insertedItems; + +/// NOTE: `data` will be modified +- (instancetype)initWithData:(NSArray *)data; + ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64; +- (NSString *)base64Representation; +@end + +@implementation ASThrashUpdate + +- (instancetype)initWithData:(NSMutableArray *)data { + self = [super init]; + if (self != nil) { + _oldData = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; + + _deletedItemIndexes = [NSMutableArray array]; + _replacedItemIndexes = [NSMutableArray array]; + _insertedItemIndexes = [NSMutableArray array]; + + // Randomly reload some items + for (ASThrashTestSection *section in data) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; + [_replacedItemIndexes addObject:indexes]; + } + + // Randomly replace some sections + _replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + _replacingSections = [ASThrashTestSection sectionsWithCount:_replacedSectionIndexes.count]; + [data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections]; + + // Randomly delete some items + [data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { + if (section.items.count >= kMinimumItemCount) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; + + /// Cannot reload & delete the same item. + [indexes removeIndexes:_replacedItemIndexes[idx]]; + + [section.items removeObjectsAtIndexes:indexes]; + [_deletedItemIndexes addObject:indexes]; + } else { + [_deletedItemIndexes addObject:[NSMutableIndexSet indexSet]]; + } + }]; + + // Randomly delete some sections + if (data.count >= kMinimumSectionCount) { + _deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + } else { + _deletedSectionIndexes = [NSMutableIndexSet indexSet]; + } + // Cannot replace & delete the same section. + [_deletedSectionIndexes removeIndexes:_replacedSectionIndexes]; + + // Cannot delete/replace item in deleted/replaced section + [_deletedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [_replacedItemIndexes[idx] removeAllIndexes]; + [_deletedItemIndexes[idx] removeAllIndexes]; + }]; + [_replacedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [_replacedItemIndexes[idx] removeAllIndexes]; + [_deletedItemIndexes[idx] removeAllIndexes]; + }]; + [data removeObjectsAtIndexes:_deletedSectionIndexes]; + + // Randomly insert some sections + _insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(data.count + 1) probability:kFickleness insertMode:YES]; + _insertedSections = [ASThrashTestSection sectionsWithCount:_insertedSectionIndexes.count]; + [data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes]; + + // Randomly insert some items + for (ASThrashTestSection *section in data) { + // Only insert items into the old sections – not replaced/inserted sections. + if ([_oldData containsObject:section]) { + NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:(section.items.count + 1) probability:kFickleness insertMode:YES]; + NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; + [section.items insertObjects:newItems atIndexes:indexes]; + [_insertedItemIndexes addObject:indexes]; + } else { + [_insertedItemIndexes addObject:[NSMutableIndexSet indexSet]]; + } + } + } + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + ++ (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64 { + return [NSKeyedUnarchiver unarchiveObjectWithData:[[NSData alloc] initWithBase64EncodedString:base64 options:kNilOptions]]; +} + +- (NSString *)base64Representation { + return [[NSKeyedArchiver archivedDataWithRootObject:self] base64EncodedStringWithOptions:kNilOptions]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + NSDictionary *dict = [self dictionaryWithValuesForKeys:@[ + @"oldData", + @"deletedSectionIndexes", + @"replacedSectionIndexes", + @"reloadedSections", + @"insertedSectionIndexes", + @"_insertedSectionIndexes", + @"deletedItemIndexes", + @"replacedItemIndexes", + @"reloadedItems", + @"insertedItemIndexes", + @"_insertedItemIndexes" + ]]; + [aCoder encodeObject:dict forKey:@"_dict"]; + [aCoder encodeObject:@(ASThrashUpdateCurrentSerializationVersion) forKey:@"_version"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + NSAssert(ASThrashUpdateCurrentSerializationVersion == [aDecoder decodeIntegerForKey:@"_version"], @"This thrash update was archived from a different version and can't be read. Sorry."); + NSDictionary *dict = [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:@"_dict"]; + [self setValuesForKeysWithDictionary:dict]; + } + return self; +} + +- (void)applyToTableView:(UITableView *)tableView { + [tableView beginUpdates]; + + [tableView insertSections:_insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView deleteSections:_deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView reloadSections:_replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [_insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [_deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [_replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [tableView endUpdates]; +} + @end @interface ASTableViewThrashTests: XCTestCase @@ -201,16 +449,9 @@ static NSString *ASThrashArrayDescription(NSArray *array) { #else ASTableView *tableView; #endif - - NSInteger minimumItemCount; - NSInteger minimumSectionCount; - float fickleness; } - (void)setUp { - minimumItemCount = 5; - minimumSectionCount = 3; - fickleness = 0.1; window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; ds = [[ASThrashDataSource alloc] init]; #if USE_UIKIT_REFERENCE @@ -248,129 +489,17 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [self verifyTableStateWithHierarchy]; LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); - // NOTE: This is not a deep copy, so these sections will still have their - // item counts updated throughout the update. - NSArray *oldSections = [ds.data copy]; - - NSMutableArray *deletedItems = [NSMutableArray array]; - NSMutableArray *replacedItems = [NSMutableArray array]; - NSMutableArray *insertedItems = [NSMutableArray array]; - - // Randomly reload some items - for (ASThrashTestSection *section in ds.data) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; - NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; - [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; - [replacedItems addObject:indexes]; - } - - // Randomly replace some sections - NSMutableIndexSet *replacedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; - NSArray *replacingSections = [ASThrashTestSection sectionsWithCount:replacedSections.count]; - [ds.data replaceObjectsAtIndexes:replacedSections withObjects:replacingSections]; - - // Randomly delete some items - [ds.data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { - if (section.items.count >= minimumItemCount) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:section.items.count probability:fickleness insertMode:NO]; - - /// Cannot reload & delete the same item. - [indexes removeIndexes:replacedItems[idx]]; - - [section.items removeObjectsAtIndexes:indexes]; - [deletedItems addObject:indexes]; - } else { - [deletedItems addObject:[NSMutableIndexSet indexSet]]; - } - }]; - - // Randomly delete some sections - NSMutableIndexSet *deletedSections = nil; - if (ds.data.count >= minimumSectionCount) { - deletedSections = [self randomIndexesLessThan:ds.data.count probability:fickleness insertMode:NO]; - } else { - deletedSections = [NSMutableIndexSet indexSet]; - } - // Cannot replace & delete the same section. - [deletedSections removeIndexes:replacedSections]; - - // Cannot delete/replace item in deleted/replaced section - [deletedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - [replacedItems[idx] removeAllIndexes]; - [deletedItems[idx] removeAllIndexes]; - }]; - [replacedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - [replacedItems[idx] removeAllIndexes]; - [deletedItems[idx] removeAllIndexes]; - }]; - [ds.data removeObjectsAtIndexes:deletedSections]; - - // Randomly insert some sections - NSMutableIndexSet *insertedSections = [self randomIndexesLessThan:(ds.data.count + 1) probability:fickleness insertMode:YES]; - NSArray *newSections = [ASThrashTestSection sectionsWithCount:insertedSections.count]; - [ds.data insertObjects:newSections atIndexes:insertedSections]; - - // Randomly insert some items - for (ASThrashTestSection *section in ds.data) { - // Only insert items into the old sections – not replaced/inserted sections. - if ([oldSections containsObject:section]) { - NSMutableIndexSet *indexes = [self randomIndexesLessThan:(section.items.count + 1) probability:fickleness insertMode:YES]; - NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; - [section.items insertObjects:newItems atIndexes:indexes]; - [insertedItems addObject:indexes]; - } else { - [insertedItems addObject:[NSMutableIndexSet indexSet]]; - } - } + ASThrashUpdate *update = [[ASThrashUpdate alloc] initWithData:ds.data]; LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); - // TODO: Submit changes in random order, randomly chunked up - - [tableView beginUpdates]; - [insertedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:idx]; - [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [tableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationNone]; - - [tableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationNone]; - - [tableView reloadSections:replacedSections withRowAnimation:UITableViewRowAnimationNone]; - - [deletedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [replacedItems enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [tableView endUpdates]; + [update applyToTableView:tableView]; #if !USE_UIKIT_REFERENCE [tableView waitUntilAllUpdatesAreCommitted]; #endif [self verifyTableStateWithHierarchy]; } -/// `insertMode` means that for each index selected, the max goes up by one. -- (NSMutableIndexSet *)randomIndexesLessThan:(NSInteger)max probability:(float)probability insertMode:(BOOL)insertMode { - NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; - u_int32_t cutoff = probability * 100; - for (NSInteger i = 0; i < max; i++) { - if (arc4random_uniform(100) < cutoff) { - [indexes addIndex:i]; - if (insertMode) { - max += 1; - } - } - } - return indexes; -} - #pragma mark Helpers - (void)verifyTableStateWithHierarchy { From d547372a635da6ec3e20e2e4c94f092a87be965b Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Wed, 22 Jun 2016 15:37:01 -0700 Subject: [PATCH 38/51] [Carthage] define PIN_REMOTE_IMAGE in .pch for non-CocoaPod users (#1797) * [Carthage] define PIN_REMOTE_IMAGE in PCH * fixed ClassString --- AsyncDisplayKit/AsyncDisplayKit-Prefix.pch | 10 ++++++++++ .../Details/ASPINRemoteImageDownloader.m | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch index 625be4d2..c2e80814 100644 --- a/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch +++ b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch @@ -7,3 +7,13 @@ #ifdef __OBJC__ #import #endif + + +// CocoaPods has a preproceessor macro for PIN_REMOTE_IMAGE, if already defined, okay +#ifndef PIN_REMOTE_IMAGE + +// For Carthage or manual builds, this will define PIN_REMOTE_IMAGE if the header is available in the +// search path e.g. they've dragged in the framework (technically this will not be able to detect if +// a user does not include the framework in the link binary with build step). +#define PIN_REMOTE_IMAGE __has_include() +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m index 3b9b201d..99fad4ec 100644 --- a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m +++ b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m @@ -85,7 +85,23 @@ static PINRemoteImageManager *sharedPINRemoteImageManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ + #if PIN_ANIMATED_AVAILABLE + // Check that Carthage users have linked both PINRemoteImage & PINCache by testing for one file each + if (!(NSClassFromString(@"PINRemoteImageManager"))) { + NSException *e = [NSException + exceptionWithName:@"FrameworkSetupException" + reason:@"Missing the path to the PINRemoteImage framework." + userInfo:nil]; + @throw e; + } + if (!(NSClassFromString(@"PINCache"))) { + NSException *e = [NSException + exceptionWithName:@"FrameworkSetupException" + reason:@"Missing the path to the PINCache framework." + userInfo:nil]; + @throw e; + } sharedPINRemoteImageManager = [[PINRemoteImageManager alloc] initWithSessionConfiguration:nil alternativeRepresentationProvider:self]; #else sharedPINRemoteImageManager = [[PINRemoteImageManager alloc] initWithSessionConfiguration:nil]; From eb9e8167c3cda622f37e0d3f12a9e7e0f1894103 Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Wed, 22 Jun 2016 15:37:31 -0700 Subject: [PATCH 39/51] [Carthage] Add cartfile, update Carthage example (#1796) --- Cartfile | 1 + .../Sample.xcodeproj/project.pbxproj | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 Cartfile diff --git a/Cartfile b/Cartfile new file mode 100644 index 00000000..6cdeeb89 --- /dev/null +++ b/Cartfile @@ -0,0 +1 @@ +github "pinterest/PINRemoteImage" "3.0.0-beta.2" diff --git a/examples/CarthageBuildTest/Sample.xcodeproj/project.pbxproj b/examples/CarthageBuildTest/Sample.xcodeproj/project.pbxproj index cffd86f3..6d3073fb 100644 --- a/examples/CarthageBuildTest/Sample.xcodeproj/project.pbxproj +++ b/examples/CarthageBuildTest/Sample.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 871BB3591C7C98B1005CF62A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 871BB3581C7C98B1005CF62A /* Assets.xcassets */; }; 871BB35C1C7C98B1005CF62A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 871BB35A1C7C98B1005CF62A /* LaunchScreen.storyboard */; }; 871BB3651C7C99B0005CF62A /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */; }; + DEAE185D1D1A504A0083FAD0 /* PINCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEAE185B1D1A504A0083FAD0 /* PINCache.framework */; }; + DEAE185E1D1A504A0083FAD0 /* PINRemoteImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -28,6 +30,8 @@ 871BB35B1C7C98B1005CF62A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 871BB35D1C7C98B1005CF62A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AsyncDisplayKit.framework; path = Carthage/Build/iOS/AsyncDisplayKit.framework; sourceTree = SOURCE_ROOT; }; + DEAE185B1D1A504A0083FAD0 /* PINCache.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PINCache.framework; path = ../Carthage/Build/iOS/PINCache.framework; sourceTree = ""; }; + DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PINRemoteImage.framework; path = ../Carthage/Build/iOS/PINRemoteImage.framework; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -36,6 +40,8 @@ buildActionMask = 2147483647; files = ( 871BB3651C7C99B0005CF62A /* AsyncDisplayKit.framework in Frameworks */, + DEAE185D1D1A504A0083FAD0 /* PINCache.framework in Frameworks */, + DEAE185E1D1A504A0083FAD0 /* PINRemoteImage.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -87,6 +93,8 @@ isa = PBXGroup; children = ( 871BB3641C7C99B0005CF62A /* AsyncDisplayKit.framework */, + DEAE185B1D1A504A0083FAD0 /* PINCache.framework */, + DEAE185C1D1A504A0083FAD0 /* PINRemoteImage.framework */, ); name = Frameworks; sourceTree = ""; @@ -126,7 +134,7 @@ }; }; }; - buildConfigurationList = 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "CarthageExample" */; + buildConfigurationList = 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "Sample" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; @@ -325,7 +333,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "CarthageExample" */ = { + 871BB3441C7C98B1005CF62A /* Build configuration list for PBXProject "Sample" */ = { isa = XCConfigurationList; buildConfigurations = ( 871BB35E1C7C98B1005CF62A /* Debug */, From 4184c21c0c8905489237e681db5093c1f478eb22 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 15:46:47 -0700 Subject: [PATCH 40/51] [ASThrashTesting] Continue setting up replay feature --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 47 +++++++++++++++---- .../TestResources/ASThrashTestRecordedCase | 0 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index dc1ddbbd..d32bab23 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -386,17 +386,17 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @"oldData", @"deletedSectionIndexes", @"replacedSectionIndexes", - @"reloadedSections", + @"replacingSections", @"insertedSectionIndexes", - @"_insertedSectionIndexes", + @"insertedSections", @"deletedItemIndexes", @"replacedItemIndexes", - @"reloadedItems", + @"replacingItems", @"insertedItemIndexes", - @"_insertedItemIndexes" + @"insertedItems" ]]; [aCoder encodeObject:dict forKey:@"_dict"]; - [aCoder encodeObject:@(ASThrashUpdateCurrentSerializationVersion) forKey:@"_version"]; + [aCoder encodeInteger:ASThrashUpdateCurrentSerializationVersion forKey:@"_version"]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { @@ -432,8 +432,12 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; NSArray *indexPaths = [indexes indexPathsInSection:sec]; [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; }]; - - [tableView endUpdates]; + @try { + [tableView endUpdates]; + } @catch (NSException *exception) { + NSLog(@"Rejected update base64: %@", self.base64Representation); + @throw exception; + } } @end @@ -449,6 +453,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; #else ASTableView *tableView; #endif + ASThrashUpdate *currentUpdate; } - (void)setUp { @@ -477,7 +482,28 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; [self verifyTableStateWithHierarchy]; } -- (void)DISABLED_testThrashingWildly { +- (void)testSpecificThrashing { + NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; + NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:nil]; + + ASThrashUpdate *update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; + if (update == nil) { + return; + } + + currentUpdate = update; + + LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); + + [update applyToTableView:tableView]; +#if !USE_UIKIT_REFERENCE + XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); +#endif + [self verifyTableStateWithHierarchy]; + currentUpdate = nil; +} + +- (void)testThrashingWildly { for (NSInteger i = 0; i < 100; i++) { [self setUp]; [self _testThrashingWildly]; @@ -486,18 +512,19 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; } - (void)_testThrashingWildly { - [self verifyTableStateWithHierarchy]; LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); ASThrashUpdate *update = [[ASThrashUpdate alloc] initWithData:ds.data]; + currentUpdate = update; LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); [update applyToTableView:tableView]; #if !USE_UIKIT_REFERENCE - [tableView waitUntilAllUpdatesAreCommitted]; + XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); #endif [self verifyTableStateWithHierarchy]; + currentUpdate = nil; } #pragma mark Helpers diff --git a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase new file mode 100644 index 00000000..e69de29b From a05d3119adc8a97b66b76ea86e3f8d65135f9d5d Mon Sep 17 00:00:00 2001 From: Nick Velloff Date: Wed, 22 Jun 2016 16:21:10 -0700 Subject: [PATCH 41/51] [WIP] Trigger new batch fetch for collection view after layout transitions (#1697) [Table / Collection] Trigger new batch fetch after programmatic scrolls, or layout transitions. --- AsyncDisplayKit/ASCollectionView.mm | 6 ++++++ AsyncDisplayKit/ASTableView.mm | 5 +++++ AsyncDisplayKit/Details/ASRangeController.h | 7 +++++++ AsyncDisplayKit/Details/ASRangeController.mm | 1 + 4 files changed, 19 insertions(+) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 821e21dd..a7ab280e 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -1002,6 +1002,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); + // Calling visibleNodeIndexPathsForRangeController: will trigger UIKit to call reloadData if it never has, which can result // in incorrect layout if performed at zero size. We can use the fact that nothing can be visible at zero size to return fast. BOOL isZeroSized = CGRectEqualToRect(self.bounds, CGRectZero); @@ -1065,6 +1066,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _performingBatchUpdates = NO; } +- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController +{ + [self _checkForBatchFetching]; +} + - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index b8a174bb..871589b4 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -968,6 +968,11 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } +- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController +{ + [self _checkForBatchFetching]; +} + - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index 2de7e3ce..c79124a9 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -146,6 +146,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController * )rangeController didEndUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion; +/** + * Completed updates to cell node addition and removal. + * + * @param rangeController Sender. + */ +- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController; + /** * Called for nodes insertion. * diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 436cb8f4..6c849355 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -333,6 +333,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [modifiedIndexPaths sortUsingSelector:@selector(compare:)]; NSLog(@"Range update complete; modifiedIndexPaths: %@", [self descriptionWithIndexPaths:modifiedIndexPaths]); #endif + [_delegate didCompleteUpdatesInRangeController:self]; } #pragma mark - Notification observers From ea64d7d09be74580a27f7f4b2a9e3b358705a71b Mon Sep 17 00:00:00 2001 From: John Engelhart Date: Wed, 22 Jun 2016 16:56:53 -0700 Subject: [PATCH 42/51] Pr/fix unit tests memory leaks (#1795) * Fix some concurrency problems detected by Xcode 8's new Thread Sanitizer. Some of these changes are arguably just to silence the warnings from Thread Sanitizer. * Fix several memory leaks in the unit tests. A number of the unit test source files are compield with `-fno-objc-arc`. This was clearly overlooked when writing several of the unit tests. Fixed by (mostly) switching to use of `-autorelease` for the problem code. NOTE: This commit doesn't fix all the memory leaks found. There's still at least one leak in `-[ASDisplayNodeTests testSetNeedsDataFetchImmediateState]`, and several leaks in `ASBasicImageDownloader.mm`. I wasn't able to find a trivial cause to these, unfortunately. --- .../Transactions/_ASAsyncTransaction.mm | 34 +++--- AsyncDisplayKit/Private/_AS-objc-internal.h | 9 +- AsyncDisplayKitTests/ASDisplayLayerTests.m | 11 +- .../ASDisplayNodeAppearanceTests.m | 20 ++-- AsyncDisplayKitTests/ASDisplayNodeTests.m | 101 ++++++++---------- .../ASMultiplexImageNodeTests.m | 18 +++- 6 files changed, 98 insertions(+), 95 deletions(-) diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm index 3f02afba..5342e34f 100644 --- a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.mm @@ -327,7 +327,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() _callbackQueue = callbackQueue; _completionBlock = [completionBlock copy]; - _state = ASAsyncTransactionStateOpen; + __atomic_store_n(&_state, ASAsyncTransactionStateOpen, __ATOMIC_SEQ_CST); } return self; } @@ -335,7 +335,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() - (void)dealloc { // Uncommitted transactions break our guarantees about releasing completion blocks on callbackQueue. - ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed"); + ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed"); if (_group) { _group->release(); } @@ -360,7 +360,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); + ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); [self _ensureTransactionData]; @@ -368,7 +368,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() [_operations addObject:operation]; _group->schedule(priority, queue, ^{ @autoreleasepool { - if (_state != ASAsyncTransactionStateCanceled) { + if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateCanceled) { _group->enter(); block(^(id value){ operation.value = value; @@ -395,7 +395,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); + ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); [self _ensureTransactionData]; @@ -403,7 +403,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() [_operations addObject:operation]; _group->schedule(priority, queue, ^{ @autoreleasepool { - if (_state != ASAsyncTransactionStateCanceled) { + if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateCanceled) { operation.value = block(); } } @@ -422,15 +422,15 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() - (void)cancel { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction"); - _state = ASAsyncTransactionStateCanceled; + ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction"); + __atomic_store_n(&_state, ASAsyncTransactionStateCanceled, __ATOMIC_SEQ_CST); } - (void)commit { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction"); - _state = ASAsyncTransactionStateCommitted; + ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction"); + __atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST); if ([_operations count] == 0) { // Fast path: if a transaction was opened, but no operations were added, execute completion block synchronously. @@ -451,8 +451,8 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() - (void)completeTransaction { - if (_state != ASAsyncTransactionStateComplete) { - BOOL isCanceled = (_state == ASAsyncTransactionStateCanceled); + if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) { + BOOL isCanceled = (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateCanceled); for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) { [operation callAndReleaseCompletionBlock:isCanceled]; } @@ -460,7 +460,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() // Always set _state to Complete, even if we were cancelled, to block any extraneous // calls to this method that may have been scheduled for the next runloop // (e.g. if we needed to force one in this runloop with -waitUntilComplete, but another was already scheduled) - _state = ASAsyncTransactionStateComplete; + __atomic_store_n(&_state, ASAsyncTransactionStateComplete, __ATOMIC_SEQ_CST); if (_completionBlock) { _completionBlock(self, isCanceled); @@ -471,7 +471,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() - (void)waitUntilComplete { ASDisplayNodeAssertMainThread(); - if (_state != ASAsyncTransactionStateComplete) { + if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) { if (_group) { ASDisplayNodeAssertTrue(_callbackQueue == dispatch_get_main_queue()); _group->wait(); @@ -481,9 +481,9 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() // commit ourselves via the group to avoid double-committing the transaction. // This is only necessary when forcing display work to complete before allowing the runloop // to continue, e.g. in the implementation of -[ASDisplayNode recursivelyEnsureDisplay]. - if (_state == ASAsyncTransactionStateOpen) { + if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateOpen) { [_ASAsyncTransactionGroup commit]; - ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group"); + ASDisplayNodeAssert(__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group"); } // If we needed to commit the group above, -completeTransaction may have already been run. // It is designed to accommodate this by checking _state to ensure it is not complete. @@ -508,7 +508,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() - (NSString *)description { - return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %p, _operations = %@>", self, (unsigned long)_state, _group, _operations]; + return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %p, _operations = %@>", self, (unsigned long)__atomic_load_n(&_state, __ATOMIC_SEQ_CST), _group, _operations]; } @end diff --git a/AsyncDisplayKit/Private/_AS-objc-internal.h b/AsyncDisplayKit/Private/_AS-objc-internal.h index b4635c63..196b3c82 100644 --- a/AsyncDisplayKit/Private/_AS-objc-internal.h +++ b/AsyncDisplayKit/Private/_AS-objc-internal.h @@ -442,7 +442,7 @@ typedef enum { -(BOOL)_tryRetain { \ __typeof__(_rc_ivar) _prev; \ do { \ - _prev = _rc_ivar; \ + _prev = __atomic_load_n(&_rc_ivar, __ATOMIC_SEQ_CST);; \ if (_prev & 1) { \ return 0; \ } else if (_prev == -2) { \ @@ -454,12 +454,13 @@ typedef enum { return 1; \ } \ -(BOOL)_isDeallocating { \ - if (_rc_ivar == -2) { \ + __typeof__(_rc_ivar) _prev = __atomic_load_n(&_rc_ivar, __ATOMIC_SEQ_CST); \ + if (_prev == -2) { \ return 1; \ - } else if (_rc_ivar < -2) { \ + } else if (_prev < -2) { \ __builtin_trap(); /* BUG: over-release elsewhere */ \ } \ - return _rc_ivar & 1; \ + return _prev & 1; \ } #define _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, _dealloc2main) \ diff --git a/AsyncDisplayKitTests/ASDisplayLayerTests.m b/AsyncDisplayKitTests/ASDisplayLayerTests.m index e0d3b653..c0c2613d 100644 --- a/AsyncDisplayKitTests/ASDisplayLayerTests.m +++ b/AsyncDisplayKitTests/ASDisplayLayerTests.m @@ -227,7 +227,12 @@ static _ASDisplayLayerTestDelegateClassModes _class_modes; // DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests! + (void)drawRect:(CGRect)bounds withParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(asdisplaynode_iscancelled_block_t)sentinelBlock isRasterizing:(BOOL)isRasterizing { - delegate->_drawRectCount++; + __atomic_add_fetch(&delegate->_drawRectCount, 1, __ATOMIC_SEQ_CST); +} + +- (NSUInteger)drawRectCount +{ + return(__atomic_load_n(&_drawRectCount, __ATOMIC_SEQ_CST)); } - (void)dealloc @@ -267,9 +272,9 @@ static _ASDisplayLayerTestDelegateClassModes _class_modes; // make sure we don't lock up the tests indefinitely; fail after 1 sec by using an async barrier __block BOOL didHitBarrier = NO; dispatch_barrier_async([_ASDisplayLayer displayQueue], ^{ - didHitBarrier = YES; + __atomic_store_n(&didHitBarrier, YES, __ATOMIC_SEQ_CST); }); - XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return didHitBarrier; })); + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return __atomic_load_n(&didHitBarrier, __ATOMIC_SEQ_CST); })); } - (void)waitForLayer:(_ASDisplayLayerTestLayer *)layer asyncDisplayCount:(NSUInteger)count diff --git a/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m b/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m index b7273893..f350eb1b 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m @@ -60,11 +60,11 @@ static dispatch_block_t modifyMethodByAddingPrologueBlockAndReturnCleanupBlock(C @end // Conveniences for making nodes named a certain way -#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n +#define DeclareNodeNamed(n) ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease]; n.name = @#n #define DeclareViewNamed(v) UIView *v = viewWithName(@#v) static UIView *viewWithName(NSString *name) { - ASDisplayNode *n = [[ASDisplayNode alloc] init]; + ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease]; n.name = name; return n.view; } @@ -130,7 +130,7 @@ static UIView *viewWithName(NSString *name) { - (void)checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:(BOOL)isLayerBacked { // ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it. - UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + UIWindow *window = [[[UIWindow alloc] initWithFrame:CGRectZero] autorelease]; DeclareNodeNamed(n); DeclareViewNamed(superview); @@ -162,15 +162,12 @@ static UIView *viewWithName(NSString *name) { XCTAssertEqual([_willEnterHierarchyCounts countForObject:n], 1u, @"willEnterHierarchy not called when node's view added to hierarchy"); XCTAssertEqual([_didExitHierarchyCounts countForObject:n], 1u, @"didExitHierarchy erroneously called"); - - [superview release]; - [window release]; } - (void)checkManualAppearanceViewLoaded:(BOOL)isViewLoaded layerBacked:(BOOL)isLayerBacked { // ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it. - UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + UIWindow *window = [[[UIWindow alloc] initWithFrame:CGRectZero] autorelease]; DeclareNodeNamed(parent); DeclareNodeNamed(a); @@ -263,13 +260,13 @@ static UIView *viewWithName(NSString *name) { - (void)testSynchronousIntermediaryView { // Parent is a wrapper node for a scrollview - ASDisplayNode *parentSynchronousNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]]; + ASDisplayNode *parentSynchronousNode = [[[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]] autorelease]; DeclareNodeNamed(layerBackedNode); DeclareNodeNamed(viewBackedNode); layerBackedNode.layerBacked = YES; - UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + UIWindow *window = [[[UIWindow alloc] initWithFrame:CGRectZero] autorelease]; [parentSynchronousNode addSubnode:layerBackedNode]; [parentSynchronousNode addSubnode:viewBackedNode]; @@ -303,11 +300,6 @@ static UIView *viewWithName(NSString *name) { XCTAssertFalse(parentSynchronousNode.inHierarchy, @"Should not have changed"); XCTAssertFalse(layerBackedNode.inHierarchy, @"Should have been marked invisible when synchronous superview was removed from the window"); XCTAssertFalse(viewBackedNode.inHierarchy, @"Should have been marked invisible when synchronous superview was removed from the window"); - - [window release]; - [parentSynchronousNode release]; - [layerBackedNode release]; - [viewBackedNode release]; } - (void)checkMoveAcrossHierarchyLayerBacked:(BOOL)isLayerBacked useManualCalls:(BOOL)useManualDisable useNodeAPI:(BOOL)useNodeAPI diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index c85b7d31..86529730 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -21,18 +21,18 @@ #import "ASCellNode.h" // Conveniences for making nodes named a certain way -#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n +#define DeclareNodeNamed(n) ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease]; n.name = @#n #define DeclareViewNamed(v) UIView *v = viewWithName(@#v) #define DeclareLayerNamed(l) CALayer *l = layerWithName(@#l) static UIView *viewWithName(NSString *name) { - ASDisplayNode *n = [[ASDisplayNode alloc] init]; + ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease]; n.name = name; return n.view; } static CALayer *layerWithName(NSString *name) { - ASDisplayNode *n = [[ASDisplayNode alloc] init]; + ASDisplayNode *n = [[[ASDisplayNode alloc] init] autorelease]; n.layerBacked = YES; n.name = name; return n.layer; @@ -144,6 +144,12 @@ for (ASDisplayNode *n in @[ nodes ]) {\ { if (_willDeallocBlock) { _willDeallocBlock(self); + [_willDeallocBlock release]; + _willDeallocBlock = nil; + } + if (_calculateSizeBlock) { + [_calculateSizeBlock release]; + _calculateSizeBlock = nil; } [super dealloc]; } @@ -214,19 +220,19 @@ for (ASDisplayNode *n in @[ nodes ]) {\ } - (void)testOverriddenFirstResponderBehavior { - ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; + ASTestDisplayNode *node = [[[ASTestResponderNode alloc] init] autorelease]; XCTAssertTrue([node canBecomeFirstResponder]); XCTAssertTrue([node becomeFirstResponder]); } - (void)testDefaultFirstResponderBehavior { - ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease]; XCTAssertFalse([node canBecomeFirstResponder]); XCTAssertFalse([node becomeFirstResponder]); } - (void)testLayerBackedFirstResponderBehavior { - ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; + ASTestDisplayNode *node = [[[ASTestResponderNode alloc] init] autorelease]; node.layerBacked = YES; XCTAssertTrue([node canBecomeFirstResponder]); XCTAssertFalse([node becomeFirstResponder]); @@ -250,6 +256,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ [self executeOffThread:^{ node = [[ASDisplayNode alloc] init]; }]; + // executeOffThread: blocks until the background thread finishes executing. + node = [node autorelease]; // XXX This is very bad style. UIView *view = node.view; XCTAssertNotNil(view, @"Getting node's view on-thread should succeed."); @@ -257,7 +265,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ - (void)testNodeCreatedOffThreadWithExistingView { - UIView *view = [[UIDisplayNodeTestView alloc] init]; + UIView *view = [[[UIDisplayNodeTestView alloc] init] autorelease]; __block ASDisplayNode *node = nil; [self executeOffThread:^{ @@ -265,6 +273,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ return view; }]; }]; + // executeOffThread: blocks until the background thread finishes executing. + node = [node autorelease]; // XXX This is very bad style. XCTAssertFalse(node.layerBacked, @"Can't be layer backed"); XCTAssertTrue(node.synchronous, @"Node with plain view should be synchronous"); @@ -283,6 +293,9 @@ for (ASDisplayNode *n in @[ nodes ]) {\ return view; }]; }]; + // executeOffThread: blocks until the background thread finishes executing. + view = [view autorelease]; // XXX This is very bad style. + node = [node autorelease]; // XXX This is very bad style. XCTAssertNil(view, @"View block should not be invoked yet"); [node view]; @@ -293,10 +306,10 @@ for (ASDisplayNode *n in @[ nodes ]) {\ - (void)testNodeCreatedWithLazyAsyncView { - ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + ASDisplayNode *node = [[[ASDisplayNode alloc] initWithViewBlock:^UIView *{ XCTAssertTrue([NSThread isMainThread], @"View block must run on the main queue"); - return [[_ASDisplayView alloc] init]; - }]; + return [[[_ASDisplayView alloc] init] autorelease]; + }] autorelease]; XCTAssertThrows([node view], @"Externally provided views should be synchronous"); XCTAssertTrue(node.synchronous, @"Node with externally provided view should be synchronous"); @@ -631,7 +644,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent. - (void)testDeriveFrameFromBoundsPositionAnchorPoint { - UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero]; + UIView *plainView = [[[UIView alloc] initWithFrame:CGRectZero] autorelease]; plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f); plainView.layer.position = CGPointMake(10, 20); plainView.layer.bounds = CGRectMake(0, 0, 60, 80); @@ -643,6 +656,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.bounds = CGRectMake(0, 0, 60, 80); node.position = CGPointMake(10, 20); }]; + // executeOffThread: blocks until the background thread finishes executing. + node = [node autorelease]; // XXX This is very bad style. XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.frame), @"Node frame should match UIView frame before realization."); XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.view.frame), @"Realized view frame should match UIView frame."); @@ -651,7 +666,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ // Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent. - (void)testSetFrameSetsBoundsPosition { - UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero]; + UIView *plainView = [[[UIView alloc] initWithFrame:CGRectZero] autorelease]; plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f); plainView.layer.frame = CGRectMake(10, 20, 60, 80); @@ -661,6 +676,8 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.anchorPoint = CGPointMake(0.25f, 0.75f); node.frame = CGRectMake(10, 20, 60, 80); }]; + // executeOffThread: blocks until the background thread finishes executing. + node = [node autorelease]; // XXX This is very bad style. XCTAssertTrue(CGPointEqualToPoint(plainView.layer.position, node.position), @"Node position should match UIView position before realization."); XCTAssertTrue(CGRectEqualToRect(plainView.layer.bounds, node.bounds), @"Node bounds should match UIView bounds before realization."); @@ -921,7 +938,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ - (void)testDisplayNodePointConversionOnDeepHierarchies { - ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASDisplayNode *node = [[[ASDisplayNode alloc] init] autorelease]; // 7 deep (six below root); each one positioned at position = (1, 1) _addTonsOfSubnodes(node, 2, 6, ^(ASDisplayNode *createdNode) { @@ -1108,7 +1125,7 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point - (void)testSubnodes { - ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + ASDisplayNode *parent = [[[ASDisplayNode alloc] init] autorelease]; ASDisplayNode *nilNode = nil; XCTAssertNoThrow([parent addSubnode:nilNode], @"Don't try to add nil, but we'll deal."); XCTAssertNoThrow([parent addSubnode:parent], @"Not good, test that we recover"); @@ -1211,11 +1228,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point XCTAssertNodesHaveParent(nilParent, a,d); //TODO: assert that things deallocate immediately and don't have latent autoreleases in here - [parent release]; - [a release]; - [b release]; - [c release]; - [d release]; } - (void)testInsertSubnodeAtIndexView @@ -1344,17 +1356,12 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point XCTAssertNodesHaveParent(nilParent, d); //TODO: assert that things deallocate immediately and don't have latent autoreleases in here - [parent release]; - [a release]; - [b release]; - [c release]; - [d release]; } // This tests our resiliancy to having other views and layers inserted into our view or layer - (void)testInsertSubviewAtIndexWithMeddlingViewsAndLayersViewBacked { - ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + ASDisplayNode *parent = [[[ASDisplayNode alloc] init] autorelease]; DeclareNodeNamed(a); DeclareNodeNamed(b); @@ -1389,12 +1396,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point XCTAssertEqual(4u, parent.layer.sublayers.count, @"Should have the right sublayer count"); //TODO: assert that things deallocate immediately and don't have latent autoreleases in here - [parent release]; - [a release]; - [b release]; - [c release]; - [d release]; - [e release]; } - (void)testAppleBugInsertSubview @@ -1467,11 +1468,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point XCTAssertEqual(4u, parent.layer.sublayers.count, @"Should have the right sublayer count"); //TODO: assert that things deallocate immediately and don't have latent autoreleases in here - [parent release]; - [a release]; - [b release]; - [c release]; - [d release]; } @@ -1550,10 +1546,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point XCTAssertNodesHaveParent(parent, a, c, b); //TODO: assert that things deallocate immediately and don't have latent autoreleases in here - [parent release]; - [a release]; - [b release]; - [c release]; } - (void)testInsertSubnodeAboveWithView @@ -1632,10 +1624,6 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point XCTAssertNodesHaveParent(parent, a, c, b); //TODO: assert that things deallocate immediately and don't have latent autoreleases in here - [parent release]; - [a release]; - [b release]; - [c release]; } - (void)testRemoveFromViewBackedLoadedSupernode @@ -1702,6 +1690,9 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point child = [[ASDisplayNode alloc] init]; [parent addSubnode:child]; }]; + // executeOffThread: blocks until the background thread finishes executing. + parent = [parent autorelease]; // XXX This is very bad style. + child = [child autorelease]; // XXX This is very bad style. XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode"); XCTAssertEqualObjects(parent, child.supernode, @"Child has the wrong parent"); @@ -1714,14 +1705,14 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point - (void)testSubnodeAddedAfterLoadingExternalView { - UIView *view = [[UIDisplayNodeTestView alloc] init]; - ASDisplayNode *parent = [[ASDisplayNode alloc] initWithViewBlock:^{ + UIView *view = [[[UIDisplayNodeTestView alloc] init] autorelease]; + ASDisplayNode *parent = [[[ASDisplayNode alloc] initWithViewBlock:^{ return view; - }]; + }] autorelease]; [parent view]; - ASDisplayNode *child = [[ASDisplayNode alloc] init]; + ASDisplayNode *child = [[[ASDisplayNode alloc] init] autorelease]; [parent addSubnode:child]; XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode"); @@ -1851,7 +1842,7 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point - (void)testInitWithViewClass { - ASDisplayNode *scrollNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]]; + ASDisplayNode *scrollNode = [[[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]] autorelease]; XCTAssertFalse(scrollNode.isLayerBacked, @"Can't be layer backed"); XCTAssertFalse(scrollNode.nodeLoaded, @"Shouldn't have a view yet"); @@ -1866,7 +1857,7 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point - (void)testInitWithLayerClass { - ASDisplayNode *transformNode = [[ASDisplayNode alloc] initWithLayerClass:[CATransformLayer class]]; + ASDisplayNode *transformNode = [[[ASDisplayNode alloc] initWithLayerClass:[CATransformLayer class]] autorelease]; XCTAssertTrue(transformNode.isLayerBacked, @"Created with layer class => should be layer-backed by default"); XCTAssertFalse(transformNode.nodeLoaded, @"Shouldn't have a view yet"); @@ -1949,7 +1940,7 @@ static bool stringContainsPointer(NSString *description, const void *p) { - (void)testBounds { - ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASDisplayNode *node = [[[ASDisplayNode alloc] init] autorelease]; node.bounds = CGRectMake(1, 2, 3, 4); node.frame = CGRectMake(5, 6, 7, 8); @@ -1961,7 +1952,7 @@ static bool stringContainsPointer(NSString *description, const void *p) { - (void)testDidEnterDisplayIsCalledWhenNodesEnterDisplayRange { - ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease]; [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; @@ -1970,7 +1961,7 @@ static bool stringContainsPointer(NSString *description, const void *p) { - (void)testDidExitDisplayIsCalledWhenNodesExitDisplayRange { - ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease]; [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; [node recursivelySetInterfaceState:ASInterfaceStateFetchData]; @@ -1980,7 +1971,7 @@ static bool stringContainsPointer(NSString *description, const void *p) { - (void)testDidEnterFetchDataIsCalledWhenNodesEnterFetchDataRange { - ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease]; [node recursivelySetInterfaceState:ASInterfaceStateFetchData]; @@ -1989,7 +1980,7 @@ static bool stringContainsPointer(NSString *description, const void *p) { - (void)testDidExitFetchDataIsCalledWhenNodesExitFetchDataRange { - ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + ASTestDisplayNode *node = [[[ASTestDisplayNode alloc] init] autorelease]; [node recursivelySetInterfaceState:ASInterfaceStateFetchData]; [node recursivelySetInterfaceState:ASInterfaceStateDisplay]; diff --git a/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m index 3da2c438..9b7b3256 100644 --- a/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m +++ b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m @@ -90,8 +90,22 @@ static BOOL ASRunRunLoopUntilBlockIsTrue(BOOL (^block)()) { [super setUp]; - _mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)]; - _mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)]; + _mockCache = [[OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)] retain]; + _mockDownloader = [[OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)] retain]; +} + +- (void)tearDown +{ + if(_mockCache) { + [_mockCache release]; + _mockCache = nil; + } + if(_mockDownloader) { + [_mockDownloader release]; + _mockDownloader = nil; + } + + [super tearDown]; } - (void)testDataSourceImageMethod From 383667f2c31282d376f40cd73ba5e2fcd617d9ff Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:02:46 -0700 Subject: [PATCH 43/51] [ASThrashTesting] Rocking and rolling! --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 268 +++++++++--------- .../TestResources/ASThrashTestRecordedCase | 1 + 2 files changed, 142 insertions(+), 127 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index d32bab23..6b849396 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -9,20 +9,21 @@ @import XCTest; #import -//#define LOG(...) NSLog(__VA_ARGS__) -#define LOG(...) - #define USE_UIKIT_REFERENCE 0 +#if USE_UIKIT_REFERENCE +#define TableView UITableView +#define kCellReuseID @"ASThrashTestCellReuseID" +#else +#define TableView ASTableView +#endif + #define kInitialSectionCount 20 #define kInitialItemCount 20 #define kMinimumItemCount 5 #define kMinimumSectionCount 3 #define kFickleness 0.1 - -#if USE_UIKIT_REFERENCE -#define kCellReuseID @"ASThrashTestCellReuseID" -#endif +#define kThrashingIterationCount 100 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" @@ -183,16 +184,32 @@ static volatile int32_t ASThrashTestSectionNextID = 1; #else #endif -@property (nonatomic, strong, readonly) NSMutableArray *data; + +@property (nonatomic, strong, readonly) UIWindow *window; +@property (nonatomic, strong, readonly) TableView *tableView; +@property (nonatomic, strong) NSArray *data; @end @implementation ASThrashDataSource -- (instancetype)init { +- (instancetype)initWithData:(NSArray *)data { self = [super init]; if (self != nil) { - _data = [ASThrashTestSection sectionsWithCount:kInitialSectionCount]; + _data = [[NSArray alloc] initWithArray:data copyItems:YES]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _tableView = [[TableView alloc] initWithFrame:_window.bounds style:UITableViewStylePlain]; + [_window addSubview:_tableView]; +#if USE_UIKIT_REFERENCE + _tableView.dataSource = self; + _tableView.delegate = self; + [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; +#else + _tableView.asyncDelegate = self; + _tableView.asyncDataSource = self; + [_tableView reloadDataImmediately]; +#endif + [_tableView layoutIfNeeded]; } return self; } @@ -267,7 +284,7 @@ static volatile int32_t ASThrashTestSectionNextID = 1; static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @interface ASThrashUpdate : NSObject -@property (nonatomic, strong, readonly) NSMutableArray *oldData; +@property (nonatomic, strong, readonly) NSArray *oldData; @property (nonatomic, strong, readonly) NSMutableArray *data; @property (nonatomic, strong, readonly) NSMutableIndexSet *deletedSectionIndexes; @property (nonatomic, strong, readonly) NSMutableIndexSet *replacedSectionIndexes; @@ -278,9 +295,9 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @property (nonatomic, strong, readonly) NSMutableArray *deletedItemIndexes; @property (nonatomic, strong, readonly) NSMutableArray *replacedItemIndexes; /// The items used to replace the replaced items. -@property (nonatomic, strong, readonly) NSMutableArray *replacingItems; +@property (nonatomic, strong, readonly) NSMutableArray *> *replacingItems; @property (nonatomic, strong, readonly) NSMutableArray *insertedItemIndexes; -@property (nonatomic, strong, readonly) NSMutableArray *insertedItems; +@property (nonatomic, strong, readonly) NSMutableArray *> *insertedItems; /// NOTE: `data` will be modified - (instancetype)initWithData:(NSArray *)data; @@ -291,30 +308,34 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @implementation ASThrashUpdate -- (instancetype)initWithData:(NSMutableArray *)data { +- (instancetype)initWithData:(NSArray *)data { self = [super init]; if (self != nil) { + _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; _oldData = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; _deletedItemIndexes = [NSMutableArray array]; _replacedItemIndexes = [NSMutableArray array]; _insertedItemIndexes = [NSMutableArray array]; + _replacingItems = [NSMutableArray array]; + _insertedItems = [NSMutableArray array]; // Randomly reload some items - for (ASThrashTestSection *section in data) { + for (ASThrashTestSection *section in _data) { NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; [section.items replaceObjectsAtIndexes:indexes withObjects:newItems]; + [_replacingItems addObject:newItems]; [_replacedItemIndexes addObject:indexes]; } // Randomly replace some sections - _replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + _replacedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO]; _replacingSections = [ASThrashTestSection sectionsWithCount:_replacedSectionIndexes.count]; - [data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections]; + [_data replaceObjectsAtIndexes:_replacedSectionIndexes withObjects:_replacingSections]; // Randomly delete some items - [data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { + [_data enumerateObjectsUsingBlock:^(ASThrashTestSection * _Nonnull section, NSUInteger idx, BOOL * _Nonnull stop) { if (section.items.count >= kMinimumItemCount) { NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:section.items.count probability:kFickleness insertMode:NO]; @@ -329,8 +350,8 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; }]; // Randomly delete some sections - if (data.count >= kMinimumSectionCount) { - _deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:data.count probability:kFickleness insertMode:NO]; + if (_data.count >= kMinimumSectionCount) { + _deletedSectionIndexes = [NSIndexSet randomIndexesLessThan:_data.count probability:kFickleness insertMode:NO]; } else { _deletedSectionIndexes = [NSMutableIndexSet indexSet]; } @@ -346,22 +367,24 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; [_replacedItemIndexes[idx] removeAllIndexes]; [_deletedItemIndexes[idx] removeAllIndexes]; }]; - [data removeObjectsAtIndexes:_deletedSectionIndexes]; + [_data removeObjectsAtIndexes:_deletedSectionIndexes]; // Randomly insert some sections - _insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(data.count + 1) probability:kFickleness insertMode:YES]; + _insertedSectionIndexes = [NSIndexSet randomIndexesLessThan:(_data.count + 1) probability:kFickleness insertMode:YES]; _insertedSections = [ASThrashTestSection sectionsWithCount:_insertedSectionIndexes.count]; - [data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes]; + [_data insertObjects:_insertedSections atIndexes:_insertedSectionIndexes]; // Randomly insert some items - for (ASThrashTestSection *section in data) { + for (ASThrashTestSection *section in _data) { // Only insert items into the old sections – not replaced/inserted sections. if ([_oldData containsObject:section]) { NSMutableIndexSet *indexes = [NSIndexSet randomIndexesLessThan:(section.items.count + 1) probability:kFickleness insertMode:YES]; NSArray *newItems = [ASThrashTestItem itemsWithCount:indexes.count]; [section.items insertObjects:newItems atIndexes:indexes]; + [_insertedItems addObject:newItems]; [_insertedItemIndexes addObject:indexes]; } else { + [_insertedItems addObject:@[]]; [_insertedItemIndexes addObject:[NSMutableIndexSet indexSet]]; } } @@ -383,18 +406,19 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; - (void)encodeWithCoder:(NSCoder *)aCoder { NSDictionary *dict = [self dictionaryWithValuesForKeys:@[ - @"oldData", - @"deletedSectionIndexes", - @"replacedSectionIndexes", - @"replacingSections", - @"insertedSectionIndexes", - @"insertedSections", - @"deletedItemIndexes", - @"replacedItemIndexes", - @"replacingItems", - @"insertedItemIndexes", - @"insertedItems" - ]]; + @"oldData", + @"data", + @"deletedSectionIndexes", + @"replacedSectionIndexes", + @"replacingSections", + @"insertedSectionIndexes", + @"insertedSections", + @"deletedItemIndexes", + @"replacedItemIndexes", + @"replacingItems", + @"insertedItemIndexes", + @"insertedItems" + ]]; [aCoder encodeObject:dict forKey:@"_dict"]; [aCoder encodeInteger:ASThrashUpdateCurrentSerializationVersion forKey:@"_version"]; } @@ -409,35 +433,12 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; return self; } -- (void)applyToTableView:(UITableView *)tableView { - [tableView beginUpdates]; - - [tableView insertSections:_insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; - - [tableView deleteSections:_deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; - - [tableView reloadSections:_replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; - - [_insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:idx]; - [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [_deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - - [_replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { - NSArray *indexPaths = [indexes indexPathsInSection:sec]; - [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - }]; - @try { - [tableView endUpdates]; - } @catch (NSException *exception) { - NSLog(@"Rejected update base64: %@", self.base64Representation); - @throw exception; - } +- (NSString *)description { + return [NSString stringWithFormat:@"", self, ASThrashArrayDescription(_oldData), ASThrashArrayDescription(_deletedItemIndexes), _deletedSectionIndexes, ASThrashArrayDescription(_replacedItemIndexes), _replacedSectionIndexes, ASThrashArrayDescription(_insertedItemIndexes), _insertedSectionIndexes, ASThrashArrayDescription(_data)]; +} + +- (NSString *)logFriendlyBase64Representation { + return [NSString stringWithFormat:@"\n\n**********\nBase64 Representation:\n**********\n%@\n**********\nEnd Base64 Representation\n**********", self.base64Representation]; } @end @@ -446,90 +447,103 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @end @implementation ASTableViewThrashTests { - ASThrashDataSource *ds; - UIWindow *window; -#if USE_UIKIT_REFERENCE - UITableView *tableView; -#else - ASTableView *tableView; -#endif - ASThrashUpdate *currentUpdate; + // The current update, which will be logged in case of a failure. + ASThrashUpdate *_update; } -- (void)setUp { - window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - ds = [[ASThrashDataSource alloc] init]; -#if USE_UIKIT_REFERENCE - tableView = [[UITableView alloc] initWithFrame:window.bounds style:UITableViewStyleGrouped]; - [window addSubview:tableView]; - tableView.dataSource = ds; - tableView.delegate = ds; - [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellReuseID]; - [window layoutIfNeeded]; -#else - ASTableNode *tableNode = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; - tableView = tableNode.view; - tableNode.frame = window.bounds; - [window addSubnode:tableNode]; - tableNode.dataSource = ds; - tableNode.delegate = ds; - [tableView reloadDataImmediately]; -#endif +#pragma mark Overrides +- (void)tearDown { + _update = nil; } +// NOTE: Despite the documentation, this is not always called if an exception is caught. +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected { + [self logCurrentUpdateIfNeeded]; + [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; +} + +#pragma mark Test Methods + - (void)testInitialDataRead { - [self verifyTableStateWithHierarchy]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + [self verifyDataSource:ds]; } -- (void)testSpecificThrashing { +/// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file +- (void)DISABLED_testRecordedThrashCase { NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; - NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:nil]; + NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; - ASThrashUpdate *update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; - if (update == nil) { + _update = [ASThrashUpdate thrashUpdateWithBase64String:base64]; + if (_update == nil) { return; } - currentUpdate = update; - - LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); - - [update applyToTableView:tableView]; -#if !USE_UIKIT_REFERENCE - XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); -#endif - [self verifyTableStateWithHierarchy]; - currentUpdate = nil; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:_update.oldData]; + [self applyUpdate:_update toDataSource:ds]; + [self verifyDataSource:ds]; } -- (void)testThrashingWildly { - for (NSInteger i = 0; i < 100; i++) { +- (void)DISABLED_testThrashingWildly { + for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; - [self _testThrashingWildly]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + _update = [[ASThrashUpdate alloc] initWithData:ds.data]; + + [self applyUpdate:_update toDataSource:ds]; + [self verifyDataSource:ds]; [self tearDown]; } } -- (void)_testThrashingWildly { - LOG(@"\n*******\nNext Iteration\n*******\nOld data: %@", ASThrashArrayDescription(ds.data)); - - ASThrashUpdate *update = [[ASThrashUpdate alloc] initWithData:ds.data]; - currentUpdate = update; - - LOG(@"Deleted items: %@\nDeleted sections: %@\nReplaced items: %@\nReplaced sections: %@\nInserted items: %@\nInserted sections: %@\nNew data: %@", ASThrashArrayDescription(deletedItems), deletedSections, ASThrashArrayDescription(replacedItems), replacedSections, ASThrashArrayDescription(insertedItems), insertedSections, ASThrashArrayDescription(ds.data)); - - [update applyToTableView:tableView]; -#if !USE_UIKIT_REFERENCE - XCTAssertNoThrow([tableView waitUntilAllUpdatesAreCommitted], @"Update assertion failure: %@", update); -#endif - [self verifyTableStateWithHierarchy]; - currentUpdate = nil; -} - #pragma mark Helpers -- (void)verifyTableStateWithHierarchy { +- (void)logCurrentUpdateIfNeeded { + if (_update != nil) { + NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); + } +} + +- (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource { + TableView *tableView = dataSource.tableView; + + [tableView beginUpdates]; + dataSource.data = update.data; + + [tableView insertSections:update.insertedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView deleteSections:update.deletedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [tableView reloadSections:update.replacedSectionIndexes withRowAnimation:UITableViewRowAnimationNone]; + + [update.insertedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:idx]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [update.deletedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + + [update.replacedItemIndexes enumerateObjectsUsingBlock:^(NSMutableIndexSet * _Nonnull indexes, NSUInteger sec, BOOL * _Nonnull stop) { + NSArray *indexPaths = [indexes indexPathsInSection:sec]; + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + }]; + @try { + [tableView endUpdates]; +#if !USE_UIKIT_REFERENCE + [tableView waitUntilAllUpdatesAreCommitted]; +#endif + } @catch (NSException *exception) { + [self logCurrentUpdateIfNeeded]; + @throw exception; + } +} + +- (void)verifyDataSource:(ASThrashDataSource *)ds { + TableView *tableView = ds.tableView; NSArray *data = [ds data]; XCTAssertEqual(data.count, tableView.numberOfSections); for (NSInteger i = 0; i < tableView.numberOfSections; i++) { diff --git a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase index e69de29b..9e834359 100644 --- a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase +++ b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase @@ -0,0 +1 @@ +YnBsaXN0MDDUAAEAAgADAAQABQAGDfYN91gkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRvcBIAAYagrxEDRQAHAAgADwAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5AEAARgBdAGEAaABrAG4AcQB0AHcAegB9AIAAgwCGAIkAjACPAJIAlQCYAJsAngChAKYAqgCuAMUAyADLAM4A0QDUANcA2gDdAOAA4wDmAOkA7ADvAPIA9QD4APsA/gEBAQUBHAEfASIBJQEoASsBLgExATQBNwE6AT0BQAFDAUYBSQFMAU8BUgFVAVgBXAFzAXYBeQF8AX8BggGFAYgBiwGOAZEBlAGXAZoBnQGgAaMBpgGpAawBrwG1AbkBvgHDAdsB4AHjAeYB6AHqAewB7wHyAfQB9gH5AfwB/wICAgUCBwIKAg0CDwITAhYCGAIaAhwCIAIjAiYCKQIsAkMCSAJLAk4CUwJWAlkCXQJgAmQCZwJsAm8CcgJ4AnsCfgKBAoUCiAKNApACkwKXApoCngKhAqYCqQKsArECtAK3Ar0CwALDAsYCywLOAtEC2ALbAt4C4QLkAu4C8QL0AvcC+gL9AwADAwMHAwoDEAMTAxYDGQMeAyEDJAMnAz4DQgNZA1wDXwNiA2UDaANrA24DcQN0A3cDegN9A4ADgwOGA4kDjAOPA5IDlQOZA7ADswO2A7kDvAO/A8IDxQPIA8sDzgPRA9QD1wPaA90D4APjA+YD6QPsA/AEBwQKBA0EEAQTBBYEGQQcBB8EIgQlBCgEKwQuBDEENAQ3BDoEPQRABEMERwReBGEEZARnBGoEbQRwBHMEdgR5BHwEfwSCBIUEiASLBI4EkQSUBJcEmgSeBLUEuAS7BL4EwQTEBMcEygTNBNAE0wTWBNkE3ATfBOIE5QToBOsE7gTxBPUFDAUPBRIFFQUYBRsFHgUhBSQFJwUqBS0FMAUzBTYFOQU8BT8FQgVFBUgFTAVjBWYFaQVsBW8FcgV1BXgFewV+BYEFhAWHBYoFjQWQBZMFlgWZBZwFnwWjBboFvQXABcMFxgXJBcwFzwXSBdUF2AXbBd4F4QXkBecF6gXtBfAF8wX2BfoGEQYUBhcGGgYdBiAGIwYmBikGLAYvBjIGNQY4BjsGPgZBBkQGRwZKBk0GUQZoBmsGbgZxBnQGdwZ6Bn0GgAaDBoYGiQaMBo8GkgaVBpgGmwaeBqEGpAaoBr8GwgbFBsgGywbOBtEG1AbXBtoG3QbgBuMG5gbpBuwG7wbyBvUG+Ab7Bv8HFgcZBxwHHwciByUHKAcrBy4HMQc0BzcHOgc9B0AHQwdGB0kHTAdPB1IHVgdtB3AHcwd2B3kHfAd/B4IHhQeIB4sHjgeRB5QHlweaB50HoAejB6YHqQetB8QHxwfKB80H0AfTB9YH2QfcB98H4gflB+gH6wfuB/EH9Af3B/oH/QgACAQIGwgeCCEIJAgnCCoILQgwCDMINgg5CDwIPwhCCEUISAhLCE4IUQhUCFcIWwhyCHUIeAh7CH4IgQiECIcIigiNCJAIkwiWCJkInAifCKIIpQioCKsIrgiyCMkIzAjPCNII1QjYCNsI3gjhCOQI5wjqCO0I8AjzCPYI+Qj8CP8JAgkFCQkJIAkjCSYJKQksCS8JMgk1CTgJOwk+CUEJRAlHCUoJTQlQCVMJVglZCVwJYAl3CXoJfQmACYMJhgmJCYwJjwmSCZUJmAmbCZ4JoQmkCacJqgmtCbAJswm3Cc4J0QnUCdcJ2gndCeAJ4wnmCekJ7AnvCfIJ9Qn4CfsJ/goBCgQKBwoKCiIKJQo8Cj8KQgpcCl8KYgplCmgKfQqACpUKmAqwCrMKtgq6Cs0K0ArTCtYK2QrcCt8K4grlCugK6wruCvEK9Ar3CvoK/QsBCxcLGgsdCyALIwsmCykLLAsvCzILNQs4CzsLPgtBC0QLRwtKC00LUAtTC2wLbwtyC3ULjAuPC5ILrAuvC7ILtQu4C8wLzwvmC+kL7AvvDAgMCwwODBEMFQwoDCsMLgwxDDQMNww6DD0MQAxDDEYMSQxMDE8MUgxVDFgMWwxyDHUMeAx7DH4MgQyWDJkMnAyfDLYMuQy8DL8M1gzYDNsM3QzgDOMM5gzpDOwM7gzwDPIM9Az2DPgM+gz9DQANAw0GDQkNCw0ODRENFA0XDRoNMQ0zDTYNOQ08DT8NQg1FDUgNSw1NDU8NUQ1TDVYNWQ1cDV8NYQ1kDWcNag1tDXANcw11DXgNew1+DYENgw2bDZ8NpQ2oDaoNrQ2wDbUNug2+DcQNxw3MDdIN2Q3eDeIN5Q3oDe4N8lUkbnVsbNMACQAKAAsADAANAA5VX2RpY3RYX3ZlcnNpb25WJGNsYXNzgAIQAYEDRNMAEAARAAsAEgA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/rxAUAV4BXwFgAWEBYgFjAWQBZQFmAWcBaAFpAWoBawFsAW0BbgFvAXABcYBXgFiAWYBagFuAXIBdgF6AX4BggGGAYoBjgGSAZYBmgGeAaIBpgGqAJ9IAXgALAXQAYBEEToAT0gBeAAsBdwBgEQRPgBPSAF4ACwF6AGARBFCAE9IAXgALAX0AYBEEUYAT0gBeAAsBgABgEQRSgBPSAF4ACwGDAGARBFOAE9IAXgALAYYAYBEEVIAT0gBeAAsBiQBgEQRVgBPSAF4ACwGMAGARBFaAE9IAXgALAY8AYBEEV4AT0gBeAAsBkgBgEQRYgBPSAF4ACwGVAGARBFmAE9IAXgALAZgAYBEEWoAT0gBeAAsBmwBgEQRbgBPSAF4ACwGeAGARBFyAE9IAXgALAaEAYBEEXYAT0gBeAAsBpABgEQRegBPSAF4ACwGnAGARBF+AE9IAXgALAaoAYBEEYIAT0gBeAAsBrQBgEQRhgBPTAbAACwGxAbIBswG0XE5TUmFuZ2VDb3VudFtOU1JhbmdlRGF0YRACgG6AbNIBtgALAbcBuFdOUy5kYXRhRAYCEAGAbdIAYgBjAboBu11OU011dGFibGVEYXRhowG8Ab0AZ11OU011dGFibGVEYXRhVk5TRGF0YdIAYgBjAb8BwF8QEU5TTXV0YWJsZUluZGV4U2V0owHBAcIAZ18QEU5TTXV0YWJsZUluZGV4U2V0Wk5TSW5kZXhTZXTSABEACwHEAD+vEBUBxQHGAccByAHJAcoBywHMAc0BzgHPAdAB0QHSAdMB1AHVAdYB1wHYAdmAcIBxgHOAdIB1gHaAeIB5gHqAfIB9gH+AgICCgIOAhYCGgIeAiICKgIyAJ9QB3AALAbAB3QHeAbMADQANWk5TTG9jYXRpb25YTlNMZW5ndGgQAIBu0wGwAAsBsQGyAbMB4oBugHLSAbYACwHkAbhEAgEUAoBt0gGwAAsB3gGzgG7SAbAACwHeAbOAbtIBsAALAd4Bs4Bu0wGwAAsBsQGyAbMB7oBugHfSAbYACwHwAbhEEAETAYBt0gGwAAsB3gGzgG7SAbAACwHeAbOAbtMBsAALAbEBsgGzAfiAboB70gG2AAsB+gG4RAMBDAGAbdQB3AALAbAB3QH9AbMADQANEAeAbtMBsAALAbEBsgGzAgGAboB+0gG2AAsCAwG4RAwCFAGAbdI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AE9IAXgALBRMAYBECgoAT0gBeAAsFFgBgEQKDgBPSAF4ACwUZAGARAoSAE9IAXgALBRwAYBEChYAT0gBeAAsFHwBgEQKGgBPSAF4ACwUiAGARAoeAE9IAXgALBSUAYBECiIAT0gBeAAsFKABgEQKJgBPSAF4ACwUrAGARAoqAE9IAXgALBS4AYBECi4AT0gBeAAsFMQBgEQKMgBPSAF4ACwU0AGARAo2AE9IAXgALBTcAYBECjoAT0gBeAAsFOgBgEQKPgBPSAF4ACwU9AGARApCAE9IAXgALBUAAYBECkYAT0gBeAAsFQwBgEQKSgBPSAF4ACwVGAGARApOAE9MAQQBCAAsFSQVKAEWBAVIQW4Ao0gARAAsFTQA/rxAUBU4FTwVQBVEFUgVTBVQFVQVWBVcFWAVZBVoFWwVcBV0FXgVfBWAFYYEBU4EBVIEBVYEBVoEBV4EBWIEBWYEBWoEBW4EBXIEBXYEBXoEBX4EBYIEBYYEBYoEBY4EBZIEBZYEBZoAn0gBeAAsFZABgEQKUgBPSAF4ACwVnAGARApWAE9IAXgALBWoAYBECloAT0gBeAAsFbQBgEQKXgBPSAF4ACwVwAGARApiAE9IAXgALBXMAYBECmYAT0gBeAAsFdgBgEQKagBPSAF4ACwV5AGARApuAE9IAXgALBXwAYBECnIAT0gBeAAsFfwBgEQKdgBPSAF4ACwWCAGARAp6AE9IAXgALBYUAYBECn4AT0gBeAAsFiABgEQKggBPSAF4ACwWLAGARAqGAE9IAXgALBY4AYBECooAT0gBeAAsFkQBgEQKjgBPSAF4ACwWUAGARAqSAE9IAXgALBZcAYBECpYAT0gBeAAsFmgBgEQKmgBPSAF4ACwWdAGARAqeAE9MAQQBCAAsFoAWhAEWBAWgQXIAo0gARAAsFpAA/rxAUBaUFpgWnBagFqQWqBasFrAWtBa4FrwWwBbEFsgWzBbQFtQW2BbcFuIEBaYEBaoEBa4EBbIEBbYEBboEBb4EBcIEBcYEBcoEBc4EBdIEBdYEBdoEBd4EBeIEBeYEBeoEBe4EBfIAn0gBeAAsFuwBgEQKogBPSAF4ACwW+AGARAqmAE9IAXgALBcEAYBECqoAT0gBeAAsFxABgEQKrgBPSAF4ACwXHAGARAqyAE9IAXgALBcoAYBECrYAT0gBeAAsFzQBgEQKugBP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CToECT4ECUIECUYDGgQJTgQJUgMeBAliAJ9IAXgALDHMAYBEEc4AT0gBeAAsMdgBgEQR0gBPSAF4ACwx5AGARBHWAE9IAXgALDHwAYBEEdoAT0wBBAEIACwx/CV4ARYEC7YAo0gARAAsMggA/rxASCWIJYwMbCWUJZglnCWgMiglqCWsMjQlsCW4JbwlxCXIJcwMcgQJbgQJcgMmBAl6BAl+BAmCBAmGBAu6BAmOBAmSBAu+BAmWBAmeBAmiBAmqBAmuBAmyAyoAn0gBeAAsMlwBgEQR3gBPSAF4ACwyaAGARBHiAE9MAQQBCAAsMnQm1AEWBAvGAKNIAEQALDKAAP68QFAm5CboJuwm8Cb0Mpgm+Cb8JwAnBCcIJwwnECcUJxgnHCcgJyQnLCcyBAnGBAnKBAnOBAnSBAnWBAvKBAnaBAneBAniBAnmBAnqBAnuBAnyBAn2BAn6BAn+BAoCBAoGBAoOBAoSAJ9IAXgALDLcAYBEEeYAT0wGwAAsBsQIQAbMMu4BugQL00gG2AAsMvQG4RgIBDQEQAoBt0gARAAsMwAA/rxAUDMEMwgzDDMQMxQzGDMcMyAzJDMoMywzMDM0MzgzPDNAM0QzSDNMM1IEC9oEC94EC+IEC+YEC+4EC/YEC/oEC/4EDAIEDAYEDAoEDA4EDBIEDBYEDB4EDCYEDCoEDC4EDDYEDD4An0gGwAAsB3gGzgG7UAdwACwGwAd0M2QGzAA0ADRANgG7SAbAACwHeAbOAbtMBsAALAbEBsgGzDN+AboEC+tIBtgALDOEBuEQDAQ0BgG3TAbAACwGxAbIBswzlgG6BAvzSAbYACwznAbhEAgEOAYBt1AHcAAsBsAHdDOoBswANAA0QDoBu0gGwAAsB3gGzgG7SAbAACwHeAbOAbtIBsAALAd4Bs4Bu0gGwAAsB3gGzgG7UAdwACwGwAd0CEAGzAA0ADYBu0gGwAAsB3gGzgG7SAbAACwHeAbOAbtMBsAALAbECEAGzDPyAboEDBtIBtgALDP4BuEYCAQoBDwGAbdMBsAALAbEBsgGzDQKAboEDCNIBtgALDQQBuEQEAQYBgG3UAdwACwGwAd0NBwGzAA0ADRATgG7SAbAACwHeAbOAbtMBsAALAbECEAGzDQ2AboEDDNIBtgALDQ8BuEYEAQYBEQKAbdMBsAALAbECHQGzDROAboEDDtI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9FUYVSBVKFUwVThVQFVkVXBVeFWcVahVsFXUVeBV6FYMVhhWIFZEVoBWiFaQVphWoFaoVrBWuFbAVuRW8Fb4VxxXKFcwV1RXYFdoV4xXmFegV8RX0FfYV/xYCFgQWDRYQFhIWGxYeFiAWIhYrFi4WMBY5FkAWQhZEFkYWSBZRFlQWVhZfFmIWZBZtFnAWchZ7FoAWghaEFoYWjxaSFpQWnRagFqIWqxasFq4WtxbiFuQW5hboFusW7hbxFvQW9xb6Fv0XABcDFwYXCRcMFw8XEhcVFxgXGxcdFyoXLBcuFzAXORdkF2YXaBdqF2wXbhdwF3IXdBd2F3gXehd8F34XgBeCF4QXhheIF4oXjBeOF5cXmhecF6UXqBeqF7MXthe4F8EXxBfGF88X0hfUF90X4BfiF+sX7hfwF/kX/Bf+GAcYChgMGBUYGBgaGCMYJhgoGDEYNBg2GD8YQhhEGE0YUBhSGFsYXhhgGGkYbBhuGHcYehh8GIUYiBiKGJMYlhiYGKEYpBimGLMYtRi3GLkYwhjtGO8Y8RjzGPUY9xj5GPsY/Rj/GQEZAxkFGQcZCRkLGQ0ZDxkRGRMZFRkXGSAZIxklGS4ZMRkzGTwZPxlBGUoZTRlPGVgZWxldGWYZaRlrGXQZdxl5GYIZhRmHGZAZkxmVGZ4ZoRmjGawZrxmxGboZvRm/GcgZyxnNGdYZ2RnbGeQZ5xnpGfIZ9Rn3GgAaAxoFGg4aERoTGhwaHxohGioaLRovGjwaPhpAGkIaSxp2Gngaehp8Gn4agBqDGoYaiRqMGo8akhqVGpgamxqeGqEapBqnGqoarRqvGrgauxq9GsYayRrLGtQa1xrZGuIa5RrnGvAa8xr1Gv4bARsDGwwbDxsRGxobHRsfGygbKxstGzYbORs7G0QbRxtJG1IbVRtXG2AbYxtlG24bcRtzG3wbfxuBG4objRuPG5gbmxudG6YbqRurG7Qbtxu5G8IbxRvHG9Qb1xvZG9sb5BwPHBIcFRwYHBscHhwhHCQcJxwqHC0cMBwzHDYcORw8HD8cQhxFHEgcSxxNHFYcWRxbHGQcZxxpHHIcdRx3HIAcgxyFHI4ckRyTHJwcnxyhHKocrRyvHLgcuxy9HMYcyRzLHNQc1xzZHOIc5RznHPAc8xz1HP4dAR0DHQwdDx0RHRodHR0fHSgdKx0tHTYdOR07HUQdRx1JHVIdVR1XHWAdYx1lHXIddR13HXkdgh2tHbAdsx22HbkdvB2/HcIdxR3IHcsdzh3RHdQd1x3aHd0d4B3jHeYd6R3rHfQd9x35HgIeBR4HHhAeEx4VHh4eIR4jHiweLx4xHjoePR4/HkgeSx5NHlYeWR5bHmQeZx5pHnIedR53HoAegx6FHo4ekR6THpwenx6hHqoerR6vHrgeux69HsYeyR7LHtQe1x7ZHuIe5R7nHvAe8x71Hv4fAR8DHxAfEx8VHxcfIB9LH04fUR9UH1cfWh9dH2AfYx9mH2kfbB9vH3IfdR94H3sffh+BH4Qfhx+JH5IflR+XH6Afox+lH64fsR+zH7wfvx/BH8ofzR/PH9gf2x/dH+Yf6R/rH/Qf9x/5IAIgBSAHIBAgEyAVIB4gISAjICwgLyAxIDogPSA/IEggSyBNIFYgWSBbIGQgZyBpIHIgdSB3IIAggyCFII4gkSCTIJwgnyChIK4gsSCzILUgviDpIOwg7yDyIPUg+CD7IP4hASEEIQchCiENIRAhEyEWIRkhHCEfISIhJSEnITAhMyE1IT4hQSFDIUwhTyFRIVohXSFfIWghayFtIXYheSF7IYQhhyGJIZIhlSGXIaAhoyGlIa4hsSGzIbwhvyHBIcohzSHPIdgh2yHdIeYh6SHrIfQh9yH5IgIiBSIHIhAiEyIVIh4iISIjIiwiLyIxIjoiPSI/IkwiTyJRIlMiXCKHIooijSKQIpMiliKZIpwinyKiIqUiqCKrIq4isSK0IrciuiK9IsAiwyLFIs4i0SLTItwi3yLhIuoi7SLvIvgi+yL9IwYjCSMLIxQjFyMZIyIjJSMnIzAjMyM1Iz4jQSNDI0wjTyNRI1ojXSNfI2gjayNtI3YjeSN7I4QjhyOJI5IjlSOXI6AjoyOlI64jsSOzI7wjvyPBI8ojzSPPI9gj2yPdI+oj7SPvI/Ej+iQlJCgkKyQuJDEkNCQ3JDokPSRAJEMkRiRJJEwkTyRSJFUkWCRbJF4kYSRjJGwkbyRxJHokfSR/JIgkiySNJJYkmSSbJKQkpySpJLIktSS3JMAkwyTFJM4k0STTJNwk3yThJOok7STvJPgk+yT9JQYlCSULJRQlFyUZJSIlJSUnJTAlMyU1JT4lQSVDJUwlTyVRJVolXSVfJWglayVtJXYleSV7JYgliyWNJY8lmCXDJcYlySXMJc8l0iXVJdgl2yXeJeEl5CXnJeol7SXwJfMl9iX5Jfwl/yYBJgomDSYPJhgmGyYdJiYmKSYrJjQmNyY5JkImRSZHJlAmUyZVJl4mYSZjJmwmbyZxJnomfSZ/JogmiyaNJpYmmSabJqQmpyapJrImtSa3JsAmwybFJs4m0SbTJtwm3ybhJuom7SbvJvgm+yb9JwYnCScLJxQnFycZJyYnKScrJy0nNidhJ2QnZydqJ20ncCdzJ3YneSd8J38ngieFJ4gniyeOJ5EnlCeXJ5onnSefJ6gnqyetJ7YnuSe7J8QnxyfJJ9In1SfXJ+An4yflJ+4n8SfzJ/wn/ygBKAooDSgPKBgoGygdKCYoKSgrKDQoNyg5KEIoRShHKFAoUyhVKF4oYShjKGwobyhxKHoofSh/KIgoiyiNKJYomSibKKQopyipKLIotSi3KMQoxyjJKMso1Cj/KQIpBSkIKQspDikRKRQpFykaKR0pICkjKSYpKSksKS8pMik1KTgpOyk9KUYpSSlLKVQpVylZKWIpZSlnKXApcyl1KX4pgSmDKYwpjymRKZopnSmfKagpqymtKbYpuSm7KcQpxynJKdIp1SnXKeAp4ynlKe4p8SnzKfwp/yoBKgoqDSoPKhgqGyodKiYqKSorKjQqNyo5KkIqRSpHKlAqUypVKmIqZSpnKmkqciqdKqAqoyqmKqkqrCqvKrIqtSq4KrsqvirBKsQqxyrKKs0q0CrTKtYq2SrbKuQq5yrpKvIq9Sr3KwArAysFKw4rESsTKxwrHyshKyorLSsvKzgrOys9K0YrSStLK1QrVytZK2IrZStnK3Arcyt1K34rgSuDK4wrjyuRK5ornSufK6grqyutK7YruSu7K8QrxyvJK9Ir1SvXK+Ar4yvlK+4r8SvzLAAsAywFLAcsECw7LD4sQSxELEcsSixNLFAsUyxWLFksXCxfLGIsZSxoLGssbixxLHQsdyx5LIIshSyHLJAskyyVLJ4soSyjLKwsryyxLLosvSy/LMgsyyzNLNYs2SzbLOQs5yzpLPIs9Sz3LQAtAy0FLQ4tES0TLRwtHy0hLSotLS0vLTgtOy09LUYtSS1LLVQtVy1ZLWItZS1nLXAtcy11LX4tgS2DLYwtjy2RLZ4toS2jLaUtri3ZLdwt3y3iLeUt6C3rLe4t8S30Lfct+i39LgAuAy4GLgkuDC4PLhIuFS4XLiAuIy4lLi4uMS4zLjwuPy5BLkouTS5PLlguWy5dLmYuaS5rLnQudy55LoIuhS6HLpAuky6VLp4uoS6jLqwury6xLrouvS6/Lsguyy7NLtYu2S7bLuQu5y7pLvIu9S73LwAvAy8FLw4vES8TLxwvHy8hLyovLS8vLzwvPy9BL0MvTC93L3ovfS+AL4Mvhi+JL4wvjy+SL5UvmC+bL54voS+kL6cvqi+tL7Avsy+1L74vwS/DL8wvzy/RL9ov3S/fL+gv6y/tL/Yv+S/7MAQwBzAJMBIwFTAXMCAwIzAlMC4wMTAzMDwwPzBBMEowTTBPMFgwWzBdMGYwaTBrMHQwdzB5MIIwhTCHMJAwkzCVMJ4woTCjMKwwrzCxMLowvTC/MMgwyzDNMNow3TDfMOEw6jEVMRgxGzEeMSExJDEnMSoxLTEwMTMxNjE5MTwxPzFCMUUxSDFLMU4xUTFTMVwxXzFhMWoxbTFvMXgxezF9MYYxiTGLMZQxlzGZMaIxpTGnMbAxszG1Mb4xwTHDMcwxzzHRMdox3THfMegx6zHtMfYx+TH7MgQyBzIJMhIyFTIXMiAyIzIlMi4yMTIzMjwyPzJBMkoyTTJPMlgyWzJdMmYyaTJrMngyezJ9Mn8yiDKzMrYyuTK8Mr8ywjLFMsgyyzLOMtEy1DLXMtoy3TLgMuMy5jLpMuwy7zLxMvoy/TL/MwgzCzMNMxYzGTMbMyQzJzMpMzIzNTM3M0AzQzNFM04zUTNTM1wzXzNhM2ozbTNvM3gzezN9M4YziTOLM5QzlzOZM6IzpTOnM7AzszO1M74zwTPDM8wzzzPRM9oz3TPfM+gz6zPtM/Yz+TP7NAQ0BzQJNBY0GTQbNB00JjRRNFQ0VzRaNF00YDRjNGY0aTRsNG80cjR1NHg0ezR+NIE0hDSHNIo0jTSPNJg0mzSdNKY0qTSrNLQ0tzS5NMI0xTTHNNA00zTVNN404TTjNOw07zTxNPo0/TT/NQg1CzUNNRY1GTUbNSQ1JzUpNTI1NTU3NUA1QzVFNU41UTVTNVw1XzVhNWo1bTVvNXg1ezV9NYY1iTWLNZQ1lzWZNaI1pTWnNbQ1tzW5Nbs1xDXvNfI19TX4Nfs1/jYBNgQ2BzYKNg02EDYTNhY2GTYcNh82IjYlNig2KzYtNjY2OTY7NkQ2RzZJNlI2VTZXNmA2YzZlNm42cTZzNnw2fzaBNoo2jTaPNpg2mzadNqY2qTarNrQ2tza5NsI2xTbHNtA20zbVNt424TbjNuw27zbxNvo2/Tb/Nwg3CzcNNxY3GTcbNyQ3JzcpNzI3NTc3N0A3QzdFN043ezd+N4E3gzeGN4k3jDePN5I3lTeYN5s3njehN6M3pjepN6s3rTewN7M3tje4N8U3yDfKN9M3/jgBOAM4BTgHOAk4CzgNOA84ETgTOBU4FzgZOBs4HTgfOCE4IzglOCc4KTgyODU4NzhEOEc4SThSOIM4hTiHOIo4jDiOOJA4kziWOJk4nDifOKE4pDinOKo4rTiwOLM4tji5OLw4vzjCOMQ4zTjQONI42zjeOOA46TjsOO44+zj+OQA5CTkwOTM5Njk5OTw5PzlBOUQ5RzlKOU05UDlTOVY5WTlcOV85YjllOWc5dDl3OXk5gjmpOaw5rjmxObQ5tzm6Ob05wDnDOcY5yTnMOc850jnVOdg52jndOd857DnvOfE5+jonOio6LTowOjM6NTo4Ojo6PTpAOkM6RTpIOks6TjpROlQ6VzpaOl06YDpjOmU6bjpxOnM6fDp/OoE6jjqROpM6lTqeOsE6xDrHOso6zTrQOtM61jrZOtw63zriOuU66DrrOu468TrzOvw6/zsBOwo7DTsPOxg7GzsdOyY7KTsrOzQ7Nzs5O0I7RTtHO1A7UztVO147YTtjO2w7bztxO3o7fTt/O4g7izuNO5Y7mTubO6Q7pzupO7I7tTu3O8A7wzvFO8470TvTO+A74zvlO+c78DwZPBw8HzwiPCU8KDwrPC48MTw0PDc8Ojw9PEA8QzxGPEk8TDxPPFI8VDxdPGA8YjxrPG48cDx5PHw8fjyHPIo8jDyVPJg8mjyjPKY8qDyxPLQ8tjy/PMI8xDzNPNA80jzbPN484DzpPOw87jz3PPo8/D0FPQg9Cj0TPRY9GD0hPSQ9Jj0vPTI9ND09PUA9Qj1LPU49UD1ZPVw9Xj1rPW49cD15Pag9qz2uPbE9tD23Pbo9vT3APcI9xT3IPcs9zj3RPdQ91z3aPd094D3jPeY96T3rPfQ99z35PgI+BT4HPhQ+Fz4ZPiI+TT5QPlM+VT5YPls+Xj5hPmQ+Zz5qPm0+cD5zPnU+eD57Pn4+gT6EPoc+iT6SPpU+lz6kPqc+qT6yPuM+5j7pPuw+7z7yPvU++D77Pv4/AT8EPwY/CT8MPw8/Ej8VPxg/Gz8dPyA/Iz8mPyg/MT80PzY/Pz9CP0Q/TT9QP1I/Xz9iP2Q/bT+SP5U/mD+bP54/oT+kP6c/qj+tP68/sj+1P7g/uz++P8A/wz/FP9I/1T/XP+BAC0AOQBFAFEAXQBpAHEAfQCJAJEAnQClALEAvQDJANUA4QDtAPkBBQENARUBOQFFAU0BcQF9AYUBuQHFAc0B8QKtArkCwQLNAtkC4QLtAvkDBQMRAx0DKQMxAzkDRQNRA10DaQN1A4EDiQORA5kDoQPFA9ED2QP9BAkEEQQ1BEEESQR9BIkEkQSZBL0FSQVVBWEFbQV5BYUFkQWdBakFtQXBBc0F2QXlBfEF/QYJBhEGNQZBBkkGbQZ5BoEGpQaxBrkG3QbpBvEHFQchBykHTQdZB2EHhQeRB5kHvQfJB9EH9QgBCAkILQg5CEEIZQhxCHkInQipCLEI1QjhCOkJDQkZCSEJRQlRCVkJfQmJCZEJxQnRCdkJ/QqpCrUKwQrNCtkK5QrxCvkLBQsRCx0LKQs1C0ELTQtZC2ELbQt5C4ELjQuVC7kLxQvNC/EL/QwFDCkMNQw9DGEMbQx1DKkMtQy9DOENfQ2JDZUNnQ2pDbUNwQ3NDdkN5Q3xDf0OCQ4VDiEOLQ45DkUOTQ5VDnkOhQ6NDrEOvQ7FDvkPBQ8NDzEP3Q/pD/UQARANEBkQJRAxED0QSRBVEGEQbRB5EIUQkRCdEKkQtRDBEM0Q1RD5EQURDRFBEUkRVRF5EZURnRHBEm0SeRKFEpESnRKpErUSwRLNEtkS5RLxEv0TCRMVEyETLRM5E0UTURNdE2UTiRORE9UT3RPlFAkUERRFFE0UWRR9FJEUmRTNFNUU4RUFFRkVIRVlFW0VdRWZFaEVxRXNFfEV+RYdFiUWaRZxFpUWnRbBFskW/RcFFxEXNRdRF1kXjReVF6EXxRfZF+EYJRgtGDUYWRhhGJUYnRipGM0Y6RjxGSUZLRk5GV0ZgRmJGc0Z1RndGgEarRq5GsUa0RrdGuka9RsBGw0bGRslGzEbPRtJG1UbYRttG3kbhRuRG50bpRvJG9EcBRwNHBkcPRxRHFkcnRylHK0c8Rz5HQEdNR09HUkdbR2BHYkdvR3FHdEd9R4RHhkePR5FHmkecR61Hr0e4R7pHx0fJR8xH1UfaR9xH6UfrR+5H90f8R/5IB0gJSBZIGEgbSCRIKUgrSDhIOkg9SEZIT0hRSF5IYEhjSGxIdUh3SIBIgkiPSJFIlEidSKRIpkizSLVIuEjBSMZIyEjRSNNI3EkJSQxJD0kSSRVJGEkbSR5JIUkkSSdJKkktSTBJM0k2STlJPEk/SUJJRUlISUpJU0lWSVlJW0lkSWtJbklxSXRJdkl/SYBJg0mMSZFJmkmbSZ1JpkmnSalJskm3SbpJvUm/SchJzUnQSdNJ1UneSeFJ5EnmSe9J9kn5SfxJ/0oBSgpKC0oNShZKG0oeSiFKI0osSjNKNko5SjxKPkpHSlBKU0pWSllKXEpeSmdKbEpvSnJKdEp9SoBKg0qFSpJKlEqXSqBKp0qpSrJKuUq8Sr9KwkrESs1K2krfSuxK9UsESwlLGEsqSy9LNAAAAAAAAAICAAAAAAAADfoAAAAAAAAAAAAAAAAAAEs2 \ No newline at end of file From 3ca95778df40e376d352ff987dab00e9a7a1847e Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:11:21 -0700 Subject: [PATCH 44/51] [ASThrashTesting] Some cleanup --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 6b849396..4289c629 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -9,6 +9,7 @@ @import XCTest; #import +// Set to 1 to use UITableView and see if the issue still exists. #define USE_UIKIT_REFERENCE 0 #if USE_UIKIT_REFERENCE @@ -85,7 +86,6 @@ static volatile int32_t ASThrashTestItemNextID = 1; return (self.itemID % 400) ?: 44; } - - (NSString *)description { return [NSString stringWithFormat:@"", (unsigned long)_itemID]; } @@ -299,7 +299,6 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @property (nonatomic, strong, readonly) NSMutableArray *insertedItemIndexes; @property (nonatomic, strong, readonly) NSMutableArray *> *insertedItems; -/// NOTE: `data` will be modified - (instancetype)initWithData:(NSArray *)data; + (ASThrashUpdate *)thrashUpdateWithBase64String:(NSString *)base64; From bd7f90f7ee41b32d137c4a145d418ca09ed836a8 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:14:01 -0700 Subject: [PATCH 45/51] [ASThrashTesting] Remove unneeded clang diagnostics --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 4289c629..cb1f9947 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -26,8 +26,6 @@ #define kFickleness 0.1 #define kThrashingIterationCount 100 -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" static NSString *ASThrashArrayDescription(NSArray *array) { NSMutableString *str = [NSMutableString stringWithString:@"(\n"]; NSInteger i = 0; @@ -38,7 +36,6 @@ static NSString *ASThrashArrayDescription(NSArray *array) { [str appendString:@")"]; return str; } -#pragma clang diagnostic pop static volatile int32_t ASThrashTestItemNextID = 1; @interface ASThrashTestItem: NSObject From 9fc3ec9096e9fe5015708c9f761bf4069e5ef5fd Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:19:36 -0700 Subject: [PATCH 46/51] [ASThrashTesting] Numbers so magic --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index cb1f9947..f1c6c880 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -462,7 +462,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; #pragma mark Test Methods - (void)testInitialDataRead { - ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } @@ -484,7 +484,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; - (void)DISABLED_testThrashingWildly { for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; - ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:20]]; + ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; _update = [[ASThrashUpdate alloc] initWithData:ds.data]; [self applyUpdate:_update toDataSource:ds]; From 12336de32548cf825bae840f8d8e844c084aa4c6 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 22 Jun 2016 17:25:09 -0700 Subject: [PATCH 47/51] [ASThrashTesting] Array is immutable --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index f1c6c880..5d6fa8f6 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -308,7 +308,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; self = [super init]; if (self != nil) { _data = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; - _oldData = [[NSMutableArray alloc] initWithArray:data copyItems:YES]; + _oldData = [[NSArray alloc] initWithArray:data copyItems:YES]; _deletedItemIndexes = [NSMutableArray array]; _replacedItemIndexes = [NSMutableArray array]; From 54e29d2236658e288fba6f050ec32925b1d450b9 Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Wed, 22 Jun 2016 23:22:42 -0700 Subject: [PATCH 48/51] [Carthage] fix travis build error (#1804) * [Carthage] Add cartfile, update example * [Carthage] fix travis build issue * [Carthage] fix cartfile for travis build error * capitalization fix --- examples/CarthageBuildTest/Cartfile | 2 +- .../CarthageExample/Base.lproj/LaunchScreen.storyboard | 6 +++--- .../CarthageExample/Base.lproj/Main.storyboard | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/CarthageBuildTest/Cartfile b/examples/CarthageBuildTest/Cartfile index 5bacd1f3..aa14143b 100644 --- a/examples/CarthageBuildTest/Cartfile +++ b/examples/CarthageBuildTest/Cartfile @@ -1 +1 @@ -git "file:///Users/scottg/code/AsyncDisplayKit" "master" +github "facebook/AsyncDisplayKit" "master" diff --git a/examples/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard b/examples/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard index 2e721e18..ebf48f60 100644 --- a/examples/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard +++ b/examples/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,8 @@ - + - + + @@ -15,7 +16,6 @@ - diff --git a/examples/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard b/examples/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard index f56d2f3b..82cf14be 100644 --- a/examples/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard +++ b/examples/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard @@ -1,13 +1,14 @@ - + - + + - + From d850057916d0ff5d7669f99dfa9c4b41548b7d93 Mon Sep 17 00:00:00 2001 From: Hannah Troisi Date: Thu, 23 Jun 2016 16:25:48 -0700 Subject: [PATCH 49/51] [Travis CI] move carthageBuildTest to examples_extra to avoid Travis error --- {examples => examples_extra}/CarthageBuildTest/Cartfile | 0 .../CarthageBuildTest/CarthageExample/AppDelegate.h | 0 .../CarthageBuildTest/CarthageExample/AppDelegate.m | 0 .../Assets.xcassets/AppIcon.appiconset/Contents.json | 0 .../CarthageExample/Base.lproj/LaunchScreen.storyboard | 0 .../CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard | 0 .../CarthageBuildTest/CarthageExample/Info.plist | 0 .../CarthageBuildTest/CarthageExample/ViewController.h | 0 .../CarthageBuildTest/CarthageExample/ViewController.m | 0 .../CarthageBuildTest/CarthageExample/main.m | 0 {examples => examples_extra}/CarthageBuildTest/README.md | 0 .../CarthageBuildTest/Sample.xcodeproj/project.pbxproj | 0 .../Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata | 0 .../project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings | 0 .../Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename {examples => examples_extra}/CarthageBuildTest/Cartfile (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/AppDelegate.h (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/AppDelegate.m (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/Info.plist (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/ViewController.h (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/ViewController.m (100%) rename {examples => examples_extra}/CarthageBuildTest/CarthageExample/main.m (100%) rename {examples => examples_extra}/CarthageBuildTest/README.md (100%) rename {examples => examples_extra}/CarthageBuildTest/Sample.xcodeproj/project.pbxproj (100%) rename {examples => examples_extra}/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {examples => examples_extra}/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {examples => examples_extra}/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme (100%) diff --git a/examples/CarthageBuildTest/Cartfile b/examples_extra/CarthageBuildTest/Cartfile similarity index 100% rename from examples/CarthageBuildTest/Cartfile rename to examples_extra/CarthageBuildTest/Cartfile diff --git a/examples/CarthageBuildTest/CarthageExample/AppDelegate.h b/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.h similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/AppDelegate.h rename to examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.h diff --git a/examples/CarthageBuildTest/CarthageExample/AppDelegate.m b/examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.m similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/AppDelegate.m rename to examples_extra/CarthageBuildTest/CarthageExample/AppDelegate.m diff --git a/examples/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples_extra/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json rename to examples_extra/CarthageBuildTest/CarthageExample/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/examples/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard b/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard rename to examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/LaunchScreen.storyboard diff --git a/examples/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard b/examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard rename to examples_extra/CarthageBuildTest/CarthageExample/Base.lproj/Main.storyboard diff --git a/examples/CarthageBuildTest/CarthageExample/Info.plist b/examples_extra/CarthageBuildTest/CarthageExample/Info.plist similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/Info.plist rename to examples_extra/CarthageBuildTest/CarthageExample/Info.plist diff --git a/examples/CarthageBuildTest/CarthageExample/ViewController.h b/examples_extra/CarthageBuildTest/CarthageExample/ViewController.h similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/ViewController.h rename to examples_extra/CarthageBuildTest/CarthageExample/ViewController.h diff --git a/examples/CarthageBuildTest/CarthageExample/ViewController.m b/examples_extra/CarthageBuildTest/CarthageExample/ViewController.m similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/ViewController.m rename to examples_extra/CarthageBuildTest/CarthageExample/ViewController.m diff --git a/examples/CarthageBuildTest/CarthageExample/main.m b/examples_extra/CarthageBuildTest/CarthageExample/main.m similarity index 100% rename from examples/CarthageBuildTest/CarthageExample/main.m rename to examples_extra/CarthageBuildTest/CarthageExample/main.m diff --git a/examples/CarthageBuildTest/README.md b/examples_extra/CarthageBuildTest/README.md similarity index 100% rename from examples/CarthageBuildTest/README.md rename to examples_extra/CarthageBuildTest/README.md diff --git a/examples/CarthageBuildTest/Sample.xcodeproj/project.pbxproj b/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.pbxproj similarity index 100% rename from examples/CarthageBuildTest/Sample.xcodeproj/project.pbxproj rename to examples_extra/CarthageBuildTest/Sample.xcodeproj/project.pbxproj diff --git a/examples/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from examples/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/examples/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from examples/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to examples_extra/CarthageBuildTest/Sample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/examples/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples_extra/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme similarity index 100% rename from examples/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme rename to examples_extra/CarthageBuildTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme From 997d37dc83feeb03d2fccda76a8bec4b2c92bbc5 Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 23 Jun 2016 20:07:45 -0700 Subject: [PATCH 50/51] [ASEnvironment] Don't relayout as a result of clearing a traitCollection's context (#1759) * Don't relayout as a result of clearing a traitCollection's context I'm not completely sure this change is the best solution. Here is context: An ASEnvironmentTraitCollection has a pointer to an optional context that an ASVC is the owner of. When the ASVC is dealloc'ed, we go through all subnodes of the VC and clear out the context so that the struct isn't holding on to a garbage pointer. Setting the traitCollection on ASCollectionNode/ASTableNode causes the cells to relayout if the trait collection changed (this is a special case for these two nodes since their cells are not actually subnodes). Setting the context to nil registered as a trait collection change and was causing a layout even as we were dealloc'ing the VC. The logic I'm implementing here is: If the trait collection changed AND the displayContext did not, then we should relayout. If the trait collection changed AND the new displayContext is non-nil then we should layout In the case where the trait collection change was caused soley by the displayContext going from non-nil to nil, then we should NOT layout. ``` // At this point we know that the two traits collections are NOT equal for some reason BOOL needsLayout = (oldTraits.displayContext == currentTraits.displayContext) || currentTraits.displayContext != nil; ``` Is there a better place/safer way to do this? * removed extra setNeedsLayout call --- AsyncDisplayKit/Details/ASEnvironment.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/Details/ASEnvironment.h b/AsyncDisplayKit/Details/ASEnvironment.h index 6c395aa1..e267de1e 100644 --- a/AsyncDisplayKit/Details/ASEnvironment.h +++ b/AsyncDisplayKit/Details/ASEnvironment.h @@ -147,6 +147,9 @@ ASDISPLAYNODE_EXTERN_C_END // // If there is any new downward propagating state, it should be added to this define. // +// If the only change in a trait collection is that its dislplayContext has gone from non-nil to nil, +// assume that we are clearing the context as part of a ASVC dealloc and do not trigger a layout. +// // This logic is used in both ASCollectionNode and ASTableNode #define ASEnvironmentCollectionTableSetEnvironmentState(lock) \ - (void)setEnvironmentState:(ASEnvironmentState)environmentState\ @@ -156,12 +159,16 @@ ASDISPLAYNODE_EXTERN_C_END [super setEnvironmentState:environmentState];\ ASEnvironmentTraitCollection currentTraits = environmentState.environmentTraitCollection;\ if (ASEnvironmentTraitCollectionIsEqualToASEnvironmentTraitCollection(currentTraits, oldTraits) == NO) {\ + /* Must dispatch to main for self.view && [self.view.dataController completedNodes]*/ \ ASPerformBlockOnMainThread(^{\ + BOOL needsLayout = (oldTraits.displayContext == currentTraits.displayContext) || currentTraits.displayContext != nil;\ NSArray *> *completedNodes = [self.view.dataController completedNodes];\ for (NSArray *sectionArray in completedNodes) {\ for (ASCellNode *cellNode in sectionArray) {\ ASEnvironmentStatePropagateDown(cellNode, currentTraits);\ - [cellNode setNeedsLayout];\ + if (needsLayout) {\ + [cellNode setNeedsLayout];\ + }\ }\ }\ });\ From 457e08005f3c1e03e73447fe25825c24eb31e7f1 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 23 Jun 2016 21:08:44 -0700 Subject: [PATCH 51/51] [ASDataController] Remove asyncDataFetching Option, Cleanup (#1794) * [ASDataController] Add some assertions to clarify what queues things happen on * [ASCollectionDataController] Optimize willReloadData * [ASDataController] Minor optimizations, no functional changes * [ASDataController] Always reload data on _editingTransactionQueue * [ASDataController] Remove async data fetching option, deprecate callbacks * [ASDataController] Not mutable * [ASMultidimensionalArrayUtils] Use fast enumeration * Optimize ASMultidimensionalArrayUtils --- AsyncDisplayKit/ASCollectionView.h | 6 +- AsyncDisplayKit/ASCollectionView.mm | 34 +- AsyncDisplayKit/ASTableView.h | 24 +- AsyncDisplayKit/ASTableView.mm | 34 +- .../Details/ASChangeSetDataController.m | 13 +- .../Details/ASCollectionDataController.mm | 14 +- AsyncDisplayKit/Details/ASDataController.h | 25 -- AsyncDisplayKit/Details/ASDataController.mm | 316 +++++++++--------- .../Private/ASMultidimensionalArrayUtils.mm | 22 +- AsyncDisplayKitTests/ASTableViewTests.m | 3 +- 10 files changed, 192 insertions(+), 299 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index a2d5e2ec..9b7fe864 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -407,8 +407,9 @@ NS_ASSUME_NONNULL_BEGIN * due to the data access in async mode. * * @param collectionView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. */ -- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView; +- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED; /** * Indicator to unlock the data source for data fetching in async mode. @@ -416,8 +417,9 @@ NS_ASSUME_NONNULL_BEGIN * due to the data access in async mode. * * @param collectionView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. */ -- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView; +- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView ASDISPLAYNODE_DEPRECATED; @end diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 0c979f43..68031f46 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -112,7 +112,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; BOOL _performingBatchUpdates; NSMutableArray *_batchUpdateBlocks; - BOOL _asyncDataFetchingEnabled; _ASCollectionViewNodeSizeInvalidationContext *_queuedNodeSizeInvalidationContext; // Main thread only BOOL _isDeallocating; @@ -155,14 +154,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; unsigned int asyncDataSourceNodeForItemAtIndexPath:1; unsigned int asyncDataSourceNodeBlockForItemAtIndexPath:1; unsigned int asyncDataSourceNumberOfSectionsInCollectionView:1; - unsigned int asyncDataSourceCollectionViewLockDataSource:1; - unsigned int asyncDataSourceCollectionViewUnlockDataSource:1; unsigned int asyncDataSourceCollectionViewConstrainedSizeForNodeAtIndexPath:1; } _asyncDataSourceFlags; } -@property (atomic, assign) BOOL asyncDataSourceLocked; - // Used only when ASCollectionView is created directly rather than through ASCollectionNode. // We create a node so that logic related to appearance, memory management, etc can be located there // for both the node-based and view-based version of the table. @@ -231,7 +226,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _rangeController.delegate = self; _rangeController.layoutController = _layoutController; - _dataController = [[ASCollectionDataController alloc] initWithAsyncDataFetching:NO]; + _dataController = [[ASCollectionDataController alloc] init]; _dataController.delegate = _rangeController; _dataController.dataSource = self; _dataController.environmentDelegate = self; @@ -240,9 +235,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _leadingScreensForBatching = 2.0; - _asyncDataFetchingEnabled = NO; - _asyncDataSourceLocked = NO; - _performingBatchUpdates = NO; _batchUpdateBlocks = [NSMutableArray array]; @@ -375,9 +367,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _asyncDataSourceFlags.asyncDataSourceNodeForItemAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeForItemAtIndexPath:)]; _asyncDataSourceFlags.asyncDataSourceNodeBlockForItemAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:nodeBlockForItemAtIndexPath:)]; _asyncDataSourceFlags.asyncDataSourceNumberOfSectionsInCollectionView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]; - _asyncDataSourceFlags.asyncDataSourceCollectionViewLockDataSource = [_asyncDataSource respondsToSelector:@selector(collectionViewLockDataSource:)]; - _asyncDataSourceFlags.asyncDataSourceCollectionViewUnlockDataSource = [_asyncDataSource respondsToSelector:@selector(collectionViewUnlockDataSource:)]; - _asyncDataSourceFlags.asyncDataSourceCollectionViewConstrainedSizeForNodeAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)];; + _asyncDataSourceFlags.asyncDataSourceCollectionViewConstrainedSizeForNodeAtIndexPath = [_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)]; // Data-source must implement collectionView:nodeForItemAtIndexPath: or collectionView:nodeBlockForItemAtIndexPath: ASDisplayNodeAssertTrue(_asyncDataSourceFlags.asyncDataSourceNodeBlockForItemAtIndexPath || _asyncDataSourceFlags.asyncDataSourceNodeForItemAtIndexPath); @@ -937,26 +927,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } -- (void)dataControllerLockDataSource -{ - ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked"); - - self.asyncDataSourceLocked = YES; - if (_asyncDataSourceFlags.asyncDataSourceCollectionViewLockDataSource) { - [_asyncDataSource collectionViewLockDataSource:self]; - } -} - -- (void)dataControllerUnlockDataSource -{ - ASDisplayNodeAssert(self.asyncDataSourceLocked, @"The data source has already been unlocked"); - - self.asyncDataSourceLocked = NO; - if (_asyncDataSourceFlags.asyncDataSourceCollectionViewUnlockDataSource) { - [_asyncDataSource collectionViewUnlockDataSource:self]; - } -} - - (id)dataControllerEnvironment { if (self.collectionNode) { diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index 4ab48771..7d36cc3d 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -44,18 +44,8 @@ NS_ASSUME_NONNULL_BEGIN * The frame of the table view changes as table cells are added and deleted. * * @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants. - * - * @param asyncDataFetchingEnabled This option is reserved for future use, and currently a no-op. - * - * @discussion If asyncDataFetching is enabled, the `ASTableView` will fetch data through `tableView:numberOfRowsInSection:` and - * `tableView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically - * from calling thread. - * Enabling asyncDataFetching could avoid blocking main thread for `ASCellNode` allocation, which is frequently reported issue for - * large scale data. On another hand, the application code need take the responsibility to avoid data inconsistence. Specifically, - * we will lock the data source through `tableViewLockDataSource`, and unlock it by `tableViewUnlockDataSource` after the data fetching. - * The application should not update the data source while the data source is locked, to keep data consistence. */ -- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled; +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style; /** * Tuning parameters for a range type in full mode. @@ -363,8 +353,9 @@ NS_ASSUME_NONNULL_BEGIN * due to the data access in async mode. * * @param tableView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. */ -- (void)tableViewLockDataSource:(ASTableView *)tableView; +- (void)tableViewLockDataSource:(ASTableView *)tableView ASDISPLAYNODE_DEPRECATED; /** * Indicator to unlock the data source for data fetching in asyn mode. @@ -372,8 +363,9 @@ NS_ASSUME_NONNULL_BEGIN * due to the data access in async mode. * * @param tableView The sender. + * @deprecated The data source is always accessed on the main thread, and this method will not be called. */ -- (void)tableViewUnlockDataSource:(ASTableView *)tableView; +- (void)tableViewUnlockDataSource:(ASTableView *)tableView ASDISPLAYNODE_DEPRECATED; @end @@ -458,4 +450,10 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASTableViewDelegate @end +@interface ASTableView (Deprecated) + +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled ASDISPLAYNODE_DEPRECATED; + +@end + NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 871589b4..c978bb9e 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -100,8 +100,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASRangeController *_rangeController; - BOOL _asyncDataFetchingEnabled; - ASBatchContext *_batchContext; NSIndexPath *_pendingVisibleIndexPath; @@ -133,12 +131,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; unsigned int asyncDataSourceNumberOfSectionsInTableView:1; unsigned int asyncDataSourceTableViewNodeBlockForRowAtIndexPath:1; unsigned int asyncDataSourceTableViewNodeForRowAtIndexPath:1; - unsigned int asyncDataSourceTableViewLockDataSource:1; - unsigned int asyncDataSourceTableViewUnlockDataSource:1; } _asyncDataSourceFlags; } -@property (atomic, assign) BOOL asyncDataSourceLocked; @property (nonatomic, strong, readwrite) ASDataController *dataController; // Used only when ASTableView is created directly rather than through ASTableNode. @@ -177,16 +172,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; _rangeController.dataSource = self; _rangeController.delegate = self; - _dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:NO]; + _dataController = [[dataControllerClass alloc] init]; _dataController.dataSource = self; _dataController.delegate = _rangeController; _dataController.environmentDelegate = self; _layoutController.dataSource = _dataController; - _asyncDataFetchingEnabled = NO; - _asyncDataSourceLocked = NO; - _leadingScreensForBatching = 2.0; _batchContext = [[ASBatchContext alloc] init]; @@ -290,8 +282,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; _asyncDataSourceFlags.asyncDataSourceNumberOfSectionsInTableView = [_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]; _asyncDataSourceFlags.asyncDataSourceTableViewNodeForRowAtIndexPath = [_asyncDataSource respondsToSelector:@selector(tableView:nodeForRowAtIndexPath:)]; _asyncDataSourceFlags.asyncDataSourceTableViewNodeBlockForRowAtIndexPath = [_asyncDataSource respondsToSelector:@selector(tableView:nodeBlockForRowAtIndexPath:)]; - _asyncDataSourceFlags.asyncDataSourceTableViewLockDataSource = [_asyncDataSource respondsToSelector:@selector(tableViewLockDataSource:)]; - _asyncDataSourceFlags.asyncDataSourceTableViewUnlockDataSource = [_asyncDataSource respondsToSelector:@selector(tableViewUnlockDataSource:)]; // Data source must implement tableView:nodeBlockForRowAtIndexPath: or tableView:nodeForRowAtIndexPath: ASDisplayNodeAssertTrue(_asyncDataSourceFlags.asyncDataSourceTableViewNodeBlockForRowAtIndexPath || _asyncDataSourceFlags.asyncDataSourceTableViewNodeForRowAtIndexPath); @@ -1083,28 +1073,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; CGSizeMake(_nodesConstrainedWidth, FLT_MAX)); } -- (void)dataControllerLockDataSource -{ - ASDisplayNodeAssert(!self.asyncDataSourceLocked, @"The data source has already been locked"); - - self.asyncDataSourceLocked = YES; - - if (_asyncDataSourceFlags.asyncDataSourceTableViewLockDataSource) { - [_asyncDataSource tableViewLockDataSource:self]; - } -} - -- (void)dataControllerUnlockDataSource -{ - ASDisplayNodeAssert(self.asyncDataSourceLocked, @"The data source has already been unlocked"); - - self.asyncDataSourceLocked = NO; - - if (_asyncDataSourceFlags.asyncDataSourceTableViewUnlockDataSource) { - [_asyncDataSource tableViewUnlockDataSource:self]; - } -} - - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section { return [_asyncDataSource tableView:self numberOfRowsInSection:section]; diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m index 21fe2b88..efce00ad 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -26,17 +26,6 @@ @implementation ASChangeSetDataController -- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled -{ - if (!(self = [super initWithAsyncDataFetching:asyncDataFetchingEnabled])) { - return nil; - } - - _changeSetBatchUpdateCounter = 0; - - return self; -} - #pragma mark - Batching (External API) - (void)beginUpdates @@ -66,6 +55,8 @@ [super deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; } + // TODO: Shouldn't reloads be processed before deletes, since deletes affect + // the index space and reloads don't? for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { [super reloadSections:change.indexSet withAnimationOptions:change.animationOptions]; } diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm index c29fef14..b54583c2 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.mm +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -32,9 +32,9 @@ NSMutableDictionary *> *_pendingContexts; } -- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled +- (instancetype)init { - self = [super initWithAsyncDataFetching:asyncDataFetchingEnabled]; + self = [super init]; if (self != nil) { _pendingContexts = [NSMutableDictionary dictionary]; } @@ -53,15 +53,13 @@ - (void)willReloadData { - NSArray *keys = _pendingContexts.allKeys; - for (NSString *kind in keys) { - NSMutableArray *contexts = _pendingContexts[kind]; + [_pendingContexts enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull kind, NSMutableArray * _Nonnull contexts, __unused BOOL * _Nonnull stop) { // Remove everything that existed before the reload, now that we're ready to insert replacements NSArray *indexPaths = [self indexPathsForEditingNodesOfKind:kind]; [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; NSArray *editingNodes = [self editingNodesOfKind:kind]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; + NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; [self deleteSectionsOfKind:kind atIndexSet:indexSet completion:nil]; // Insert each section @@ -75,8 +73,8 @@ [self batchLayoutNodesFromContexts:contexts ofKind:kind completion:^(NSArray *nodes, NSArray *indexPaths) { [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; }]; - [_pendingContexts removeObjectForKey:kind]; - } + }]; + [_pendingContexts removeAllObjects]; } - (void)prepareForInsertSections:(NSIndexSet *)sections diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 74ed8f0e..a740d4f1 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -57,17 +57,6 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ - (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController; -/** - Lock the data source for data fetching. - */ -- (void)dataControllerLockDataSource; - -/** - Unlock the data source after data fetching. - */ -- (void)dataControllerUnlockDataSource; - - @end @protocol ASDataControllerEnvironmentDelegate @@ -135,20 +124,6 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ @property (nonatomic, weak) id environmentDelegate; -/** - * Designated initializer. - * - * @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 - * will take the responsibility to avoid data inconsistency. Specifically, we will lock the data source through `dataControllerLockDataSource`, - * and unlock it by `dataControllerUnlockDataSource` after the data fetching. The application should not update the data source while - * the data source is locked. - */ -- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled; - /** @name Data Updating */ - (void)beginUpdates; diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 296f67b8..86967fc9 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -44,8 +44,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; 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 _initialReloadDataHasBeenCalled; BOOL _delegateDidInsertNodes; @@ -62,7 +60,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Lifecycle -- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled +- (instancetype)init { if (!(self = [super init])) { return nil; @@ -83,7 +81,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; _editingTransactionQueue.name = @"org.AsyncDisplayKit.ASDataController.editingTransactionQueue"; _batchUpdateCounter = 0; - _asyncDataFetchingEnabled = asyncDataFetchingEnabled; return self; } @@ -119,6 +116,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)batchLayoutNodesFromContexts:(NSArray *)contexts ofKind:(NSString *)kind completion:(ASDataControllerCompletionBlock)completionBlock { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd)); + NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; NSUInteger count = contexts.count; @@ -143,8 +142,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; */ - (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize { - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + CGSize size = [node measureWithSizeRange:constrainedSize].size; + node.frame = { .size = size }; } /** @@ -152,6 +151,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; */ - (void)_batchLayoutNodesFromContexts:(NSArray *)contexts withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd)); + [self batchLayoutNodesFromContexts:contexts ofKind:ASDataControllerRowNodeKind completion:^(NSArray *nodes, NSArray *indexPaths) { // Insert finished nodes into data storage [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -163,6 +164,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; */ - (void)_layoutNodes:(NSArray *)nodes fromContexts:(NSArray *)contexts atIndexesOfRange:(NSRange)range ofKind:(NSString *)kind { + ASDisplayNodeAssert([NSOperationQueue currentQueue] != _editingTransactionQueue, @"%@ should not be called on the editing transaction queue", NSStringFromSelector(_cmd)); + if (_dataSource == nil) { return; } @@ -182,6 +185,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)_layoutNodesFromContexts:(NSArray *)contexts ofKind:(NSString *)kind completion:(ASDataControllerCompletionBlock)completionBlock { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd)); + if (!contexts.count || _dataSource == nil) { return; } @@ -278,7 +283,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; NSMutableArray *editingNodes = _editingNodes[kind]; ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes); - _editingNodes[kind] = editingNodes; // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. NSMutableArray *completedNodes = ASTwoDimensionalArrayDeepMutableCopy(editingNodes); @@ -359,7 +363,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; */ - (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd)); + [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + ASDisplayNodeAssertMainThread(); + if (_delegateDidInsertNodes) [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }]; @@ -373,7 +381,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; */ - (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd)); + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + ASDisplayNodeAssertMainThread(); + if (_delegateDidDeleteNodes) [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }]; @@ -387,7 +399,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; */ - (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd)); + [self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) { + ASDisplayNodeAssertMainThread(); + if (_delegateDidInsertSections) [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; }]; @@ -401,7 +417,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; */ - (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"%@ must be called on the editing transaction queue", NSStringFromSelector(_cmd)); + [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSIndexSet *indexSet) { + ASDisplayNodeAssertMainThread(); + if (_delegateDidDeleteSections) [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }]; @@ -426,49 +446,44 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; ASDisplayNodeAssertMainThread(); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - [self accessDataSourceSynchronously:synchronously withBlock:^{ - NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self]; - NSIndexSet *sectionIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; - NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sectionIndexSet]; + NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self]; + NSIndexSet *sectionIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; + NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sectionIndexSet]; - // Allow subclasses to perform setup before going into the edit transaction - [self prepareForReloadData]; + // Allow subclasses to perform setup before going into the edit transaction + [self prepareForReloadData]; + + [_editingTransactionQueue addOperationWithBlock:^{ + LOG(@"Edit Transaction - reloadData"); - void (^transactionBlock)() = ^{ - LOG(@"Edit Transaction - reloadData"); - - // Remove everything that existed before the reload, now that we're ready to insert replacements - NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; - NSUInteger editingNodesSectionCount = editingNodes.count; - - if (editingNodesSectionCount) { - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodesSectionCount)]; - [self _deleteNodesAtIndexPaths:ASIndexPathsForTwoDimensionalArray(editingNodes) withAnimationOptions:animationOptions]; - [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - } - - [self willReloadData]; - - // Insert empty sections - NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; - for (int i = 0; i < sectionCount; i++) { - [sections addObject:[[NSMutableArray alloc] init]]; - } - [self _insertSections:sections atIndexSet:sectionIndexSet withAnimationOptions:animationOptions]; - - [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; - - if (completion) { - dispatch_async(dispatch_get_main_queue(), completion); - } - }; + // Remove everything that existed before the reload, now that we're ready to insert replacements + NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; + NSUInteger editingNodesSectionCount = editingNodes.count; - if (synchronously) { - transactionBlock(); - } else { - [_editingTransactionQueue addOperationWithBlock:transactionBlock]; + if (editingNodesSectionCount) { + NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodesSectionCount)]; + [self _deleteNodesAtIndexPaths:ASIndexPathsForTwoDimensionalArray(editingNodes) withAnimationOptions:animationOptions]; + [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + } + + [self willReloadData]; + + // Insert empty sections + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; + for (int i = 0; i < sectionCount; i++) { + [sections addObject:[[NSMutableArray alloc] init]]; + } + [self _insertSections:sections atIndexSet:sectionIndexSet withAnimationOptions:animationOptions]; + + [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; + + if (completion) { + dispatch_async(dispatch_get_main_queue(), completion); } }]; + if (synchronously) { + [self waitUntilAllUpdatesAreCommitted]; + } }]; } @@ -491,45 +506,21 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Data Source Access (Calling _dataSource) -/** - * Safely locks access to the data source and executes the given block, unlocking once complete. - * - * @discussion When `asyncDataFetching` is enabled, the block is executed on a background thread. - */ -- (void)accessDataSourceWithBlock:(dispatch_block_t)block -{ - [self accessDataSourceSynchronously:NO withBlock:block]; -} - -- (void)accessDataSourceSynchronously:(BOOL)synchronously withBlock:(dispatch_block_t)block -{ - if (!synchronously && _asyncDataFetchingEnabled) { - [_dataSource dataControllerLockDataSource]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - block(); - [_dataSource dataControllerUnlockDataSource]; - }); - } else { - [_dataSource dataControllerLockDataSource]; - block(); - [_dataSource dataControllerUnlockDataSource]; - } -} - /** * Fetches row contexts for the provided sections from the data source. */ - (NSArray *)_populateFromDataSourceWithSectionIndexSet:(NSIndexSet *)indexSet { + ASDisplayNodeAssertMainThread(); + id environment = [self.environmentDelegate dataControllerEnvironment]; ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection; NSMutableArray *contexts = [NSMutableArray array]; - [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:idx]; - NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + [indexSet enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL *stop) { + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:sectionIndex]; for (NSUInteger i = 0; i < rowNum; i++) { - NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sectionIndex]; ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; @@ -628,24 +619,22 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; LOG(@"Edit Command - insertSections: %@", sections); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - [self accessDataSourceWithBlock:^{ - NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sections]; + NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sections]; - [self prepareForInsertSections:sections]; + [self prepareForInsertSections:sections]; + + [_editingTransactionQueue addOperationWithBlock:^{ + [self willInsertSections:sections]; + + LOG(@"Edit Transaction - insertSections: %@", sections); + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { + [sectionArray addObject:[NSMutableArray array]]; + } + + [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; - [_editingTransactionQueue addOperationWithBlock:^{ - [self willInsertSections:sections]; - - LOG(@"Edit Transaction - insertSections: %@", sections); - NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; - for (NSUInteger i = 0; i < sections.count; i++) { - [sectionArray addObject:[NSMutableArray array]]; - } - - [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; - - [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }]; + [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; }]; }]; } @@ -678,23 +667,21 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - [self accessDataSourceWithBlock:^{ - NSArray *contexts= [self _populateFromDataSourceWithSectionIndexSet:sections]; + NSArray *contexts= [self _populateFromDataSourceWithSectionIndexSet:sections]; - [self prepareForReloadSections:sections]; + [self prepareForReloadSections:sections]; + + [_editingTransactionQueue addOperationWithBlock:^{ + [self willReloadSections:sections]; + + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - [_editingTransactionQueue addOperationWithBlock:^{ - [self willReloadSections:sections]; + LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForTwoDimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); + + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - - LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForTwoDimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); - - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - - // reinsert the elements - [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }]; + // reinsert the elements + [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; }]; }]; } @@ -818,27 +805,25 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [self accessDataSourceWithBlock:^{ - id environment = [self.environmentDelegate dataControllerEnvironment]; - ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection; - - for (NSIndexPath *indexPath in sortedIndexPaths) { - ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; - ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; - [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock - indexPath:indexPath - constrainedSize:constrainedSize - environmentTraitCollection:environmentTraitCollection]]; - } + id environment = [self.environmentDelegate dataControllerEnvironment]; + ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection; + + for (NSIndexPath *indexPath in sortedIndexPaths) { + ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; + [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock + indexPath:indexPath + constrainedSize:constrainedSize + environmentTraitCollection:environmentTraitCollection]]; + } - [self prepareForInsertRowsAtIndexPaths:indexPaths]; + [self prepareForInsertRowsAtIndexPaths:indexPaths]; - [_editingTransactionQueue addOperationWithBlock:^{ - [self willInsertRowsAtIndexPaths:indexPaths]; + [_editingTransactionQueue addOperationWithBlock:^{ + [self willInsertRowsAtIndexPaths:indexPaths]; - LOG(@"Edit Transaction - insertRows: %@", indexPaths); - [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }]; + LOG(@"Edit Transaction - insertRows: %@", indexPaths); + [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; }]; }]; } @@ -874,35 +859,32 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. - [self accessDataSourceWithBlock:^{ - NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - - // Sort indexPath to avoid messing up the index when deleting - // FIXME: Shouldn't deletes be sorted in descending order? - NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - - id environment = [self.environmentDelegate dataControllerEnvironment]; - ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection; - - for (NSIndexPath *indexPath in sortedIndexPaths) { - ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; - ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; - [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock - indexPath:indexPath - constrainedSize:constrainedSize - environmentTraitCollection:environmentTraitCollection]]; - } + NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + + // Sort indexPath to avoid messing up the index when deleting + // FIXME: Shouldn't deletes be sorted in descending order? + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + + id environment = [self.environmentDelegate dataControllerEnvironment]; + ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection; + + for (NSIndexPath *indexPath in sortedIndexPaths) { + ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; + [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock + indexPath:indexPath + constrainedSize:constrainedSize + environmentTraitCollection:environmentTraitCollection]]; + } - [self prepareForReloadRowsAtIndexPaths:indexPaths]; - - [_editingTransactionQueue addOperationWithBlock:^{ - [self willReloadRowsAtIndexPaths:indexPaths]; + [self prepareForReloadRowsAtIndexPaths:indexPaths]; + + [_editingTransactionQueue addOperationWithBlock:^{ + [self willReloadRowsAtIndexPaths:indexPaths]; - LOG(@"Edit Transaction - reloadRows: %@", indexPaths); - [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; - [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }]; + LOG(@"Edit Transaction - reloadRows: %@", indexPaths); + [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; + [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; }]; }]; } @@ -935,16 +917,18 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return; } - [self accessDataSourceWithBlock:^{ - [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { - [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; - ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; - ASLayout *layout = [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, layout.size.width, layout.size.height); - }]; - }]; - }]; + NSUInteger sectionIndex = 0; + for (NSMutableArray *section in nodes) { + NSUInteger rowIndex = 0; + for (ASCellNode *node in section) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + CGSize size = [node measureWithSizeRange:constrainedSize].size; + node.frame = { .size = size }; + rowIndex += 1; + } + sectionIndex += 1; + } } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions @@ -1021,17 +1005,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (NSIndexPath *)indexPathForNode:(ASCellNode *)cellNode; { ASDisplayNodeAssertMainThread(); - - NSArray *nodes = [self completedNodes]; - NSUInteger numberOfNodes = nodes.count; + NSInteger section = 0; // Loop through each section to look for the cellNode - for (NSUInteger i = 0; i < numberOfNodes; i++) { - NSArray *sectionNodes = nodes[i]; - NSUInteger cellIndex = [sectionNodes indexOfObjectIdenticalTo:cellNode]; - if (cellIndex != NSNotFound) { - return [NSIndexPath indexPathForRow:cellIndex inSection:i]; + for (NSArray *sectionNodes in [self completedNodes]) { + NSUInteger item = [sectionNodes indexOfObjectIdenticalTo:cellNode]; + if (item != NSNotFound) { + return [NSIndexPath indexPathForItem:item inSection:section]; } + section += 1; } return nil; diff --git a/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm b/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm index 5c7e4373..370f5339 100644 --- a/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm +++ b/AsyncDisplayKit/Private/ASMultidimensionalArrayUtils.mm @@ -11,6 +11,10 @@ #import "ASAssert.h" #import "ASMultidimensionalArrayUtils.h" +// Import UIKit to get [NSIndexPath indexPathForItem:inSection:] which uses +// static memory addresses rather than allocating new index path objects. +#import + #pragma mark - Internal Methods static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, @@ -25,8 +29,10 @@ static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray } if (curIndexPath.length < dimension - 1) { - for (int i = 0; i < mutableArray.count; i++) { - ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(mutableArray[i], indexPaths, curIdx, [curIndexPath indexPathByAddingIndex:i], dimension, updateBlock); + NSInteger i = 0; + for (NSMutableArray *subarray in mutableArray) { + ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(subarray, indexPaths, curIdx, [curIndexPath indexPathByAddingIndex:i], dimension, updateBlock); + i += 1; } } else { NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; @@ -72,7 +78,12 @@ static BOOL ASElementExistsAtIndexPathForMultidimensionalArray(NSArray *array, N NSUInteger indexesLength = indexLength - 1; NSUInteger indexes[indexesLength]; [indexPath getIndexes:indexes range:NSMakeRange(1, indexesLength)]; - NSIndexPath *indexPathByRemovingFirstIndex = [NSIndexPath indexPathWithIndexes:indexes length:indexesLength]; + NSIndexPath *indexPathByRemovingFirstIndex; + if (indexesLength == 2) { + indexPathByRemovingFirstIndex = [NSIndexPath indexPathForItem:indexes[1] inSection:indexes[0]]; + } else { + indexPathByRemovingFirstIndex = [NSIndexPath indexPathWithIndexes:indexes length:indexesLength]; + } return ASElementExistsAtIndexPathForMultidimensionalArray(array[firstIndex], indexPathByRemovingFirstIndex); } @@ -184,9 +195,8 @@ NSArray *ASIndexPathsForTwoDimensionalArray(NSArray * twoDimensionalA NSUInteger section = 0; for (NSArray *subarray in twoDimensionalArray) { ASDisplayNodeCAssert([subarray isKindOfClass:[NSArray class]], @"This function expects NSArray *"); - NSUInteger itemCount = subarray.count; - for (NSUInteger item = 0; item < itemCount; item++) { - [result addObject:[NSIndexPath indexPathWithIndexes:(const NSUInteger []){ section, item } length:2]]; + for (NSUInteger item = 0; item < subarray.count; item++) { + [result addObject:[NSIndexPath indexPathForItem:item inSection:section]]; } section++; } diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index 19a827ea..7cb13fb8 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -408,8 +408,7 @@ { CGSize tableViewSize = CGSizeMake(100, 500); ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) - style:UITableViewStylePlain - asyncDataFetching:YES]; + style:UITableViewStylePlain]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; tableView.asyncDelegate = dataSource;