diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 8a71f9356f..1516962121 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -51,7 +51,7 @@ Pod::Spec.new do |spec| spec.subspec 'PINRemoteImage' do |pin| pin.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) PIN_REMOTE_IMAGE=1' } - pin.dependency 'PINRemoteImage/iOS', '>= 2' + pin.dependency 'PINRemoteImage/iOS', '>= 2.1' pin.dependency 'AsyncDisplayKit/ASDealloc2MainObject' end diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index a8fd8fffd1..91db74aa94 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -498,6 +498,8 @@ DBDB83971C6E879900D0098C /* ASPagerFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */; }; DE040EF91C2B40AC004692FF /* ASCollectionViewFlowLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE0702FC1C3671E900D7DE62 /* libAsyncDisplayKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AC195D04C000B7D73C /* libAsyncDisplayKit.a */; }; + DE4843DB1C93EAB100A1F33B /* ASDisplayNodeLayoutContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */; }; + DE4843DC1C93EAC100A1F33B /* ASDisplayNodeLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */; }; DE6EA3221C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -514,6 +516,8 @@ DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6E91BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; + E52405B31C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */; }; + E52405B51C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */; }; E5711A2B1C840C81009619D4 /* ASIndexedNodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5711A2C1C840C81009619D4 /* ASIndexedNodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */; }; E5711A2E1C840C96009619D4 /* ASIndexedNodeContext.m in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASIndexedNodeContext.m */; }; @@ -845,6 +849,8 @@ DEC146B51C37A16A004A0EE7 /* ASCollectionInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASCollectionInternal.m; path = Details/ASCollectionInternal.m; sourceTree = ""; }; DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; + E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeLayoutContext.mm; sourceTree = ""; }; + E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeLayoutContext.h; sourceTree = ""; }; E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIndexedNodeContext.h; sourceTree = ""; }; E5711A2D1C840C96009619D4 /* ASIndexedNodeContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIndexedNodeContext.m; sourceTree = ""; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1192,6 +1198,8 @@ 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */, 058D0A0B195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm */, DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */, + E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */, + E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */, 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */, @@ -1453,6 +1461,7 @@ 055B9FA81A1C154B00035D6D /* ASNetworkImageNode.h in Headers */, ACF6ED2B1B17843500DA7C62 /* ASOverlayLayoutSpec.h in Headers */, 055F1A3819ABD413004DAFF1 /* ASRangeController.h in Headers */, + E52405B51C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h in Headers */, ACF6ED2D1B17843500DA7C62 /* ASRatioLayoutSpec.h in Headers */, AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */, 291B63FB1AA53A7A000A71B3 /* ASScrollDirection.h in Headers */, @@ -1605,6 +1614,7 @@ E5711A2C1C840C81009619D4 /* ASIndexedNodeContext.h in Headers */, 254C6B7B1BF94DF4003EC431 /* ASTextKitRenderer+Positioning.h in Headers */, CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, + DE4843DC1C93EAC100A1F33B /* ASDisplayNodeLayoutContext.h in Headers */, 254C6B761BF94DF4003EC431 /* ASTextNodeTypes.h in Headers */, 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */, 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */, @@ -1675,6 +1685,7 @@ 058D09B9195D04C000B7D73C /* Frameworks */, 058D09BA195D04C000B7D73C /* Resources */, 3B9D88CDF51B429C8409E4B6 /* Copy Pods Resources */, + B130AB1AC0A1E5162E211C19 /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -1804,6 +1815,21 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; + B130AB1AC0A1E5162E211C19 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1885,6 +1911,7 @@ ACF6ED2C1B17843500DA7C62 /* ASOverlayLayoutSpec.mm in Sources */, 0442850F1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.mm in Sources */, 257754921BED28F300737CA5 /* ASEqualityHashHelpers.mm in Sources */, + E52405B31C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm in Sources */, 257754AB1BEE44CD00737CA5 /* ASTextKitEntityAttribute.m in Sources */, 055F1A3919ABD413004DAFF1 /* ASRangeController.mm in Sources */, 044285091BAA63FE00D16268 /* ASBatchFetching.m in Sources */, @@ -1965,6 +1992,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE4843DB1C93EAB100A1F33B /* ASDisplayNodeLayoutContext.mm in Sources */, B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */, 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */, diff --git a/AsyncDisplayKit/ASCellNode+Internal.h b/AsyncDisplayKit/ASCellNode+Internal.h index 8dba99cded..5241456b9f 100644 --- a/AsyncDisplayKit/ASCellNode+Internal.h +++ b/AsyncDisplayKit/ASCellNode+Internal.h @@ -32,4 +32,9 @@ */ @property (nonatomic, weak) id layoutDelegate; +/* + * Back-pointer to the containing scrollView instance, set only for visible cells. Used for Cell Visibility Event callbacks. + */ +@property (nonatomic, weak) UIScrollView *scrollView; + @end diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 2d82a8bfab..85f9a08f0b 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -127,20 +127,38 @@ [self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize]; } -- (ASLayout *)transitionLayoutWithAnimation:(BOOL)animated +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion { CGSize oldSize = self.calculatedSize; - ASLayout *layout = [super transitionLayoutWithAnimation:animated]; - [self didRelayoutFromOldSize:oldSize toNewSize:layout.size]; - return layout; + [super transitionLayoutWithAnimation:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:^{ + [self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize]; + if (completion) { + completion(); + } + } + ]; } -- (ASLayout *)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion { CGSize oldSize = self.calculatedSize; - ASLayout *layout = [super transitionLayoutWithSizeRange:constrainedSize animated:animated]; - [self didRelayoutFromOldSize:oldSize toNewSize:layout.size]; - return layout; + [super transitionLayoutWithSizeRange:constrainedSize + animated:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:^{ + [self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize]; + if (completion) { + completion(); + } + } + ]; } - (void)didRelayoutFromOldSize:(CGSize)oldSize toNewSize:(CGSize)newSize @@ -153,6 +171,9 @@ } } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); @@ -181,11 +202,25 @@ [(_ASDisplayView *)self.view __forwardTouchesCancelled:touches withEvent:event]; } -- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event - inScrollView:(UIScrollView *)scrollView - withCellFrame:(CGRect)cellFrame +#pragma clang diagnostic pop + +- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame { - // To be overriden by subclasses + // To be overriden by subclasses +} + +- (void)visibilityDidChange:(BOOL)isVisible +{ + [super visibilityDidChange:isVisible]; + + CGRect cellFrame = CGRectZero; + if (_scrollView) { + // It is not safe to message nil with a structure return value, so ensure our _scrollView has not died. + cellFrame = [self.view convertRect:self.bounds toView:_scrollView]; + } + [self cellNodeVisibilityEvent:isVisible ? ASCellNodeVisibilityEventVisible : ASCellNodeVisibilityEventInvisible + inScrollView:_scrollView + withCellFrame:cellFrame]; } @end diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 099947457c..f8929f170d 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -15,6 +15,7 @@ #import "ASCollectionViewLayoutController.h" #import "ASCollectionViewFlowLayoutInspector.h" #import "ASCollectionViewLayoutFacilitatorProtocol.h" +#import "ASDisplayNodeExtras.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+Beta.h" #import "ASInternalHelpers.h" @@ -57,6 +58,36 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; @end +#pragma mark - +#pragma mark _ASCollectionViewNodeSizeUpdateContext + +/** + * This class contains all the nodes that have a new size and UICollectionView should requery them all at once. + * It is intended to be used strictly on main thread and is not thread safe. + */ +@interface _ASCollectionViewNodeSizeInvalidationContext : NSObject +/** + * It's possible that a node triggered multiple size changes before main thread has a chance to execute `requeryNodeSizes`. + * Therefore, a set is preferred here, to avoid asking ASDataController to search for index path of the same node multiple times. + */ +@property (nonatomic, strong) NSMutableSet *invalidatedNodes; +@property (nonatomic, assign) BOOL shouldAnimate; +@end + +@implementation _ASCollectionViewNodeSizeInvalidationContext + +- (instancetype)init +{ + self = [super init]; + if (self) { + _invalidatedNodes = [NSMutableSet set]; + _shouldAnimate = YES; + } + return self; +} + +@end + #pragma mark - #pragma mark ASCollectionView. @@ -78,7 +109,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; BOOL _asyncDelegateImplementsScrollviewDidScroll; BOOL _asyncDataSourceImplementsConstrainedSizeForNode; BOOL _asyncDataSourceImplementsNodeBlockForItemAtIndexPath; - BOOL _queuedNodeSizeUpdate; + _ASCollectionViewNodeSizeInvalidationContext *_queuedNodeSizeInvalidationContext; // Main thread only BOOL _isDeallocating; ASBatchContext *_batchContext; @@ -532,39 +563,35 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + ASCellNode *cellNode = [cell node]; + cellNode.scrollView = collectionView; if ([_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]) { [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; } - ASCellNode *cellNode = [cell node]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + if (cellNode.neverShowPlaceholders) { [cellNode recursivelyEnsureDisplaySynchronously:YES]; } if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { [_cellsForVisibilityUpdates addObject:cell]; - [cellNode cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisible - inScrollView:collectionView - withCellFrame:cell.frame]; } } -- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + + ASCellNode *cellNode = [cell node]; if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNode:forItemAtIndexPath:)]) { - ASCellNode *node = ((_ASCollectionViewCell *)cell).node; - ASDisplayNodeAssertNotNil(node, @"Expected node associated with removed cell not to be nil."); - [_asyncDelegate collectionView:self didEndDisplayingNode:node forItemAtIndexPath:indexPath]; + ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); + [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; } if ([_cellsForVisibilityUpdates containsObject:cell]) { - ASCellNode *node = ((_ASCollectionViewCell *)cell).node; - [node cellNodeVisibilityEvent:ASCellNodeVisibilityEventInvisible - inScrollView:collectionView - withCellFrame:cell.frame]; [_cellsForVisibilityUpdates removeObject:cell]; } @@ -574,6 +601,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; [_asyncDelegate collectionView:self didEndDisplayingNodeForItemAtIndexPath:indexPath]; } #pragma clang diagnostic pop + + cellNode.scrollView = nil; } #pragma mark - @@ -867,7 +896,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); - return [self indexPathsForVisibleItems]; + // 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); + return isZeroSized ? @[] : [self indexPathsForVisibleItems]; } - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController @@ -1017,23 +1049,61 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASDisplayNodeAssertMainThread(); - if (!sizeChanged || _queuedNodeSizeUpdate) { + if (!sizeChanged) { return; } - _queuedNodeSizeUpdate = YES; - [self performSelector:@selector(requeryNodeSizes) - withObject:nil - afterDelay:0 - inModes:@[ NSRunLoopCommonModes ]]; + BOOL queued = (_queuedNodeSizeInvalidationContext != nil); + if (!queued) { + _queuedNodeSizeInvalidationContext = [[_ASCollectionViewNodeSizeInvalidationContext alloc] init]; + + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf) { + [strongSelf requeryNodeSizes]; + } + }); + } + + [_queuedNodeSizeInvalidationContext.invalidatedNodes addObject:node]; + + // Check if this node or one of its subnodes can be animated. + // If the context is already non-animated, don't bother checking this node. + if (_queuedNodeSizeInvalidationContext.shouldAnimate) { + BOOL (^shouldNotAnimateBlock)(ASDisplayNode *) = ^BOOL(ASDisplayNode * _Nonnull node) { + return node.shouldAnimateSizeChanges == NO; + }; + if (ASDisplayNodeFindFirstNode(node, shouldNotAnimateBlock) != nil) { + // One single non-animated cell node causes the whole context to be non-animated + _queuedNodeSizeInvalidationContext.shouldAnimate = NO; + } + } } // Cause UICollectionView to requery for the new size of all nodes - (void)requeryNodeSizes { - _queuedNodeSizeUpdate = NO; + ASDisplayNodeAssertMainThread(); + NSSet *nodes = _queuedNodeSizeInvalidationContext.invalidatedNodes; + NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:nodes.count]; + for (ASCellNode *node in nodes) { + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (indexPath != nil) { + [indexPaths addObject:indexPath]; + } + } - [super performBatchUpdates:^{} completion:nil]; + if (indexPaths.count > 0) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; + + ASPerformBlockWithoutAnimation(!_queuedNodeSizeInvalidationContext.shouldAnimate, ^{ + // Perform an empty update transaction here to trigger UICollectionView to requery row sizes and layout its subviews again + [super performBatchUpdates:^{} completion:nil]; + }); + } + + _queuedNodeSizeInvalidationContext = nil; } #pragma mark - Memory Management diff --git a/AsyncDisplayKit/ASControlNode.h b/AsyncDisplayKit/ASControlNode.h index 886855f008..9f5c37e343 100644 --- a/AsyncDisplayKit/ASControlNode.h +++ b/AsyncDisplayKit/ASControlNode.h @@ -120,11 +120,6 @@ typedef NS_OPTIONS(NSUInteger, ASControlState) { */ - (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)event; -/** - Class method to enable a visualization overlay of the tapable area on the ASControlNode. For app debugging purposes only. - @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. - */ -+ (void)setEnableHitTestDebug:(BOOL)enable; @end NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASControlNode.mm b/AsyncDisplayKit/ASControlNode.mm index fcfb67ca6e..dde8a9e00f 100644 --- a/AsyncDisplayKit/ASControlNode.mm +++ b/AsyncDisplayKit/ASControlNode.mm @@ -89,7 +89,8 @@ static BOOL _enableHitTestDebug = NO; return self; } - +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" #pragma mark - ASDisplayNode Overrides - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event @@ -207,6 +208,8 @@ static BOOL _enableHitTestDebug = NO; withEvent:event]; } +#pragma clang diagnostic pop + - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { // If we're interested in touches, this is a tap (the only gesture we care about) and passed -hitTest for us, then no, you may not begin. Sir. diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index aa7df08583..1ea3d3376f 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -57,18 +57,38 @@ ASDISPLAYNODE_EXTERN_C_END - (void)didCompleteLayoutTransition:(id)context; /** - * @abstract Transitions the current layout with a new constrained size. + * @abstract Transitions the current layout with a new constrained size. Must be called on main thread. * - * @discussion Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. - * If the passed constrainedSize is the the same as the node's current constrained size, this method is noop. + * @param animated Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * + * @param shouldMeasureAsync Measure the layout asynchronously. + * + * @param measurementCompletion Optional completion block called only if a new layout is calculated. + * It is called on main, right after the measurement and before -animateLayoutTransition:. + * + * @discussion If the passed constrainedSize is the the same as the node's current constrained size, this method is noop. + * + * @see animateLayoutTransition: */ -- (ASLayout *)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated; +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion; /** - * @abstract Invalidates the current layout and begins a relayout of the node with the current `constrainedSize`. + * @abstract Invalidates the current layout and begins a relayout of the node with the current `constrainedSize`. Must be called on main thread. * - * @discussion Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * @param animated Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * + * @param shouldMeasureAsync Measure the layout asynchronously. + * + * @param measurementCompletion Optional completion block called only if a new layout is calculated. + * It is called right after the measurement and before -animateLayoutTransition:. + * + * @see animateLayoutTransition: */ -- (ASLayout *)transitionLayoutWithAnimation:(BOOL)animated; +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion; @end diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 60a0224f96..d7dad4cbc8 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -206,6 +206,8 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is * about to begin. + * + * @note Called on the main thread only */ - (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER; @@ -214,6 +216,8 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) has * completed. + * + * @note Called on the main thread only */ - (void)displayDidFinish ASDISPLAYNODE_REQUIRES_SUPER; @@ -225,9 +229,14 @@ NS_ASSUME_NONNULL_BEGIN * @discussion Subclasses may use this to monitor when they become visible, should free cached data, and much more. * @see ASInterfaceState */ -- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState; +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState ASDISPLAYNODE_REQUIRES_SUPER; -- (void)visibilityDidChange:(BOOL)isVisible; +/** + * @abstract Called whenever the visiblity of the node changed. + * + * @discussion Subclasses may use this to monitor when they become visible. + */ +- (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER; /** * Called just before the view is added to a window. @@ -340,7 +349,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** * @abstract Tells the node when touches moved in its view. @@ -348,7 +357,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** * @abstract Tells the node when touches ended in its view. @@ -356,7 +365,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** * @abstract Tells the node when touches was cancelled in its view. @@ -364,7 +373,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** @name Managing Gesture Recognizers */ diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 2f67dc170c..7a8d6ddffe 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -428,6 +428,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL displaySuspended; +/** + * @abstract Whether size changes should be animated. Default to YES. + */ +@property (nonatomic, assign) BOOL shouldAnimateSizeChanges; + /** * @abstract Prevent the node and its descendants' layer from displaying. * diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index ebedf8aef1..0974d48609 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -9,6 +9,7 @@ #import "ASDisplayNodeInternal.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" +#import "ASDisplayNode+Beta.h" #import "ASLayoutOptionsPrivate.h" #import @@ -20,10 +21,10 @@ #import "_ASDisplayView.h" #import "_ASScopeTimer.h" #import "_ASCoreAnimationExtras.h" +#import "ASDisplayNodeLayoutContext.h" #import "ASDisplayNodeExtras.h" #import "ASEqualityHelpers.h" #import "ASRunLoopQueue.h" -#import "NSArray+Diffing.h" #import "ASInternalHelpers.h" #import "ASLayout.h" @@ -34,7 +35,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification = @"ASRenderingEngineDidDisplayScheduledNodes"; NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp"; -@interface ASDisplayNode () +@interface ASDisplayNode () /** * @@ -113,6 +114,7 @@ static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *i flags.isInHierarchy = NO; flags.displaysAsynchronously = YES; + flags.shouldAnimateSizeChanges = YES; flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); if (instance) { @@ -219,7 +221,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) renderQueue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { CFAbsoluteTime timestamp = isQueueDrained ? CFAbsoluteTimeGetCurrent() : 0; - [dequeuedItem __recursivelyTriggerDisplayAndBlock:NO]; + [dequeuedItem _recursivelyTriggerDisplayAndBlock:NO]; if (isQueueDrained) { [[NSNotificationCenter defaultCenter] postNotificationName:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil @@ -350,9 +352,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [self __setSupernode:nil]; _pendingViewState = nil; - _replaceAsyncSentinel = nil; _displaySentinel = nil; + _transitionSentinel = nil; _pendingDisplayNodes = nil; } @@ -583,111 +585,162 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { - void (^manageSubnodesBlock)() = ^void() { - ASDN::MutexLocker l(_propertyLock); - if (self.usesImplicitHierarchyManagement) { - [self __implicitlyInsertSubnodes]; - [self __implicitlyRemoveSubnodes]; - } - [self __completeLayoutCalculation]; - }; + ASDN::MutexLocker l(_propertyLock); + if (! [self shouldMeasureWithSizeRange:constrainedSize]) { + return _layout; + } + + if ([self _hasTransitionsInProgress]) { + // Invalidate transition sentinel to cancel transitions in progress + [self _invalidateTransitionSentinel]; + // Tell subnodes to exit layout pending state and clear related properties + ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) { + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + } + + ASLayout *previousLayout = _layout; + ASSizeRange previousConstrainedSize = _constrainedSize; + ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize]; - return [self measureWithSizeRange:constrainedSize completion:^{ - if (!self.isNodeLoaded) { - manageSubnodesBlock(); - } else { - ASPerformBlockOnMainThread(manageSubnodesBlock); + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + _pendingLayoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self + pendingLayout:newLayout + pendingConstrainedSize:constrainedSize + previousLayout:previousLayout + previousConstrainedSize:previousConstrainedSize]; + } else { + ASDisplayNodeLayoutContext *layoutContext; + if (self.usesImplicitHierarchyManagement) { + layoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self + pendingLayout:newLayout + pendingConstrainedSize:constrainedSize + previousLayout:previousLayout + previousConstrainedSize:previousConstrainedSize]; } - }]; + [self applyLayout:newLayout constrainedSize:constrainedSize layoutContext:layoutContext]; + [self _completeLayoutCalculation]; + } + + return newLayout; } -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize completion:(void(^)())completion +- (BOOL)shouldMeasureWithSizeRange:(ASSizeRange)constrainedSize { ASDN::MutexLocker l(_propertyLock); - if (![self __shouldSize]) - return nil; + if (![self __shouldSize]) { + return NO; + } + + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) && constrainedSize.transitionID != _pendingTransitionID) { + return NO; + } // only calculate the size if // - we haven't already // - the constrained size range is different - if (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)) { - _previousLayout = _layout; - _layout = [self calculateLayoutThatFits:constrainedSize]; + return (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)); +} - ASDisplayNodeAssertTrue(_layout.layoutableObject == self); - ASDisplayNodeAssertTrue(_layout.size.width >= 0.0); - ASDisplayNodeAssertTrue(_layout.size.height >= 0.0); - - _previousConstrainedSize = _constrainedSize; - _constrainedSize = constrainedSize; - - if (self.usesImplicitHierarchyManagement) { - [self __calculateSubnodeOperations]; - } - _flags.isMeasured = YES; +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASSizeRange currentConstrainedSize = _constrainedSize; + [self invalidateCalculatedLayout]; + [self transitionLayoutWithSizeRange:currentConstrainedSize + animated:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:completion]; +} - completion(); +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + if (! [self shouldMeasureWithSizeRange:constrainedSize]) { + return; + } + + { + ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one."); } - return _layout; -} + int32_t transitionID = [self _newTransitionID]; + constrainedSize.transitionID = transitionID; -- (ASLayout *)transitionLayoutWithAnimation:(BOOL)animated -{ - [self invalidateCalculatedLayout]; - return [self transitionLayoutWithSizeRange:_constrainedSize animated:animated]; -} - -- (ASLayout *)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated -{ - BOOL disableImplicitHierarchyManagement = self.usesImplicitHierarchyManagement == NO; - self.usesImplicitHierarchyManagement = YES; // Temporary flag for 1.9.x + ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) { + ASDisplayNodeAssert([node _hasTransitionsInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one."); + node.hierarchyState |= ASHierarchyStateLayoutPending; + node.pendingTransitionID = transitionID; + }); - return [self measureWithSizeRange:constrainedSize completion:^{ - if (disableImplicitHierarchyManagement) { - self.usesImplicitHierarchyManagement = NO; // Temporary flag for 1.9.x - } - - ASPerformBlockOnMainThread(^{ + void (^transitionBlock)() = ^{ + ASLayout *newLayout; + { ASDN::MutexLocker l(_propertyLock); - _transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated delegate:self]; - [self __implicitlyInsertSubnodes]; + BOOL disableImplicitHierarchyManagement = self.usesImplicitHierarchyManagement == NO; + self.usesImplicitHierarchyManagement = YES; // Temporary flag for 1.9.x + newLayout = [self calculateLayoutThatFits:constrainedSize]; + if (disableImplicitHierarchyManagement) { + self.usesImplicitHierarchyManagement = NO; // Temporary flag for 1.9.x + } + } + + if ([self _shouldAbortTransitionWithID:transitionID]) { + return; + } + + ASPerformBlockOnMainThread(^{ + if ([self _shouldAbortTransitionWithID:transitionID]) { + return; + } + + ASDN::MutexLocker l(_propertyLock); + + ASLayout *previousLayout = _layout; + ASSizeRange previousConstrainedSize = _constrainedSize; + [self applyLayout:newLayout constrainedSize:constrainedSize layoutContext:nil]; + + [self _invalidateTransitionSentinel]; + + ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) { + [node applyPendingLayoutContext]; + [node _completeLayoutCalculation]; + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + + if (completion) { + completion(); + } + + _pendingLayoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self + pendingLayout:newLayout + pendingConstrainedSize:constrainedSize + previousLayout:previousLayout + previousConstrainedSize:previousConstrainedSize]; + [_pendingLayoutContext applySubnodeInsertions]; + + _transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated + layoutDelegate:_pendingLayoutContext + completionDelegate:self]; [self animateLayoutTransition:_transitionContext]; }); - }]; -} + }; -- (void)__calculateSubnodeOperations -{ - ASDN::MutexLocker l(_propertyLock); - if (_previousLayout) { - NSIndexSet *insertions, *deletions; - [_previousLayout.immediateSublayouts asdk_diffWithArray:_layout.immediateSublayouts - insertions:&insertions - deletions:&deletions - compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) { - return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject); - }]; - filterNodesInLayoutAtIndexes(_layout, insertions, &_insertedSubnodes, &_insertedSubnodePositions); - filterNodesInLayoutAtIndexesWithIntersectingNodes(_previousLayout, - deletions, - _insertedSubnodes, - &_removedSubnodes, - &_removedSubnodePositions); + if (shouldMeasureAsync) { + ASPerformBlockOnBackgroundThread(transitionBlock); } else { - NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [_layout.immediateSublayouts count])]; - filterNodesInLayoutAtIndexes(_layout, indexes, &_insertedSubnodes, &_insertedSubnodePositions); - _removedSubnodes = nil; + transitionBlock(); } } -- (void)__completeLayoutCalculation +- (void)_completeLayoutCalculation { ASDN::MutexLocker l(_propertyLock); - _insertedSubnodes = nil; - _removedSubnodes = nil; - _previousLayout = nil; - [self calculatedLayoutDidChange]; // we generate placeholders at measureWithSizeRange: time so that a node is guaranteed @@ -704,53 +757,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } } -/** - * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. - */ -static inline void filterNodesInLayoutAtIndexes( - ASLayout *layout, - NSIndexSet *indexes, - NSArray * __strong *storedNodes, - std::vector *storedPositions - ) -{ - filterNodesInLayoutAtIndexesWithIntersectingNodes(layout, indexes, nil, storedNodes, storedPositions); -} - -/** - * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. - * @discussion If the node exists in the `intersectingNodes` array, the node is not added to `storedNodes`. - */ -static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes( - ASLayout *layout, - NSIndexSet *indexes, - NSArray *intersectingNodes, - NSArray * __strong *storedNodes, - std::vector *storedPositions - ) -{ - NSMutableArray *nodes = [NSMutableArray array]; - std::vector positions = std::vector(); - NSInteger idx = [indexes firstIndex]; - while (idx != NSNotFound) { - BOOL skip = NO; - ASDisplayNode *node = (ASDisplayNode *)layout.immediateSublayouts[idx].layoutableObject; - ASDisplayNodeCAssert(node, @"A flattened layout must consist exclusively of node sublayouts"); - for (ASDisplayNode *i in intersectingNodes) { - if (node == i) { - skip = YES; - break; - } - } - if (!skip) { - [nodes addObject:node]; - positions.push_back(idx); - } - idx = [indexes indexGreaterThanIndex:idx]; - } - *storedNodes = nodes; - *storedPositions = positions; -} - (void)calculatedLayoutDidChange { @@ -779,66 +785,12 @@ static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes( - (void)didCompleteLayoutTransition:(id)context { - [self __implicitlyRemoveSubnodes]; - [self __completeLayoutCalculation]; + [_pendingLayoutContext applySubnodeRemovals]; + [self _completeLayoutCalculation]; + _pendingLayoutContext = nil; } -#pragma mark - Implicit node hierarchy managagment - -- (void)__implicitlyInsertSubnodes -{ - ASDN::MutexLocker l(_propertyLock); - for (NSInteger i = 0; i < [_insertedSubnodes count]; i++) { - NSInteger p = _insertedSubnodePositions[i]; - [self insertSubnode:_insertedSubnodes[i] atIndex:p]; - } -} - -- (void)__implicitlyRemoveSubnodes -{ - ASDN::MutexLocker l(_propertyLock); - for (NSInteger i = 0; i < [_removedSubnodes count]; i++) { - [_removedSubnodes[i] removeFromSupernode]; - } -} - -#pragma mark - _ASTransitionContextDelegate - -- (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context -{ - return _subnodes; -} - -- (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context -{ - return _insertedSubnodes; -} - -- (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context -{ - return _removedSubnodes; -} - -- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key -{ - if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { - return _previousLayout; - } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { - return _layout; - } else { - return nil; - } -} -- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key -{ - if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { - return _previousConstrainedSize; - } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { - return _constrainedSize; - } else { - return ASSizeRangeMake(CGSizeZero, CGSizeZero); - } -} +#pragma mark - _ASTransitionContextCompletionDelegate - (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete { @@ -1706,7 +1658,7 @@ static NSInteger incrementIfFound(NSInteger i) { // The node sending the message should usually be passed as the parameter, similar to the delegation pattern. - (void)_pendingNodeWillDisplay:(ASDisplayNode *)node { - ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssertMainThread(); if (!_pendingDisplayNodes) { _pendingDisplayNodes = [[NSMutableSet alloc] init]; @@ -1719,27 +1671,25 @@ static NSInteger incrementIfFound(NSInteger i) { // The node sending the message should usually be passed as the parameter, similar to the delegation pattern. - (void)_pendingNodeDidDisplay:(ASDisplayNode *)node { - ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssertMainThread(); [_pendingDisplayNodes removeObject:node]; // only trampoline if there is a placeholder and nodes are done displaying if ([self _pendingDisplayNodesHaveFinished] && _placeholderLayer.superlayer) { - dispatch_async(dispatch_get_main_queue(), ^{ - void (^cleanupBlock)() = ^{ - [self _tearDownPlaceholderLayer]; - }; + void (^cleanupBlock)() = ^{ + [self _tearDownPlaceholderLayer]; + }; - if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { - [CATransaction begin]; - [CATransaction setCompletionBlock:cleanupBlock]; - [CATransaction setAnimationDuration:_placeholderFadeDuration]; - _placeholderLayer.opacity = 0.0; - [CATransaction commit]; - } else { - cleanupBlock(); - } - }); + if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { + [CATransaction begin]; + [CATransaction setCompletionBlock:cleanupBlock]; + [CATransaction setAnimationDuration:_placeholderFadeDuration]; + _placeholderLayer.opacity = 0.0; + [CATransaction commit]; + } else { + cleanupBlock(); + } } } @@ -1808,7 +1758,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } } -- (void)__recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock +- (void)_recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock { ASDisplayNodeAssertMainThread(); @@ -1824,7 +1774,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously { - [self __recursivelyTriggerDisplayAndBlock:synchronously]; + [self _recursivelyTriggerDisplayAndBlock:synchronously]; } - (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay @@ -1896,6 +1846,13 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) return _constrainedSize; } +- (void)setPendingTransitionID:(int32_t)pendingTransitionID +{ + ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssertTrue(_pendingTransitionID < pendingTransitionID); + _pendingTransitionID = pendingTransitionID; +} + - (void)setPreferredFrameSize:(CGSize)preferredFrameSize { ASDN::MutexLocker l(_propertyLock); @@ -2026,6 +1983,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)visibilityDidChange:(BOOL)isVisible { + // subclass override } /** @@ -2045,6 +2003,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)setInterfaceState:(ASInterfaceState)newState { + // It should never be possible for a node to be visible but not be allowed / expected to display. + ASDisplayNodeAssertFalse(ASInterfaceStateIncludesVisible(newState) && !ASInterfaceStateIncludesDisplay(newState)); ASInterfaceState oldState = ASInterfaceStateNone; { ASDN::MutexLocker l(_propertyLock); @@ -2211,6 +2171,19 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } } + if ((newState & ASHierarchyStateLayoutPending) != (oldState & ASHierarchyStateLayoutPending)) { + if (newState & ASHierarchyStateLayoutPending) { + // Entering layout pending state + } else { + // Leaving layout pending state, reset related properties + { + ASDN::MutexLocker l(_propertyLock); + _pendingTransitionID = 0; + _pendingLayoutContext = nil; + } + } + } + if (newState != oldState) { LOG(@"setHierarchyState: oldState = %lu, newState = %lu", (unsigned long)oldState, (unsigned long)newState); } @@ -2236,6 +2209,37 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) }); } +- (void)applyPendingLayoutContext +{ + ASDN::MutexLocker l(_propertyLock); + if (_pendingLayoutContext) { + [self applyLayout:_pendingLayoutContext.pendingLayout + constrainedSize:_pendingLayoutContext.pendingConstrainedSize + layoutContext:_pendingLayoutContext]; + _pendingLayoutContext = nil; + } +} + +- (void)applyLayout:(ASLayout *)layout + constrainedSize:(ASSizeRange)constrainedSize + layoutContext:(ASDisplayNodeLayoutContext *)layoutContext +{ + ASDN::MutexLocker l(_propertyLock); + _layout = layout; + + ASDisplayNodeAssertTrue(layout.layoutableObject == self); + ASDisplayNodeAssertTrue(layout.size.width >= 0.0); + ASDisplayNodeAssertTrue(layout.size.height >= 0.0); + + _constrainedSize = constrainedSize; + _flags.isMeasured = YES; + + if (self.usesImplicitHierarchyManagement && layoutContext != nil) { + [layoutContext applySubnodeInsertions]; + [layoutContext applySubnodeRemovals]; + } +} + - (void)layout { ASDisplayNodeAssertMainThread(); @@ -2256,6 +2260,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)displayWillStart { + ASDisplayNodeAssertMainThread(); + // in case current node takes longer to display than it's subnodes, treat it as a dependent node [self _pendingNodeWillDisplay:self]; @@ -2284,6 +2290,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)displayDidFinish { + ASDisplayNodeAssertMainThread(); + [self _pendingNodeDidDisplay:self]; [_supernode subnodeDisplayDidFinish:self]; @@ -2490,14 +2498,31 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, self.asyncLayer.displaySuspended = flag; if ([self __implementsDisplay]) { - if (flag) { - [_supernode subnodeDisplayDidFinish:self]; - } else { - [_supernode subnodeDisplayWillStart:self]; - } + // Display start and finish methods needs to happen on the main thread + ASPerformBlockOnMainThread(^{ + if (flag) { + [_supernode subnodeDisplayDidFinish:self]; + } else { + [_supernode subnodeDisplayWillStart:self]; + } + }); } } +- (BOOL)shouldAnimateSizeChanges +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return _flags.shouldAnimateSizeChanges; +} + +-(void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; +} + static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; - (void)setDrawingPriority:(NSInteger)drawingPriority @@ -2552,24 +2577,31 @@ static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; return asyncSizingQueue; } -- (BOOL)_isMarkedForReplacement +- (BOOL)_hasTransitionsInProgress { ASDN::MutexLocker l(_propertyLock); - - return _replaceAsyncSentinel != nil; + return _transitionSentinel != nil; } -// FIXME: This method doesn't appear to be called, and could be removed. -// However, it may be useful for an API similar to what Paper used to create a new node hierarchy, -// trigger asynchronous measurement and display on it, and have it swap out and replace an old hierarchy. -- (ASSentinel *)_asyncReplaceSentinel +- (void)_invalidateTransitionSentinel { ASDN::MutexLocker l(_propertyLock); + _transitionSentinel = nil; +} - if (!_replaceAsyncSentinel) { - _replaceAsyncSentinel = [[ASSentinel alloc] init]; +- (BOOL)_shouldAbortTransitionWithID:(int32_t)transitionID +{ + ASDN::MutexLocker l(_propertyLock); + return _transitionSentinel == nil || transitionID != _transitionSentinel.value; +} + +- (int32_t)_newTransitionID +{ + ASDN::MutexLocker l(_propertyLock); + if (!_transitionSentinel) { + _transitionSentinel = [[ASSentinel alloc] init]; } - return _replaceAsyncSentinel; + return [_transitionSentinel increment]; } // Calls completion with nil to indicated cancellation diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 319b5ff62d..6b5bf3f84d 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -91,12 +91,12 @@ extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^b /** Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block. */ -extern id _Nullable ASDisplayNodeFind(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)); +extern id _Nullable ASDisplayNodeFindFirstSupernode(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)); /** Given a display node, traverses up the layer tree hierarchy, returning the first display node of kind class. */ -extern id _Nullable ASDisplayNodeFindClass(ASDisplayNode *start, Class c); +extern id _Nullable ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c); /** * Given two nodes, finds their most immediate common parent. Used for geometry conversion methods. @@ -124,7 +124,12 @@ extern NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *sta extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c); /** - Given a display node, traverses down the node hierarchy, returning the depth-first display node that pass the block. + Given a display node, traverses down the node hierarchy, returning the depth-first display node, including the start node that pass the block. + */ +extern __kindof ASDisplayNode * ASDisplayNodeFindFirstNode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node, excluding the start node, that pass the block */ extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index 67e9185d4e..75b9ddd575 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -53,7 +53,7 @@ extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^b } } -id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +id ASDisplayNodeFindFirstSupernode(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) { CALayer *layer = node.layer; @@ -68,9 +68,9 @@ id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) return nil; } -id ASDisplayNodeFindClass(ASDisplayNode *start, Class c) +id ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) { - return ASDisplayNodeFind(start, ^(ASDisplayNode *n) { + return ASDisplayNodeFindFirstSupernode(start, ^(ASDisplayNode *n) { return [n isKindOfClass:c]; }); } @@ -128,10 +128,10 @@ extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNo #pragma mark - Find first subnode -static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) +static ASDisplayNode *_ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) { for (ASDisplayNode *subnode in startNode.subnodes) { - ASDisplayNode *foundNode = _ASDisplayNodeFindFirstSubnode(subnode, YES, block); + ASDisplayNode *foundNode = _ASDisplayNodeFindFirstNode(subnode, YES, block); if (foundNode) { return foundNode; } @@ -143,9 +143,14 @@ static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, B return nil; } +extern __kindof ASDisplayNode * ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstNode(startNode, YES, block); +} + extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) { - return _ASDisplayNodeFindFirstSubnode(startNode, NO, block); + return _ASDisplayNodeFindFirstNode(startNode, NO, block); } extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) diff --git a/AsyncDisplayKit/ASImageNode.h b/AsyncDisplayKit/ASImageNode.h index c82391ea00..4253043ca4 100644 --- a/AsyncDisplayKit/ASImageNode.h +++ b/AsyncDisplayKit/ASImageNode.h @@ -96,7 +96,7 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image); * @discussion Can be used to add image effects (such as rounding, adding * borders, or other pattern overlays) without extraneous display calls. */ -@property (nonatomic, readwrite, copy) asimagenode_modification_block_t imageModificationBlock; +@property (nullable, nonatomic, readwrite, copy) asimagenode_modification_block_t imageModificationBlock; /** * @abstract Marks the receiver as needing display and performs a block after diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index fb62a7f124..5b5f97e793 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -49,6 +49,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL _cacheSupportsNewProtocol; BOOL _cacheSupportsClearing; + BOOL _cacheSupportsSynchronousFetch; } @end @@ -73,6 +74,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _cacheSupportsNewProtocol = [cache respondsToSelector:@selector(cachedImageWithURL:callbackQueue:completion:)]; _cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)]; + _cacheSupportsSynchronousFetch = [cache respondsToSelector:@selector(synchronouslyFetchedCachedImageWithURL:)]; _shouldCacheImage = YES; self.shouldBypassEnsureDisplay = YES; @@ -169,6 +171,17 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - (void)displayWillStart { [super displayWillStart]; + + if (_cacheSupportsSynchronousFetch) { + ASDN::MutexLocker l(_lock); + if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { + UIImage *result = [_cache synchronouslyFetchedCachedImageWithURL:_URL]; + if (result) { + self.image = result; + _imageLoaded = YES; + } + } + } [self fetchData]; @@ -184,6 +197,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; in ASMultiplexImageNode as well. */ - (void)visibilityDidChange:(BOOL)isVisible { + [super visibilityDidChange:isVisible]; + if (_downloaderImplementsSetPriority) { ASDN::MutexLocker l(_lock); if (_downloadIdentifier != nil) { diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 1cad0832db..695b1e8d7e 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -615,24 +615,23 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { _pendingVisibleIndexPath = indexPath; - - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + + ASCellNode *cellNode = [cell node]; + cellNode.scrollView = tableView; if ([_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNodeForRowAtIndexPath:)]) { [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; } + + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; - ASCellNode *cellNode = [cell node]; - - if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { - [_cellsForVisibilityUpdates addObject:cell]; - [cellNode cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisible - inScrollView:tableView - withCellFrame:cell.frame]; - } if (cellNode.neverShowPlaceholders) { [cellNode recursivelyEnsureDisplaySynchronously:YES]; } + + if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { + [_cellsForVisibilityUpdates addObject:cell]; + } } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath @@ -640,21 +639,18 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; if ([_pendingVisibleIndexPath isEqual:indexPath]) { _pendingVisibleIndexPath = nil; } + + ASCellNode *cellNode = [cell node]; [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]) { - ASCellNode *node = ((_ASTableViewCell *)cell).node; - ASDisplayNodeAssertNotNil(node, @"Expected node associated with removed cell not to be nil."); - [_asyncDelegate tableView:self didEndDisplayingNode:node forRowAtIndexPath:indexPath]; + ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); + [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; } if ([_cellsForVisibilityUpdates containsObject:cell]) { [_cellsForVisibilityUpdates removeObject:cell]; - ASCellNode *node = ((_ASTableViewCell *)cell).node; - [node cellNodeVisibilityEvent:ASCellNodeVisibilityEventInvisible - inScrollView:tableView - withCellFrame:cell.frame]; } #pragma clang diagnostic push @@ -663,6 +659,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_asyncDelegate tableView:self didEndDisplayingNodeForRowAtIndexPath:indexPath]; } #pragma clang diagnostic pop + + cellNode.scrollView = nil; } @@ -718,6 +716,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { ASDisplayNodeAssertMainThread(); + // Calling indexPathsForVisibleRows 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. + if (CGRectEqualToRect(self.bounds, CGRectZero)) { + return @[]; + } + NSArray *visibleIndexPaths = self.indexPathsForVisibleRows; if (_pendingVisibleIndexPath) { diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 92212d250a..15438fa3c8 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -60,6 +60,8 @@ - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { + [super interfaceStateDidChange:newState fromState:oldState]; + if (!(newState & ASInterfaceStateVisible)) { if (oldState & ASInterfaceStateVisible) { if (_shouldBePlaying) { @@ -231,6 +233,8 @@ - (void)visibilityDidChange:(BOOL)isVisible { + [super visibilityDidChange:isVisible]; + ASDN::MutexLocker l(_videoLock); if (_shouldAutoplay && _playerNode.isNodeLoaded) { diff --git a/AsyncDisplayKit/AsyncDisplayKit+Debug.h b/AsyncDisplayKit/AsyncDisplayKit+Debug.h index c9be26f11a..4192d67398 100644 --- a/AsyncDisplayKit/AsyncDisplayKit+Debug.h +++ b/AsyncDisplayKit/AsyncDisplayKit+Debug.h @@ -6,9 +6,19 @@ // Copyright © 2016 Facebook. All rights reserved. // -#import +#import "ASControlNode.h" #import "ASImageNode.h" +@interface ASControlNode (Debugging) + +/** + Class method to enable a visualization overlay of the tapable area on the ASControlNode. For app debugging purposes only. + @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. + */ ++ (void)setEnableHitTestDebug:(BOOL)enable; + +@end + @interface ASImageNode (Debugging) /** @@ -20,4 +30,4 @@ + (void)setShouldShowImageScalingOverlay:(BOOL)show; + (BOOL)shouldShowImageScalingOverlay; -@end \ No newline at end of file +@end diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index fc345706c3..8fbeec4771 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -74,5 +74,6 @@ #import #import #import +#import #import diff --git a/AsyncDisplayKit/Details/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h index ddbccb0eb1..25ee762862 100644 --- a/AsyncDisplayKit/Details/ASImageProtocols.h +++ b/AsyncDisplayKit/Details/ASImageProtocols.h @@ -17,6 +17,19 @@ typedef void(^ASImageCacherCompletion)(UIImage * _Nullable imageFromCache); @optional +/** + @abstract Attempts to fetch an image with the given URL from a memory cache. + @param URL The URL of the image to retrieve from the cache. + @discussion This method exists to support synchronous rendering of nodes. Before the layer is drawn, this method + is called to attempt to get the image out of the cache synchronously. This allows drawing to occur on the main thread + if displaysAsynchronously is set to NO or recursivelyEnsureDisplaySynchronously: has been called. + + If `URL` is nil, `completion` will be invoked immediately with a nil image. This method *should* block + the calling thread to fetch the image from a fast memory cache. It is OK to return nil from this method and instead + support only cachedImageWithURL:callbackQueue:completion: however, synchronous rendering will not be possible. + */ +- (nullable UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL; + /** @abstract Attempts to fetch an image with the given URL from the cache. @param URL The URL of the image to retrieve from the cache. diff --git a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m index dc15fb98ec..13f304f76e 100644 --- a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m +++ b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m @@ -29,6 +29,13 @@ #pragma mark ASImageProtocols +- (UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL +{ + NSString *key = [[PINRemoteImageManager sharedImageManager] cacheKeyForURL:URL processorKey:nil]; + PINRemoteImageManagerResult *result = [[PINRemoteImageManager sharedImageManager] synchronousImageFromCacheWithCacheKey:key options:PINRemoteImageManagerDownloadOptionsSkipDecode]; + return result.image; +} + - (void)fetchCachedImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue completion:(void (^)(CGImageRef imageFromCache))completion diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index c466d28f68..d89d117af4 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -90,6 +90,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; { _scrollDirection = scrollDirection; + // Perform update immediately, so that cells receive a visibilityDidChange: call before their first pixel is visible. [self scheduleRangeUpdate]; } @@ -149,6 +150,10 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; return; } + // allNodes is a 2D array: it contains arrays for each section, each containing nodes. + NSArray *allNodes = [_dataSource completedNodes]; + NSUInteger numberOfSections = [allNodes count]; + // TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges // Example: ... = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; @@ -165,10 +170,6 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [_layoutController setVisibleNodeIndexPaths:visibleNodePaths]; } - // allNodes is a 2D array: it contains arrays for each section, each containing nodes. - NSArray *allNodes = [_dataSource completedNodes]; - NSUInteger numberOfSections = [allNodes count]; - NSArray *currentSectionNodes = nil; NSInteger currentSectionIndex = -1; // Set to -1 so we don't match any indexPath.section on the first iteration. NSUInteger numberOfNodesInSection = 0; @@ -237,6 +238,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [self registerForNotificationsForInterfaceStateIfNeeded:selfInterfaceState]; #if ASRangeControllerLoggingEnabled + ASDisplayNodeAssertTrue([visibleIndexPaths isSubsetOfSet:displayIndexPaths]); NSMutableArray *modifiedIndexPaths = (ASRangeControllerLoggingEnabled ? [NSMutableArray array] : nil); #endif @@ -246,14 +248,15 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout; if (ASInterfaceStateIncludesVisible(selfInterfaceState)) { - if ([fetchDataIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateFetchData; - } - if ([displayIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateDisplay; - } if ([visibleIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateVisible; + interfaceState |= (ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData); + } else { + if ([fetchDataIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateFetchData; + } + if ([displayIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateDisplay; + } } } else { // If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the @@ -490,19 +493,22 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible + (void)didReceiveMemoryWarning:(NSNotification *)notification { -#if ASRangeControllerLoggingEnabled - NSLog(@"+[ASRangeController didReceiveMemoryWarning] with controllers: %@", [self allRangeControllersWeakSet]); -#endif - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; + for (ASRangeController *rangeController in allRangeControllers) { BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]); [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeMinimum : __rangeModeForMemoryWarnings]; [rangeController performRangeUpdate]; } + +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController didReceiveMemoryWarning] with controllers: %@", allRangeControllers); +#endif } + (void)didEnterBackground:(NSNotification *)notification { - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; + for (ASRangeController *rangeController in allRangeControllers) { // We do not want to fully collapse the Display ranges of any visible range controllers so that flashes can be avoided when // the app is resumed. Non-visible controllers can be more aggressively culled to the LowMemory state (see definitions for documentation) BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); @@ -511,27 +517,28 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible // Because -interfaceState checks __ApplicationState and always clears the "visible" bit if Backgrounded, we must set this after updating the range mode. __ApplicationState = UIApplicationStateBackground; - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + for (ASRangeController *rangeController in allRangeControllers) { // Trigger a range update immediately, as we may not be allowed by the system to run the update block scheduled by changing range mode. [rangeController performRangeUpdate]; } #if ASRangeControllerLoggingEnabled - NSLog(@"+[ASRangeController didEnterBackground] with controllers, after backgrounding: %@", [self allRangeControllersWeakSet]); + NSLog(@"+[ASRangeController didEnterBackground] with controllers, after backgrounding: %@", allRangeControllers); #endif } + (void)willEnterForeground:(NSNotification *)notification { + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; __ApplicationState = UIApplicationStateActive; - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + for (ASRangeController *rangeController in allRangeControllers) { BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly]; [rangeController performRangeUpdate]; } #if ASRangeControllerLoggingEnabled - NSLog(@"+[ASRangeController willEnterForeground] with controllers, after foregrounding: %@", [self allRangeControllersWeakSet]); + NSLog(@"+[ASRangeController willEnterForeground] with controllers, after foregrounding: %@", allRangeControllers); #endif } diff --git a/AsyncDisplayKit/Layout/ASDimension.h b/AsyncDisplayKit/Layout/ASDimension.h index c96b2155ae..c8b0093830 100644 --- a/AsyncDisplayKit/Layout/ASDimension.h +++ b/AsyncDisplayKit/Layout/ASDimension.h @@ -30,6 +30,7 @@ typedef struct { typedef struct { CGSize min; CGSize max; + int32_t transitionID; } ASSizeRange; extern ASRelativeDimension const ASRelativeDimensionUnconstrained; @@ -58,6 +59,9 @@ extern CGFloat ASRelativeDimensionResolve(ASRelativeDimension dimension, CGFloat extern ASSizeRange ASSizeRangeMake(CGSize min, CGSize max); +/** Creates an ASSizeRange with the provided size as both min and max */ +extern ASSizeRange ASSizeRangeMakeExactSize(CGSize size); + /** Clamps the provided CGSize between the [min, max] bounds of this ASSizeRange. */ extern CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size); diff --git a/AsyncDisplayKit/Layout/ASDimension.mm b/AsyncDisplayKit/Layout/ASDimension.mm index a1e42c4b76..7715e3b07a 100644 --- a/AsyncDisplayKit/Layout/ASDimension.mm +++ b/AsyncDisplayKit/Layout/ASDimension.mm @@ -77,6 +77,11 @@ ASSizeRange ASSizeRangeMake(CGSize min, CGSize max) ASSizeRange sizeRange; sizeRange.min = min; sizeRange.max = max; return sizeRange; } +ASSizeRange ASSizeRangeMakeExactSize(CGSize size) +{ + return ASSizeRangeMake(size, size); +} + CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size) { return CGSizeMake(MAX(sizeRange.min.width, MIN(sizeRange.max.width, size.width)), diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index aa0982b77a..98e8a2bc68 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -331,7 +331,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, // FIXME: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing // from the displayQueue? Need to not cancel early fails from displaySentinel changes. ASSentinel *displaySentinel = (asynchronously ? _displaySentinel : nil); - int64_t displaySentinelValue = [displaySentinel increment]; + int32_t displaySentinelValue = [displaySentinel increment]; asdisplaynode_iscancelled_block_t isCancelledBlock = ^{ return BOOL(displaySentinelValue != displaySentinel.value); diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 4fdaa8632a..79f2b2e273 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -45,9 +45,17 @@ typedef NS_OPTIONS(NSUInteger, ASHierarchyState) ASHierarchyStateRangeManaged = 1 << 1, /** Down-propogated version of _flags.visibilityNotificationsDisabled. This flag is very rarely set, but by having it locally available to nodes, they do not have to walk up supernodes at the critical points it is checked. */ - ASHierarchyStateTransitioningSupernodes = 1 << 2 + ASHierarchyStateTransitioningSupernodes = 1 << 2, + /** One of the supernodes of this node is performing a transition. + Any layout calculated during this state should not be applied immediately, but pending until later. */ + ASHierarchyStateLayoutPending = 1 << 3 }; +inline BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateLayoutPending) == ASHierarchyStateLayoutPending); +} + @interface ASDisplayNode () { @protected diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index c63b89582b..89c5678a96 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -18,12 +18,14 @@ #import "ASThread.h" #import "ASLayoutOptions.h" #import "_ASTransitionContext.h" +#import "ASDisplayNodeLayoutContext.h" #include @protocol _ASDisplayLayerDelegate; @class _ASDisplayLayer; @class _ASPendingState; +@class ASSentinel; BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); @@ -68,6 +70,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo unsigned shouldRasterizeDescendants:1; unsigned shouldBypassEnsureDisplay:1; unsigned displaySuspended:1; + unsigned shouldAnimateSizeChanges:1; unsigned hasCustomDrawingPriority:1; // whether custom drawing is enabled @@ -89,15 +92,13 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo ASDisplayNode * __weak _supernode; ASSentinel *_displaySentinel; - ASSentinel *_replaceAsyncSentinel; + ASSentinel *_transitionSentinel; // This is the desired contentsScale, not the scale at which the layer's contents should be displayed CGFloat _contentsScaleForDisplay; - ASLayout *_previousLayout; ASLayout *_layout; - ASSizeRange _previousConstrainedSize; ASSizeRange _constrainedSize; UIEdgeInsets _hitTestSlop; @@ -107,11 +108,9 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo _ASTransitionContext *_transitionContext; BOOL _usesImplicitHierarchyManagement; - NSArray *_insertedSubnodes; - NSArray *_removedSubnodes; - std::vector _insertedSubnodePositions; - std::vector _removedSubnodePositions; - + int32_t _pendingTransitionID; + ASDisplayNodeLayoutContext *_pendingLayoutContext; + ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; ASDisplayNodeDidLoadBlock _nodeLoadedBlock; diff --git a/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.h b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.h new file mode 100644 index 0000000000..2c5530cab2 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.h @@ -0,0 +1,33 @@ +// +// ASDisplayNodeLayoutContext.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 3/8/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASDimension.h" +#import "_ASTransitionContext.h" + +@class ASDisplayNode; +@class ASLayout; + +@interface ASDisplayNodeLayoutContext : NSObject <_ASTransitionContextLayoutDelegate> + +@property (nonatomic, readonly, weak) ASDisplayNode *node; +@property (nonatomic, readonly, strong) ASLayout *pendingLayout; +@property (nonatomic, readonly, assign) ASSizeRange pendingConstrainedSize; +@property (nonatomic, readonly, strong) ASLayout *previousLayout; +@property (nonatomic, readonly, assign) ASSizeRange previousConstrainedSize; + +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(ASLayout *)pendingLayout + pendingConstrainedSize:(ASSizeRange)pendingConstrainedSize + previousLayout:(ASLayout *)previousLayout + previousConstrainedSize:(ASSizeRange)previousConstrainedSize; + +- (void)applySubnodeInsertions; + +- (void)applySubnodeRemovals; + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.mm b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.mm new file mode 100644 index 0000000000..446bdb2439 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.mm @@ -0,0 +1,190 @@ +// +// ASDisplayNodeLayoutContext.mm +// AsyncDisplayKit +// +// Created by Huy Nguyen on 3/8/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASDisplayNodeLayoutContext.h" + +#import "ASDisplayNode.h" +#import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASLayout.h" + +#import + +#import "NSArray+Diffing.h" +#import "ASEqualityHelpers.h" + +@implementation ASDisplayNodeLayoutContext { + ASDN::RecursiveMutex _propertyLock; + BOOL _calculatedSubnodeOperations; + NSArray *_insertedSubnodes; + NSArray *_removedSubnodes; + std::vector _insertedSubnodePositions; + std::vector _removedSubnodePositions; +} + +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(ASLayout *)pendingLayout + pendingConstrainedSize:(ASSizeRange)pendingConstrainedSize + previousLayout:(ASLayout *)previousLayout + previousConstrainedSize:(ASSizeRange)previousConstrainedSize +{ + self = [super init]; + if (self) { + _node = node; + _pendingLayout = pendingLayout; + _pendingConstrainedSize = pendingConstrainedSize; + _previousLayout = previousLayout; + _previousConstrainedSize = previousConstrainedSize; + } + return self; +} + +- (void)applySubnodeInsertions +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + for (NSInteger i = 0; i < [_insertedSubnodes count]; i++) { + NSInteger p = _insertedSubnodePositions[i]; + [_node insertSubnode:_insertedSubnodes[i] atIndex:p]; + } +} + +- (void)applySubnodeRemovals +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + for (NSInteger i = 0; i < [_removedSubnodes count]; i++) { + [_removedSubnodes[i] removeFromSupernode]; + } +} + +- (void)calculateSubnodeOperationsIfNeeded +{ + ASDN::MutexLocker l(_propertyLock); + if (_calculatedSubnodeOperations) { + return; + } + if (_previousLayout) { + NSIndexSet *insertions, *deletions; + [_previousLayout.immediateSublayouts asdk_diffWithArray:_pendingLayout.immediateSublayouts + insertions:&insertions + deletions:&deletions + compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) { + return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject); + }]; + filterNodesInLayoutAtIndexes(_pendingLayout, insertions, &_insertedSubnodes, &_insertedSubnodePositions); + filterNodesInLayoutAtIndexesWithIntersectingNodes(_previousLayout, + deletions, + _insertedSubnodes, + &_removedSubnodes, + &_removedSubnodePositions); + } else { + NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [_pendingLayout.immediateSublayouts count])]; + filterNodesInLayoutAtIndexes(_pendingLayout, indexes, &_insertedSubnodes, &_insertedSubnodePositions); + _removedSubnodes = nil; + } + _calculatedSubnodeOperations = YES; +} + +#pragma mark - _ASTransitionContextDelegate + +- (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + ASDN::MutexLocker l(_propertyLock); + return _node.subnodes; +} + +- (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + return _insertedSubnodes; +} + +- (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + return _removedSubnodes; +} + +- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key +{ + ASDN::MutexLocker l(_propertyLock); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousLayout; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingLayout; + } else { + return nil; + } +} + +- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key +{ + ASDN::MutexLocker l(_propertyLock); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousConstrainedSize; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingConstrainedSize; + } else { + return ASSizeRangeMake(CGSizeZero, CGSizeZero); + } +} + +#pragma mark - Filter helpers + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + */ +static inline void filterNodesInLayoutAtIndexes( + ASLayout *layout, + NSIndexSet *indexes, + NSArray * __strong *storedNodes, + std::vector *storedPositions + ) +{ + filterNodesInLayoutAtIndexesWithIntersectingNodes(layout, indexes, nil, storedNodes, storedPositions); +} + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + * @discussion If the node exists in the `intersectingNodes` array, the node is not added to `storedNodes`. + */ +static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes( + ASLayout *layout, + NSIndexSet *indexes, + NSArray *intersectingNodes, + NSArray * __strong *storedNodes, + std::vector *storedPositions + ) +{ + NSMutableArray *nodes = [NSMutableArray array]; + std::vector positions = std::vector(); + NSInteger idx = [indexes firstIndex]; + while (idx != NSNotFound) { + BOOL skip = NO; + ASDisplayNode *node = (ASDisplayNode *)layout.immediateSublayouts[idx].layoutableObject; + ASDisplayNodeCAssert(node, @"A flattened layout must consist exclusively of node sublayouts"); + for (ASDisplayNode *i in intersectingNodes) { + if (node == i) { + skip = YES; + break; + } + } + if (!skip) { + [nodes addObject:node]; + positions.push_back(idx); + } + idx = [indexes indexGreaterThanIndex:idx]; + } + *storedNodes = nodes; + *storedPositions = positions; +} + +@end diff --git a/AsyncDisplayKit/Private/ASWeakSet.h b/AsyncDisplayKit/Private/ASWeakSet.h index 8f6a6576ca..2a72b19e9d 100644 --- a/AsyncDisplayKit/Private/ASWeakSet.h +++ b/AsyncDisplayKit/Private/ASWeakSet.h @@ -27,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN /// Removes all objects from the set. - (void)removeAllObjects; +/// Returns a standard *retained* NSArray of all objects. Not free to generate, but useful for iterating over contents. +- (NSArray *)allObjects; + /** How many objects are contained in this set. diff --git a/AsyncDisplayKit/Private/ASWeakSet.m b/AsyncDisplayKit/Private/ASWeakSet.m index 516d056307..7d8b180080 100644 --- a/AsyncDisplayKit/Private/ASWeakSet.m +++ b/AsyncDisplayKit/Private/ASWeakSet.m @@ -7,6 +7,7 @@ // #import "ASWeakSet.h" +#import @interface ASWeakSet<__covariant ObjectType> () @property (nonatomic, strong, readonly) NSMapTable *mapTable; @@ -25,7 +26,7 @@ - (void)addObject:(id)object { - [_mapTable setObject:[NSNull null] forKey:object]; + [_mapTable setObject:(NSNull *)kCFNull forKey:object]; } - (void)removeObject:(id)object @@ -38,6 +39,22 @@ [_mapTable removeAllObjects]; } +- (NSArray *)allObjects +{ + // We use keys instead of values in the map table for efficiency and better characteristics when the keys are deallocated. + // Documentation is currently unclear on whether -keyEnumerator retains its values, but does imply that modifying a + // mutable collection is still not safe while enumerating that way - which is one of the main uses for this method. + // A helper function called NSAllMapTableKeys() might do exactly what we want and should be more efficient, but unfortunately + // is throwing a strange compiler error and may not be available in practice on the latest iOS version. + // Lastly, even -dictionaryRepresentation and then -allKeys won't work, because it attemps to copy the values of each key, + // which may not support copying (such as ASRangeControllers). + NSMutableArray *allObjects = [NSMutableArray array]; + for (id object in _mapTable) { + [allObjects addObject:object]; + } + return allObjects; +} + - (BOOL)containsObject:(id)object { return [_mapTable objectForKey:object] != nil; @@ -75,7 +92,7 @@ - (NSString *)description { - return [[super description] stringByAppendingFormat:@" count: %lu, contents: %@", self.count, _mapTable]; + return [[super description] stringByAppendingFormat:@" count: %lu, contents: %@", (unsigned long)self.count, _mapTable]; } @end diff --git a/AsyncDisplayKit/_ASTransitionContext.h b/AsyncDisplayKit/_ASTransitionContext.h index 9411d76ac1..c9d4cd7ff9 100644 --- a/AsyncDisplayKit/_ASTransitionContext.h +++ b/AsyncDisplayKit/_ASTransitionContext.h @@ -13,7 +13,7 @@ @class ASLayout; @class _ASTransitionContext; -@protocol _ASTransitionContextDelegate +@protocol _ASTransitionContextLayoutDelegate - (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context; @@ -23,6 +23,10 @@ - (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key; - (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key; +@end + +@protocol _ASTransitionContextCompletionDelegate + - (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete; @end @@ -31,6 +35,8 @@ @property (assign, readonly, nonatomic, getter=isAnimated) BOOL animated; -- (instancetype)initWithAnimation:(BOOL)animated delegate:(id<_ASTransitionContextDelegate>)delegate; +- (instancetype)initWithAnimation:(BOOL)animated + layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate + completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate; @end diff --git a/AsyncDisplayKit/_ASTransitionContext.m b/AsyncDisplayKit/_ASTransitionContext.m index 8c69f194fa..2474f3f395 100644 --- a/AsyncDisplayKit/_ASTransitionContext.m +++ b/AsyncDisplayKit/_ASTransitionContext.m @@ -16,18 +16,22 @@ NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransi @interface _ASTransitionContext () -@property (weak, nonatomic) id<_ASTransitionContextDelegate> delegate; +@property (weak, nonatomic) id<_ASTransitionContextLayoutDelegate> layoutDelegate; +@property (weak, nonatomic) id<_ASTransitionContextCompletionDelegate> completionDelegate; @end @implementation _ASTransitionContext -- (instancetype)initWithAnimation:(BOOL)animated delegate:(id<_ASTransitionContextDelegate>)delegate +- (instancetype)initWithAnimation:(BOOL)animated + layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate + completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate { self = [super init]; if (self) { _animated = animated; - _delegate = delegate; + _layoutDelegate = layoutDelegate; + _completionDelegate = completionDelegate; } return self; } @@ -36,17 +40,17 @@ NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransi - (ASLayout *)layoutForKey:(NSString *)key { - return [_delegate transitionContext:self layoutForKey:key]; + return [_layoutDelegate transitionContext:self layoutForKey:key]; } - (ASSizeRange)constrainedSizeForKey:(NSString *)key { - return [_delegate transitionContext:self constrainedSizeForKey:key]; + return [_layoutDelegate transitionContext:self constrainedSizeForKey:key]; } - (CGRect)initialFrameForNode:(ASDisplayNode *)node { - for (ASDisplayNode *subnode in [_delegate currentSubnodesWithTransitionContext:self]) { + for (ASDisplayNode *subnode in [_layoutDelegate currentSubnodesWithTransitionContext:self]) { if (node == subnode) { return node.frame; } @@ -75,17 +79,17 @@ NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransi - (NSArray *)insertedSubnodes { - return [_delegate insertedSubnodesWithTransitionContext:self]; + return [_layoutDelegate insertedSubnodesWithTransitionContext:self]; } - (NSArray *)removedSubnodes { - return [_delegate removedSubnodesWithTransitionContext:self]; + return [_layoutDelegate removedSubnodesWithTransitionContext:self]; } - (void)completeTransition:(BOOL)didComplete { - [_delegate transitionContext:self didComplete:didComplete]; + [_completionDelegate transitionContext:self didComplete:didComplete]; } @end diff --git a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m index 02d5d9d127..b3c772ae8a 100644 --- a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m +++ b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m @@ -1,5 +1,5 @@ // -// ASZBasicImageDownloaderTests.m +// ASBasicImageDownloaderTests.m // AsyncDisplayKit // // Created by Victor Mayorov on 10/06/15. @@ -10,7 +10,6 @@ #import -// Z in the name to delay running until after the test instance is operating normally. @interface ASBasicImageDownloaderTests : XCTestCase @end @@ -19,35 +18,30 @@ - (void)testAsynchronouslyDownloadTheSameURLTwice { - ASBasicImageDownloader *downloader = [ASBasicImageDownloader sharedImageDownloader]; - - NSURL *URL = [NSURL URLWithString:@"http://wrongPath/wrongResource.png"]; + XCTestExpectation *firstExpectation = [self expectationWithDescription:@"First ASBasicImageDownloader completion handler should be called within 3 seconds"]; + XCTestExpectation *secondExpectation = [self expectationWithDescription:@"Second ASBasicImageDownloader completion handler should be called within 3 seconds"]; + + ASBasicImageDownloader *downloader = [ASBasicImageDownloader sharedImageDownloader]; + NSURL *URL = [NSURL URLWithString:@"http://wrongPath/wrongResource.png"]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + [downloader downloadImageWithURL:URL + callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + downloadProgressBlock:nil + completion:^(CGImageRef image, NSError *error) { + [firstExpectation fulfill]; + }]; - __block BOOL firstDone = NO; - - [downloader downloadImageWithURL:URL - callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) - downloadProgressBlock:nil - completion:^(CGImageRef image, NSError *error) { - firstDone = YES; - }]; - - __block BOOL secondDone = NO; - - [downloader downloadImageWithURL:URL - callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) - downloadProgressBlock:nil - completion:^(CGImageRef image, NSError *error) { - secondDone = YES; - }]; - + [downloader downloadImageWithURL:URL + callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + downloadProgressBlock:nil + completion:^(CGImageRef image, NSError *error) { + [secondExpectation fulfill]; + }]; #pragma clang diagnostic pop - sleep(3); - XCTAssert(firstDone && secondDone, @"Not all ASBasicImageDownloader completion handlers have been called after 3 seconds"); + [self waitForExpectationsWithTimeout:3 handler:nil]; } @end diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index 455591cd1b..3c2fb4c67b 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -278,7 +278,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ NSString *targetName = isLayerBacked ? @"layer" : @"view"; NSString *hasLoadedView = node.nodeLoaded ? @"with view" : [NSString stringWithFormat:@"after loading %@", targetName]; - id rgbBlackCGColorIdPtr = (id)[UIColor colorWithRed:0 green:0 blue:0 alpha:1].CGColor; +// id rgbBlackCGColorIdPtr = (id)[UIColor blackColor].CGColor; XCTAssertEqual((id)nil, node.contents, @"default contents broken %@", hasLoadedView); XCTAssertEqual(NO, node.clipsToBounds, @"default clipsToBounds broken %@", hasLoadedView); @@ -298,12 +298,12 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.subnodeTransform), @"default subnodeTransform broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.backgroundColor, @"default backgroundColor broken %@", hasLoadedView); XCTAssertEqual(UIViewContentModeScaleToFill, node.contentMode, @"default contentMode broken %@", hasLoadedView); - XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.shadowColor, @"default shadowColor broken %@", hasLoadedView); +// XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.shadowColor, @"default shadowColor broken %@", hasLoadedView); XCTAssertEqual(0.0f, node.shadowOpacity, @"default shadowOpacity broken %@", hasLoadedView); XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(0, -3), node.shadowOffset), @"default shadowOffset broken %@", hasLoadedView); XCTAssertEqual(3.f, node.shadowRadius, @"default shadowRadius broken %@", hasLoadedView); XCTAssertEqual(0.0f, node.borderWidth, @"default borderWidth broken %@", hasLoadedView); - XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.borderColor, @"default borderColor broken %@", hasLoadedView); +// XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.borderColor, @"default borderColor broken %@", hasLoadedView); XCTAssertEqual(NO, node.displaySuspended, @"default displaySuspended broken %@", hasLoadedView); XCTAssertEqual(YES, node.displaysAsynchronously, @"default displaysAsynchronously broken %@", hasLoadedView); XCTAssertEqual(NO, node.asyncdisplaykit_asyncTransactionContainer, @"default asyncdisplaykit_asyncTransactionContainer broken %@", hasLoadedView); diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index d14329d936..4338323705 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -140,7 +140,7 @@ _videoNode.asset = _firstAsset; [_videoNode pause]; - [_videoNode setInterfaceState:ASInterfaceStateVisible]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay]; [_videoNode didLoad]; XCTAssert(![_videoNode.subnodes containsObject:_videoNode.playerNode]);