diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 5781496c5c..4995f00798 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -407,6 +407,10 @@ D785F6621A74327E00291744 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; D785F6631A74327E00291744 /* ASScrollNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.m */; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; + DE6D9E321C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6D9E301C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h */; }; + DE6D9E331C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6D9E301C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h */; }; + DE6D9E341C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE6D9E311C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm */; }; + DE6D9E351C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE6D9E311C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm */; }; DECBD6E71BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 */; }; @@ -675,6 +679,8 @@ D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = ""; }; + DE6D9E301C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandlerVisible.h; sourceTree = ""; }; + DE6D9E311C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRangeHandlerVisible.mm; 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 = ""; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -938,6 +944,8 @@ 055F1A3619ABD413004DAFF1 /* ASRangeController.h */, 055F1A3719ABD413004DAFF1 /* ASRangeController.mm */, 292C599C1A956527007E5DD6 /* ASRangeHandler.h */, + DE6D9E301C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h */, + DE6D9E311C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm */, 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */, 292C599B1A956527007E5DD6 /* ASRangeHandlerPreload.mm */, 292C599D1A956527007E5DD6 /* ASRangeHandlerRender.h */, @@ -1159,6 +1167,7 @@ 05F20AA41A15733C00DCA68A /* ASImageProtocols.h in Headers */, 430E7C8F1B4C23F100697A4C /* ASIndexPath.h in Headers */, ACF6ED221B17843500DA7C62 /* ASInsetLayoutSpec.h in Headers */, + DE6D9E321C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h in Headers */, ACF6ED4B1B17847A00DA7C62 /* ASInternalHelpers.h in Headers */, ACF6ED241B17843500DA7C62 /* ASLayout.h in Headers */, 251B8EFB1BBB3D690087C538 /* ASDataController+Subclasses.h in Headers */, @@ -1254,6 +1263,7 @@ B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, B35062191B010EFD0018CF92 /* ASDealloc2MainObject.h in Headers */, 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, + DE6D9E331C0AD9ED001A1DD3 /* ASRangeHandlerVisible.h in Headers */, B350624F1B010EFD0018CF92 /* ASDisplayNode+DebugTiming.h in Headers */, B35061FD1B010EFD0018CF92 /* ASDisplayNode+Subclasses.h in Headers */, B35061FB1B010EFD0018CF92 /* ASDisplayNode.h in Headers */, @@ -1373,7 +1383,6 @@ 058D09B9195D04C000B7D73C /* Frameworks */, 058D09BA195D04C000B7D73C /* Resources */, 3B9D88CDF51B429C8409E4B6 /* Copy Pods Resources */, - 6CC5F540055A48FCA8C12BF5 /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -1503,21 +1512,6 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 6CC5F540055A48FCA8C12BF5 /* 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 */ @@ -1599,6 +1593,7 @@ 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, 058D0A17195D050800B7D73C /* ASTextNode.mm in Sources */, 058D0A1C195D050800B7D73C /* ASTextNodeCoreTextAdditions.m in Sources */, + DE6D9E341C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm in Sources */, 058D0A1D195D050800B7D73C /* ASTextNodeRenderer.mm in Sources */, 058D0A1E195D050800B7D73C /* ASTextNodeShadower.m in Sources */, 058D0A1F195D050800B7D73C /* ASTextNodeTextKitHelpers.mm in Sources */, @@ -1717,6 +1712,7 @@ 34EFC7741B701D0A00AD841F /* ASStaticLayoutSpec.mm in Sources */, B350620B1B010EFD0018CF92 /* ASTableView.mm in Sources */, B350620E1B010EFD0018CF92 /* ASTextNode.mm in Sources */, + DE6D9E351C0AD9ED001A1DD3 /* ASRangeHandlerVisible.mm in Sources */, B350622F1B010EFD0018CF92 /* ASTextNodeCoreTextAdditions.m in Sources */, B35062311B010EFD0018CF92 /* ASTextNodeRenderer.mm in Sources */, B35062331B010EFD0018CF92 /* ASTextNodeShadower.m in Sources */, diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 4ba3c3b372..d9f1a59345 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -115,13 +115,22 @@ @interface _ASCollectionViewCell : UICollectionViewCell @implementation _ASCollectionViewCell +- (void)setNode:(ASCellNode *)node +{ + _node = node; + node.selected = self.selected; + node.highlighted = self.highlighted; +} + - (void)setSelected:(BOOL)selected { + [super setSelected:selected]; _node.selected = selected; } - (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; _node.highlighted = highlighted; } @@ -479,11 +488,8 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell _ASCollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; - - [_rangeController configureContentView:cell.contentView forCellNode:node]; - cell.node = node; - + [_rangeController configureContentView:cell.contentView forCellNode:node]; return cell; } diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index cf9b7a54f3..80ee51aa6f 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -32,6 +32,27 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); */ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); +/** + Interface state is available on ASDisplayNode and ASViewController, and + allows checking whether a node is in an interface situation where it is prudent to trigger certain + actions: measurement, data fetching, display, and visibility (the latter for animations or other onscreen-only effects). + */ + +typedef NS_OPTIONS(NSUInteger, ASInterfaceState) +{ + /** The element is not predicted to be onscreen soon and preloading should not be performed */ + ASInterfaceStateNone = 1 << 0, + /** The element may be added to a view soon that could become visible. Measure the layout, including size calculation. */ + ASInterfaceStateMeasureLayout = 1 << 1, + /** The element is likely enough to come onscreen that disk and/or network data required for display should be fetched. */ + ASInterfaceStateFetchData = 1 << 2, + /** The element is very likely to become visible, and concurrent rendering should be executed for any -setNeedsDisplay. */ + ASInterfaceStateDisplay = 1 << 3, + /** The element is physically onscreen by at least 1 pixel. + In practice, all other bit fields should also be set when this flag is set. */ + ASInterfaceStateVisible = 1 << 4, +}; + /** * An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view * hierarchy off the main thread, and could do rendering off the main thread as well. @@ -123,7 +144,6 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); /** @name Getting view and layer */ - /** * @abstract Returns a view. * @@ -160,10 +180,17 @@ typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); */ @property (nonatomic, readonly, retain) CALayer *layer; +/** + * @abstract Returns the Interface State of the node. + * + * @return The current ASInterfaceState of the node, indicating whether it is visible and other situational properties. + * + * @see ASInterfaceState + */ +@property (nonatomic, readonly) ASInterfaceState interfaceState; /** @name Managing dimensions */ - /** * @abstract Asks the node to measure and return the size that best fits its subnodes. * diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index e7c6e54c0b..9c52f08c21 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -379,11 +379,6 @@ - (BOOL)__shouldSize return YES; } -- (void)__exitedHierarchy -{ - -} - - (UIView *)_viewToLoad { UIView *view; @@ -787,55 +782,9 @@ - (CATransform3D)_transformToAncestor:(ASDisplayNode *)ancestor return transform; } -static inline BOOL _ASDisplayNodeIsAncestorOfDisplayNode(ASDisplayNode *possibleAncestor, ASDisplayNode *possibleDescendent) -{ - ASDisplayNode *supernode = possibleDescendent; - while (supernode) { - if (supernode == possibleAncestor) { - return YES; - } - supernode = supernode.supernode; - } - - return NO; -} - -/** - * NOTE: It is an error to try to convert between nodes which do not share a common ancestor. This behavior is - * disallowed in UIKit documentation and the behavior is left undefined. The output does not have a rigorously defined - * failure mode (i.e. returning CGPointZero or returning the point exactly as passed in). Rather than track the internal - * undefined and undocumented behavior of UIKit in ASDisplayNode, this operation is defined to be incorrect in all - * circumstances and must be fixed wherever encountered. - */ -static inline ASDisplayNode *_ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2) -{ - ASDisplayNode *possibleAncestor = node1; - while (possibleAncestor) { - if (_ASDisplayNodeIsAncestorOfDisplayNode(possibleAncestor, node2)) { - break; - } - possibleAncestor = possibleAncestor.supernode; - } - - ASDisplayNodeCAssertNotNil(possibleAncestor, @"Could not find a common ancestor between node1: %@ and node2: %@", node1, node2); - return possibleAncestor; -} - -static inline ASDisplayNode *_getRootNode(ASDisplayNode *node) -{ - // node <- supernode on each loop - // previous <- node on each loop where node is not nil - // previous is the final non-nil value of supernode, i.e. the root node - ASDisplayNode *previousNode = node; - while ((node = [node supernode])) { - previousNode = node; - } - return previousNode; -} - static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNode *referenceNode, ASDisplayNode *targetNode) { - ASDisplayNode *ancestor = _ASDisplayNodeFindClosestCommonAncestor(referenceNode, targetNode); + ASDisplayNode *ancestor = ASDisplayNodeFindClosestCommonAncestor(referenceNode, targetNode); // Transform into global (away from reference coordinate space) CATransform3D transformToGlobal = [referenceNode _transformToAncestor:ancestor]; @@ -850,7 +799,7 @@ - (CGPoint)convertPoint:(CGPoint)point fromNode:(ASDisplayNode *)node { ASDisplayNodeAssertThreadAffinity(self); // Get root node of the accessible node hierarchy, if node not specified - node = node ? node : _getRootNode(self); + node = node ? node : ASDisplayNodeUltimateParentOfNode(self); // Calculate transform to map points between coordinate spaces CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); @@ -865,7 +814,7 @@ - (CGPoint)convertPoint:(CGPoint)point toNode:(ASDisplayNode *)node { ASDisplayNodeAssertThreadAffinity(self); // Get root node of the accessible node hierarchy, if node not specified - node = node ? node : _getRootNode(self); + node = node ? node : ASDisplayNodeUltimateParentOfNode(self); // Calculate transform to map points between coordinate spaces CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); @@ -880,7 +829,7 @@ - (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node { ASDisplayNodeAssertThreadAffinity(self); // Get root node of the accessible node hierarchy, if node not specified - node = node ? node : _getRootNode(self); + node = node ? node : ASDisplayNodeUltimateParentOfNode(self); // Calculate transform to map points between coordinate spaces CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); @@ -895,7 +844,7 @@ - (CGRect)convertRect:(CGRect)rect toNode:(ASDisplayNode *)node { ASDisplayNodeAssertThreadAffinity(self); // Get root node of the accessible node hierarchy, if node not specified - node = node ? node : _getRootNode(self); + node = node ? node : ASDisplayNodeUltimateParentOfNode(self); // Calculate transform to map points between coordinate spaces CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); @@ -1614,6 +1563,7 @@ - (void)invalidateCalculatedLayout - (void)__didLoad { + ASDN::MutexLocker l(_propertyLock); if (_nodeLoadedBlock) { _nodeLoadedBlock(self); _nodeLoadedBlock = nil; @@ -1638,8 +1588,6 @@ - (void)didExitHierarchy ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(_flags.isExitingHierarchy, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); - - [self __exitedHierarchy]; } - (void)clearContents @@ -1650,6 +1598,7 @@ - (void)clearContents _placeholderImage = nil; } +// TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or exitInterfaceState: - (void)recursivelyClearContents { for (ASDisplayNode *subnode in self.subnodes) { @@ -1663,6 +1612,7 @@ - (void)fetchData // subclass override } +// TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or enterInterfaceState: - (void)recursivelyFetchData { for (ASDisplayNode *subnode in self.subnodes) { @@ -1676,6 +1626,7 @@ - (void)clearFetchedData // subclass override } +// TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or exitInterfaceState: - (void)recursivelyClearFetchedData { for (ASDisplayNode *subnode in self.subnodes) { @@ -1684,6 +1635,67 @@ - (void)recursivelyClearFetchedData [self clearFetchedData]; } +- (ASInterfaceState)interfaceState +{ + ASDN::MutexLocker l(_propertyLock); + return _interfaceState; +} + +- (void)setInterfaceState:(ASInterfaceState)interfaceState +{ + ASDN::MutexLocker l(_propertyLock); + if (interfaceState != _interfaceState) { + if ((interfaceState & ASInterfaceStateMeasureLayout) != (_interfaceState & ASInterfaceStateMeasureLayout)) { + // Trigger asynchronous measurement if it is not already cached or being calculated. + } + + // Entered or exited data loading state. + if ((interfaceState & ASInterfaceStateFetchData) != (_interfaceState & ASInterfaceStateFetchData)) { + if (interfaceState & ASInterfaceStateFetchData) { + [self fetchData]; + } else { + [self clearFetchedData]; + } + } + + // Entered or exited contents rendering state. + if ((interfaceState & ASInterfaceStateDisplay) != (_interfaceState & ASInterfaceStateDisplay)) { + if (interfaceState & ASInterfaceStateDisplay) { + // Once the working window is eliminated (ASRangeHandlerRender), trigger display directly here. + [self setDisplaySuspended:NO]; + } else { + [self setDisplaySuspended:YES]; + [self clearContents]; + } + } + + // Entered or exited data loading state. + if ((interfaceState & ASInterfaceStateVisible) != (_interfaceState & ASInterfaceStateVisible)) { + if (interfaceState & ASInterfaceStateVisible) { + // Consider providing a -didBecomeVisible. + } else { + // Consider providing a -didBecomeInvisible. + } + } + + _interfaceState = interfaceState; + } +} + +- (void)enterInterfaceState:(ASInterfaceState)interfaceState +{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, ^(ASDisplayNode *node) { + node.interfaceState |= interfaceState; + }); +} + +- (void)exitInterfaceState:(ASInterfaceState)interfaceState +{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, ^(ASDisplayNode *node) { + node.interfaceState &= (~interfaceState); + }); +} + - (void)layout { ASDisplayNodeAssertMainThread(); @@ -1740,6 +1752,7 @@ - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode - (void)setNeedsDisplayAtScale:(CGFloat)contentsScale { + ASDN::MutexLocker l(_propertyLock); if (contentsScale != self.contentsScaleForDisplay) { self.contentsScaleForDisplay = contentsScale; [self setNeedsDisplay]; @@ -1748,12 +1761,9 @@ - (void)setNeedsDisplayAtScale:(CGFloat)contentsScale - (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale { - [self setNeedsDisplayAtScale:contentsScale]; - - ASDN::MutexLocker l(_propertyLock); - for (ASDisplayNode *child in _subnodes) { - [child recursivelySetNeedsDisplayAtScale:contentsScale]; - } + ASDisplayNodePerformBlockOnEveryNode(nil, self, ^(ASDisplayNode *node) { + [node setNeedsDisplayAtScale:contentsScale]; + }); } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event @@ -1889,6 +1899,7 @@ - (void)recursivelySetDisplaySuspended:(BOOL)flag _recursivelySetDisplaySuspended(self, nil, flag); } +// TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or a variant with a condition / test block. static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, BOOL flag) { // If there is no layer, but node whose its view is loaded, then we can traverse down its layer hierarchy. Otherwise we must stick to the node hierarchy to avoid loading views prematurely. Note that for nodes that haven't loaded their views, they can't possibly have subviews/sublayers, so we don't need to traverse the layer hierarchy for them. diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 5ce6ac2ba5..4ccf0b48ca 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -24,6 +24,19 @@ extern ASDisplayNode *ASLayerToDisplayNode(CALayer *layer); */ extern ASDisplayNode *ASViewToDisplayNode(UIView *view); +/** + Given a node, returns the root of the node heirarchy (where supernode == nil) + */ +extern ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node); + +/** + This function will walk the layer heirarchy, spanning discontinuous sections of the node heirarchy (e.g. the layers + of UIKit intermediate views in UIViewControllers, UITableView, UICollectionView). + In the event that a node's backing layer is not created yet, the function will only walk the direct subnodes instead + of forcing the layer heirarchy to be created. + */ +extern void ASDisplayNodePerformBlockOnEveryNode(CALayer *layer, ASDisplayNode *node, void(^block)(ASDisplayNode *node)); + /** Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block. */ @@ -34,6 +47,16 @@ extern id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *no */ extern id ASDisplayNodeFindClass(ASDisplayNode *start, Class c); +/** + * Given two nodes, finds their most immediate common parent. Used for geometry conversion methods. + * NOTE: It is an error to try to convert between nodes which do not share a common ancestor. This behavior is + * disallowed in UIKit documentation and the behavior is left undefined. The output does not have a rigorously defined + * failure mode (i.e. returning CGPointZero or returning the point exactly as passed in). Rather than track the internal + * undefined and undocumented behavior of UIKit in ASDisplayNode, this operation is defined to be incorrect in all + * circumstances and must be fixed wherever encountered. + */ +extern ASDisplayNode *ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2); + /** Given a display node, collects all descendents. This is a specialization of ASCollectContainer() that walks the Core Animation layer tree as opposed to the display node tree, thus supporting non-continues display node hierarchies. */ diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index 8d2e261570..681640ffa3 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -10,16 +10,42 @@ #import "ASDisplayNodeInternal.h" -ASDisplayNode *ASLayerToDisplayNode(CALayer *layer) +extern ASDisplayNode *ASLayerToDisplayNode(CALayer *layer) { return layer.asyncdisplaykit_node; } -ASDisplayNode *ASViewToDisplayNode(UIView *view) +extern ASDisplayNode *ASViewToDisplayNode(UIView *view) { return view.asyncdisplaykit_node; } +extern void ASDisplayNodePerformBlockOnEveryNode(CALayer *layer, ASDisplayNode *node, void(^block)(ASDisplayNode *node)) +{ + if (!node) { + ASDisplayNodeCAssertNotNil(layer, @"Cannot recursively perform with nil node and nil layer"); + ASDisplayNodeCAssertMainThread(); + node = ASLayerToDisplayNode(layer); + } + + if (node) { + block(node); + } + if (!layer && [node isNodeLoaded]) { + layer = node.layer; + } + + if (layer) { + for (CALayer *sublayer in [layer sublayers]) { + ASDisplayNodePerformBlockOnEveryNode(sublayer, nil, block); + } + } else if (node) { + for (ASDisplayNode *subnode in [node subnodes]) { + ASDisplayNodePerformBlockOnEveryNode(nil, subnode, block); + } + } +} + id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) { CALayer *layer = node.layer; @@ -122,6 +148,45 @@ extern id ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) }); } +static inline BOOL _ASDisplayNodeIsAncestorOfDisplayNode(ASDisplayNode *possibleAncestor, ASDisplayNode *possibleDescendent) +{ + ASDisplayNode *supernode = possibleDescendent; + while (supernode) { + if (supernode == possibleAncestor) { + return YES; + } + supernode = supernode.supernode; + } + + return NO; +} + +extern ASDisplayNode *ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2) +{ + ASDisplayNode *possibleAncestor = node1; + while (possibleAncestor) { + if (_ASDisplayNodeIsAncestorOfDisplayNode(possibleAncestor, node2)) { + break; + } + possibleAncestor = possibleAncestor.supernode; + } + + ASDisplayNodeCAssertNotNil(possibleAncestor, @"Could not find a common ancestor between node1: %@ and node2: %@", node1, node2); + return possibleAncestor; +} + +extern ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node) +{ + // node <- supernode on each loop + // previous <- node on each loop where node is not nil + // previous is the final non-nil value of supernode, i.e. the root node + ASDisplayNode *previousNode = node; + while ((node = [node supernode])) { + previousNode = node; + } + return previousNode; +} + #pragma mark - Placeholders UIColor *ASDisplayNodeDefaultPlaceholderColor() diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 57cb605a79..7354bdcf55 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -135,13 +135,22 @@ - (void)didTransitionToState:(UITableViewCellStateMask)state [super didTransitionToState:state]; } +- (void)setNode:(ASCellNode *)node +{ + _node = node; + node.selected = self.selected; + node.highlighted = self.highlighted; +} + - (void)setSelected:(BOOL)selected animated:(BOOL)animated { + [super setSelected:selected animated:animated]; _node.selected = selected; } - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { + [super setHighlighted:highlighted animated:animated]; _node.highlighted = highlighted; } diff --git a/AsyncDisplayKit/Details/ASAbstractLayoutController.mm b/AsyncDisplayKit/Details/ASAbstractLayoutController.mm index 30723e43b7..1aef1c1e0e 100644 --- a/AsyncDisplayKit/Details/ASAbstractLayoutController.mm +++ b/AsyncDisplayKit/Details/ASAbstractLayoutController.mm @@ -26,14 +26,19 @@ - (instancetype)init } _tuningParameters = std::vector(ASLayoutRangeTypeCount); + _tuningParameters[ASLayoutRangeTypeVisible] = { + .leadingBufferScreenfuls = 0, + .trailingBufferScreenfuls = 0 + }; + _tuningParameters[ASLayoutRangeTypeRender] = { + .leadingBufferScreenfuls = 1.5, + .trailingBufferScreenfuls = 0.75 + }; _tuningParameters[ASLayoutRangeTypePreload] = { .leadingBufferScreenfuls = 3, .trailingBufferScreenfuls = 2 }; - _tuningParameters[ASLayoutRangeTypeRender] = { - .leadingBufferScreenfuls = 2, - .trailingBufferScreenfuls = 1 - }; + return self; } @@ -49,6 +54,7 @@ - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)range - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType { ASDisplayNodeAssert(rangeType < _tuningParameters.size(), @"Requesting a range that is OOB for the configured tuning parameters"); + ASDisplayNodeAssert(rangeType != ASLayoutRangeTypeVisible, @"Must not set Visible range tuning parameters (always 0, 0)"); _tuningParameters[rangeType] = tuningParameters; } diff --git a/AsyncDisplayKit/Details/ASLayoutRangeType.h b/AsyncDisplayKit/Details/ASLayoutRangeType.h index 128b88b9fe..a5f2075485 100644 --- a/AsyncDisplayKit/Details/ASLayoutRangeType.h +++ b/AsyncDisplayKit/Details/ASLayoutRangeType.h @@ -9,6 +9,7 @@ #import typedef NS_ENUM(NSInteger, ASLayoutRangeType) { + ASLayoutRangeTypeVisible, ASLayoutRangeTypeRender, ASLayoutRangeTypePreload, ASLayoutRangeTypeCount diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 1907baff75..ccbeff0c66 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -11,6 +11,7 @@ #import "ASAssert.h" #import "ASDisplayNodeExtras.h" #import "ASMultiDimensionalArrayUtils.h" +#import "ASRangeHandlerVisible.h" #import "ASRangeHandlerRender.h" #import "ASRangeHandlerPreload.h" #import "ASInternalHelpers.h" @@ -36,6 +37,7 @@ - (instancetype)init { _rangeIsValid = YES; _rangeTypeIndexPaths = [NSMutableDictionary dictionary]; _rangeTypeHandlers = @{ + @(ASLayoutRangeTypeVisible): [[ASRangeHandlerVisible alloc] init], @(ASLayoutRangeTypeRender): [[ASRangeHandlerRender alloc] init], @(ASLayoutRangeTypePreload): [[ASRangeHandlerPreload alloc] init], }; diff --git a/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm b/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm index d30f6c1a40..adbbf5ecfb 100644 --- a/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm +++ b/AsyncDisplayKit/Details/ASRangeHandlerPreload.mm @@ -7,22 +7,21 @@ */ #import "ASRangeHandlerPreload.h" - #import "ASDisplayNode.h" -#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNodeInternal.h" @implementation ASRangeHandlerPreload - (void)node:(ASDisplayNode *)node enteredRangeOfType:(ASLayoutRangeType)rangeType { ASDisplayNodeAssert(rangeType == ASLayoutRangeTypePreload, @"Preload delegate should not handle other ranges"); - [node recursivelyFetchData]; + [node enterInterfaceState:ASInterfaceStateFetchData]; } - (void)node:(ASDisplayNode *)node exitedRangeOfType:(ASLayoutRangeType)rangeType { ASDisplayNodeAssert(rangeType == ASLayoutRangeTypePreload, @"Preload delegate should not handle other ranges"); - [node recursivelyClearFetchedData]; + [node exitInterfaceState:ASInterfaceStateFetchData]; } @end diff --git a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm index e564564956..b2a751a8bd 100644 --- a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm +++ b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm @@ -57,7 +57,8 @@ - (void)node:(ASDisplayNode *)node enteredRangeOfType:(ASLayoutRangeType)rangeTy [node.view removeFromSuperview]; } - [node recursivelySetDisplaySuspended:NO]; + // The node un-suspends display. + [node enterInterfaceState:ASInterfaceStateDisplay]; // Add the node's layer to an off-screen window to trigger display and mark its contents as non-volatile. // Use the layer directly to avoid the substantial overhead of UIView heirarchy manipulations. @@ -89,7 +90,8 @@ - (void)node:(ASDisplayNode *)node exitedRangeOfType:(ASLayoutRangeType)rangeTyp // preservation of this content could result in the app being killed, which is not likely preferable over briefly seeing placeholders in the event the user scrolls backwards. // Nonetheless, future changes to the implementation will likely eliminate this behavior to simplify debugging and extensibility of working range functionality. - [node recursivelySetDisplaySuspended:YES]; + // The node calls clearCurrentContents and suspends display + [node exitInterfaceState:ASInterfaceStateDisplay]; if (node.layer.superlayer != [[self workingWindow] layer]) { // In this case, the node has previously passed through the working range (or it is zero), and it has now fallen outside the working range. @@ -102,7 +104,6 @@ - (void)node:(ASDisplayNode *)node exitedRangeOfType:(ASLayoutRangeType)rangeTyp // At this point, the node's layer may validly be present either in the workingWindow, or in the contentsView of a cell. [node.layer removeFromSuperlayer]; - [node recursivelyClearContents]; } @end diff --git a/AsyncDisplayKit/Details/ASRangeHandlerVisible.h b/AsyncDisplayKit/Details/ASRangeHandlerVisible.h new file mode 100644 index 0000000000..eab9f77f29 --- /dev/null +++ b/AsyncDisplayKit/Details/ASRangeHandlerVisible.h @@ -0,0 +1,15 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@interface ASRangeHandlerVisible : NSObject + +@end diff --git a/AsyncDisplayKit/Details/ASRangeHandlerVisible.mm b/AsyncDisplayKit/Details/ASRangeHandlerVisible.mm new file mode 100644 index 0000000000..d8daf51889 --- /dev/null +++ b/AsyncDisplayKit/Details/ASRangeHandlerVisible.mm @@ -0,0 +1,25 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASRangeHandlerVisible.h" +#import "ASDisplayNode.h" +#import "ASDisplayNodeInternal.h" + +@implementation ASRangeHandlerVisible + +- (void)node:(ASDisplayNode *)node enteredRangeOfType:(ASLayoutRangeType)rangeType +{ + [node enterInterfaceState:ASInterfaceStateVisible]; +} + +- (void)node:(ASDisplayNode *)node exitedRangeOfType:(ASLayoutRangeType)rangeType +{ + [node exitInterfaceState:ASInterfaceStateVisible]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h index 0ca7bb3c49..47d72b34d6 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -67,3 +67,27 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { /** Expand to fill cross axis */ ASStackLayoutAlignSelfStretch, }; + +/** Orientation of children along horizontal axis */ +typedef NS_ENUM(NSUInteger, ASHorizontalAlignment) { + /** No alignment specified. Default value */ + ASHorizontalAlignmentNone, + /** Left aligned */ + ASAlignmentLeft, + /** Center aligned */ + ASAlignmentMiddle, + /** Right aligned */ + ASAlignmentRight, +}; + +/** Orientation of children along vertical axis */ +typedef NS_ENUM(NSUInteger, ASVerticalAlignment) { + /** No alignment specified. Default value */ + ASVerticalAlignmentNone, + /** Top aligned */ + ASAlignmentTop, + /** Center aligned */ + ASAlignmentCenter, + /** Bottom aligned */ + ASAlignmentBottom, +}; diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h index ebf7130bfe..a2afc29609 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h @@ -35,10 +35,25 @@ */ @interface ASStackLayoutSpec : ASLayoutSpec -/** Specifies the direction children are stacked in. */ +/** + Specifies the direction children are stacked in. If horizontalAlignment and verticalAlignment were set, + they will be resolved again, causing justifyContent and alignItems to be updated accordingly + */ @property (nonatomic, assign) ASStackLayoutDirection direction; /** The amount of space between each child. */ @property (nonatomic, assign) CGFloat spacing; +/** + Specifies how children are aligned horizontally. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties + */ +@property (nonatomic, assign) ASHorizontalAlignment horizontalAlignment; +/** + Specifies how children are aligned vertically. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties + */ +@property (nonatomic, assign) ASVerticalAlignment verticalAlignment; /** The amount of space between each child. */ @property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent; /** Orientation of children along cross axis */ diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index 0763bf2615..ed07b7d6e0 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -25,7 +25,7 @@ @implementation ASStackLayoutSpec { - ASDN::RecursiveMutex _propertyLock; + ASDN::RecursiveMutex _propertyLock; } - (instancetype)init @@ -58,8 +58,10 @@ - (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGF return nil; } _direction = direction; - _alignItems = alignItems; _spacing = spacing; + _horizontalAlignment = ASHorizontalAlignmentNone; + _verticalAlignment = ASVerticalAlignmentNone; + _alignItems = alignItems; _justifyContent = justifyContent; [self setChildren:children]; @@ -69,18 +71,44 @@ - (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGF - (void)setDirection:(ASStackLayoutDirection)direction { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); - _direction = direction; + if (_direction != direction) { + _direction = direction; + [self resolveHorizontalAlignment]; + [self resolveVerticalAlignment]; + } +} + +- (void)setHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + if (_horizontalAlignment != horizontalAlignment) { + _horizontalAlignment = horizontalAlignment; + [self resolveHorizontalAlignment]; + } +} + +- (void)setVerticalAlignment:(ASVerticalAlignment)verticalAlignment +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + if (_verticalAlignment != verticalAlignment) { + _verticalAlignment = verticalAlignment; + [self resolveVerticalAlignment]; + } } - (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used"); + ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used"); _alignItems = alignItems; } - (void)setJustifyContent:(ASStackLayoutJustifyContent)justifyContent { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used"); + ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used"); _justifyContent = justifyContent; } @@ -149,6 +177,24 @@ - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize sublayouts:sublayouts]; } +- (void)resolveHorizontalAlignment +{ + if (_direction == ASStackLayoutDirectionHorizontal) { + _justifyContent = justifyContent(_horizontalAlignment, _justifyContent); + } else { + _alignItems = alignment(_horizontalAlignment, _alignItems); + } +} + +- (void)resolveVerticalAlignment +{ + if (_direction == ASStackLayoutDirectionHorizontal) { + _alignItems = alignment(_verticalAlignment, _alignItems); + } else { + _justifyContent = justifyContent(_verticalAlignment, _justifyContent); + } +} + @end @implementation ASStackLayoutSpec (Debugging) diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index cf0681b72b..0d3ab7efe3 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -72,6 +72,8 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { NSMutableSet *_pendingDisplayNodes; _ASPendingState *_pendingViewState; + + ASInterfaceState _interfaceState; struct ASDisplayNodeFlags { // public properties @@ -116,10 +118,13 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { // Bitmask to check which methods an object overrides. @property (nonatomic, assign, readonly) ASDisplayNodeMethodOverrides methodOverrides; +// These methods are recursive, and either union or remove the provided interfaceState to all sub-elements. +- (void)enterInterfaceState:(ASInterfaceState)interfaceState; +- (void)exitInterfaceState:(ASInterfaceState)interfaceState; + // Swizzle to extend the builtin functionality with custom logic - (BOOL)__shouldLoadViewOrLayer; - (BOOL)__shouldSize; -- (void)__exitedHierarchy; // Core implementation of -measureWithSizeRange:. Must be called with _propertyLock held. - (ASLayout *)__measureWithSizeRange:(ASSizeRange)constrainedSize; diff --git a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h index a61218cfe4..ae018a1f87 100644 --- a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h +++ b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h @@ -68,3 +68,63 @@ inline ASStackLayoutAlignItems alignment(ASStackLayoutAlignSelf childAlignment, return stackAlignment; } } + +inline ASStackLayoutAlignItems alignment(ASHorizontalAlignment alignment, ASStackLayoutAlignItems defaultAlignment) +{ + switch (alignment) { + case ASAlignmentLeft: + return ASStackLayoutAlignItemsStart; + case ASAlignmentMiddle: + return ASStackLayoutAlignItemsCenter; + case ASAlignmentRight: + return ASStackLayoutAlignItemsEnd; + case ASHorizontalAlignmentNone: + default: + return defaultAlignment; + } +} + +inline ASStackLayoutAlignItems alignment(ASVerticalAlignment alignment, ASStackLayoutAlignItems defaultAlignment) +{ + switch (alignment) { + case ASAlignmentTop: + return ASStackLayoutAlignItemsStart; + case ASAlignmentCenter: + return ASStackLayoutAlignItemsCenter; + case ASAlignmentBottom: + return ASStackLayoutAlignItemsEnd; + case ASVerticalAlignmentNone: + default: + return defaultAlignment; + } +} + +inline ASStackLayoutJustifyContent justifyContent(ASHorizontalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent) +{ + switch (alignment) { + case ASAlignmentLeft: + return ASStackLayoutJustifyContentStart; + case ASAlignmentMiddle: + return ASStackLayoutJustifyContentCenter; + case ASAlignmentRight: + return ASStackLayoutJustifyContentEnd; + case ASHorizontalAlignmentNone: + default: + return defaultJustifyContent; + } +} + +inline ASStackLayoutJustifyContent justifyContent(ASVerticalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent) +{ + switch (alignment) { + case ASAlignmentTop: + return ASStackLayoutJustifyContentStart; + case ASAlignmentCenter: + return ASStackLayoutJustifyContentCenter; + case ASAlignmentBottom: + return ASStackLayoutJustifyContentEnd; + case ASVerticalAlignmentNone: + default: + return defaultJustifyContent; + } +} diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index 2252078da6..5c3733e707 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -28,6 +28,8 @@ - (void)setUp self.recordMode = NO; } +#pragma mark - Utility methods + static NSArray *defaultSubnodes() { return defaultSubnodesWithSameSize(CGSizeZero, NO); @@ -63,6 +65,24 @@ - (void)testStackLayoutSpecWithJustify:(ASStackLayoutJustifyContent)justify [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; } +- (void)testStackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + itemsHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment + itemsVerticalAlignment:(ASVerticalAlignment)verticalAlignment + identifier:(NSString *)identifier +{ + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, NO); + + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; + stackLayoutSpec.direction = direction; + stackLayoutSpec.children = subnodes; + [stackLayoutSpec setHorizontalAlignment:horizontalAlignment]; + [stackLayoutSpec setVerticalAlignment:verticalAlignment]; + + CGSize exactSize = CGSizeMake(200, 200); + static ASSizeRange kSize = ASSizeRangeMake(exactSize, exactSize); + [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:subnodes identifier:identifier]; +} + - (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style sizeRange:(ASSizeRange)sizeRange subnodes:(NSArray *)subnodes @@ -76,13 +96,23 @@ - (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style sizeRange:(ASSizeRange)sizeRange subnodes:(NSArray *)subnodes identifier:(NSString *)identifier +{ + ASStackLayoutSpec *stackLayoutSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:style.direction + spacing:style.spacing + justifyContent:style.justifyContent + alignItems:style.alignItems + children:children]; + [self testStackLayoutSpec:stackLayoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +- (void)testStackLayoutSpec:(ASStackLayoutSpec *)stackLayoutSpec + sizeRange:(ASSizeRange)sizeRange + subnodes:(NSArray *)subnodes + identifier:(NSString *)identifier { ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]); - ASLayoutSpec *layoutSpec = - [ASBackgroundLayoutSpec - backgroundLayoutSpecWithChild:[ASStackLayoutSpec stackLayoutSpecWithDirection:style.direction spacing:style.spacing justifyContent:style.justifyContent alignItems:style.alignItems children:children] - background:backgroundNode]; + ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:stackLayoutSpec background:backgroundNode]; NSMutableArray *newSubnodes = [NSMutableArray arrayWithObject:backgroundNode]; [newSubnodes addObjectsFromArray:subnodes]; @@ -90,6 +120,8 @@ - (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:newSubnodes identifier:identifier]; } +#pragma mark - + - (void)testUnderflowBehaviors { // width 300px; height 0-300px @@ -522,4 +554,54 @@ - (void)testViolationIsDistributedEquallyAmongFlexibleChildren [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; } +- (void)testHorizontalAndVerticalAlignments +{ + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentLeft itemsVerticalAlignment:ASAlignmentTop identifier:@"horizontalTopLeft"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentMiddle itemsVerticalAlignment:ASAlignmentCenter identifier:@"horizontalCenter"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentRight itemsVerticalAlignment:ASAlignmentBottom identifier:@"horizontalBottomRight"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentLeft itemsVerticalAlignment:ASAlignmentTop identifier:@"verticalTopLeft"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentMiddle itemsVerticalAlignment:ASAlignmentCenter identifier:@"verticalCenter"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentRight itemsVerticalAlignment:ASAlignmentBottom identifier:@"verticalBottomRight"]; +} + +- (void)testDirectionChangeAfterSettingHorizontalAndVerticalAlignments +{ + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; // Default direction is horizontal + stackLayoutSpec.horizontalAlignment = ASAlignmentRight; + stackLayoutSpec.verticalAlignment = ASAlignmentCenter; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd); + + stackLayoutSpec.direction = ASStackLayoutDirectionVertical; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter); +} + +- (void)testAlignItemsAndJustifyContentRestrictionsIfHorizontalAndVerticalAlignmentsAreUsed +{ + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; + + // No assertions should be thrown here because alignments are not used + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd; + + // Set alignments and assert that assertions are thrown + stackLayoutSpec.horizontalAlignment = ASAlignmentMiddle; + stackLayoutSpec.verticalAlignment = ASAlignmentCenter; + XCTAssertThrows(stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd); + XCTAssertThrows(stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd); + + // Unset alignments. alignItems and justifyContent should not be changed + stackLayoutSpec.horizontalAlignment = ASHorizontalAlignmentNone; + stackLayoutSpec.verticalAlignment = ASVerticalAlignmentNone; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter); + + // Now that alignments are none, setting alignItems and justifyContent should be allowed again + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd); +} + @end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png new file mode 100644 index 0000000000..489764fe6e Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png new file mode 100644 index 0000000000..eb0ea8c3da Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png new file mode 100644 index 0000000000..8864402768 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png new file mode 100644 index 0000000000..4e46d0f9dd Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png new file mode 100644 index 0000000000..a0b412886f Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png new file mode 100644 index 0000000000..8277b97c00 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png differ diff --git a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj index 9e3d98c2b5..56aef5f7f6 100644 --- a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FDEC911BF31EE700CEB123 /* ItemNode.m */; }; 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */; }; 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; }; AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; @@ -17,6 +18,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 25FDEC901BF31EE700CEB123 /* ItemNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ItemNode.h; sourceTree = ""; }; + 25FDEC911BF31EE700CEB123 /* ItemNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ItemNode.m; sourceTree = ""; }; 2DBAEE96397BB913350C4530 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SupplementaryNode.h; sourceTree = ""; }; 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SupplementaryNode.m; sourceTree = ""; }; @@ -83,6 +86,8 @@ AC3C4A611A11F47200143C57 /* Supporting Files */, 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */, 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */, + 25FDEC901BF31EE700CEB123 /* ItemNode.h */, + 25FDEC911BF31EE700CEB123 /* ItemNode.m */, ); indentWidth = 2; path = Sample; @@ -228,6 +233,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */, AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */, AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, diff --git a/examples/ASCollectionView/Sample/ItemNode.h b/examples/ASCollectionView/Sample/ItemNode.h new file mode 100644 index 0000000000..d95f2fa4d2 --- /dev/null +++ b/examples/ASCollectionView/Sample/ItemNode.h @@ -0,0 +1,18 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface ItemNode : ASTextCellNode + +- (instancetype)initWithString:(NSString *)string; + +@end diff --git a/examples/ASCollectionView/Sample/ItemNode.m b/examples/ASCollectionView/Sample/ItemNode.m new file mode 100644 index 0000000000..1a5f5ce40a --- /dev/null +++ b/examples/ASCollectionView/Sample/ItemNode.m @@ -0,0 +1,49 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "ItemNode.h" + +@implementation ItemNode + +- (instancetype)initWithString:(NSString *)string +{ + self = [super init]; + if (self != nil) { + self.text = string; + [self updateBackgroundColor]; + } + return self; +} + +- (void)updateBackgroundColor +{ + if (self.highlighted) { + self.backgroundColor = [UIColor grayColor]; + } else if (self.selected) { + self.backgroundColor = [UIColor darkGrayColor]; + } else { + self.backgroundColor = [UIColor lightGrayColor]; + } +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [self updateBackgroundColor]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [self updateBackgroundColor]; +} + +@end diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 4f337ce1d1..a9d3e12a7b 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -13,6 +13,7 @@ #import #import "SupplementaryNode.h" +#import "ItemNode.h" @interface ViewController () { @@ -78,11 +79,7 @@ - (void)reloadTapped - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath { NSString *text = [NSString stringWithFormat:@"[%zd.%zd] says hi", indexPath.section, indexPath.item]; - ASTextCellNode *node = [[ASTextCellNode alloc] init]; - node.text = text; - node.backgroundColor = [UIColor lightGrayColor]; - - return node; + return [[ItemNode alloc] initWithString:text]; } - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath diff --git a/examples/Kittens/Sample/KittenNode.mm b/examples/Kittens/Sample/KittenNode.mm index adc656aa22..2b3800601e 100644 --- a/examples/Kittens/Sample/KittenNode.mm +++ b/examples/Kittens/Sample/KittenNode.mm @@ -204,4 +204,27 @@ - (void)toggleNodesSwap }]; } +- (void)updateBackgroundColor +{ + if (self.highlighted) { + self.backgroundColor = [UIColor lightGrayColor]; + } else if (self.selected) { + self.backgroundColor = [UIColor blueColor]; + } else { + self.backgroundColor = [UIColor whiteColor]; + } +} + +- (void)setSelected:(BOOL)selected +{ + [super setSelected:selected]; + [self updateBackgroundColor]; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + [super setHighlighted:highlighted]; + [self updateBackgroundColor]; +} + @end diff --git a/examples/SocialAppLayout/Sample/PostNode.m b/examples/SocialAppLayout/Sample/PostNode.m index 9992bb6ebb..b7f2a9c961 100644 --- a/examples/SocialAppLayout/Sample/PostNode.m +++ b/examples/SocialAppLayout/Sample/PostNode.m @@ -14,6 +14,9 @@ #import "LikesNode.h" #import "CommentsNode.h" +@interface PostNode() +@end + @implementation PostNode - (instancetype)initWithPost:(Post *)post { @@ -90,6 +93,7 @@ - (instancetype)initWithPost:(Post *)post { _mediaNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); _mediaNode.cornerRadius = 4.0; _mediaNode.URL = [NSURL URLWithString:_post.media]; + _mediaNode.delegate = self; _mediaNode.imageModificationBlock = ^UIImage *(UIImage *image) { UIImage *modifiedImage; @@ -258,4 +262,12 @@ - (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attr [[UIApplication sharedApplication] openURL:URL]; } +#pragma mark - +#pragma mark ASNetworkImageNodeDelegate methods. + +- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image +{ + [self setNeedsLayout]; +} + @end diff --git a/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj b/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj index 038f1b0bf6..2be08f1dcd 100644 --- a/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj +++ b/examples/VerticalWithinHorizontalScrolling/Sample.xcodeproj/project.pbxproj @@ -266,7 +266,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -301,7 +301,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES;