From e84e61896ba312bd81ce0e05382cb4017eb89a4a Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Mon, 11 Mar 2019 15:38:32 -0700 Subject: [PATCH 1/6] Add layer-action support to nodes, unify hierarchy notifications on it --- Source/ASDisplayNode+Subclasses.h | 7 ++++++ Source/ASDisplayNode.h | 2 ++ Source/ASDisplayNode.mm | 19 +++++++--------- Source/Details/UIView+ASConvenience.h | 1 + Source/Details/_ASDisplayView.mm | 24 ++++++++++---------- Source/Private/ASDisplayNode+UIViewBridge.mm | 12 ++++++++++ Source/Private/ASDisplayNodeInternal.h | 2 +- Source/Private/_ASPendingState.mm | 12 ++++++++++ Tests/ASDisplayNodeTests.mm | 14 ++++++++++++ 9 files changed, 69 insertions(+), 24 deletions(-) diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h index 2b80f2b5c..df84b577a 100644 --- a/Source/ASDisplayNode+Subclasses.h +++ b/Source/ASDisplayNode+Subclasses.h @@ -376,6 +376,13 @@ AS_CATEGORY_IMPLEMENTABLE */ @property (readonly) CGFloat contentsScaleForDisplay; +/** + * Called as part of actionForLayer:forKey:. Gives the node a chance to provide a custom action for its layer. + * + * The default implementation returns NSNull, indicating that no action should be taken. + */ +AS_CATEGORY_IMPLEMENTABLE +- (nullable id)layerActionForKey:(NSString *)event; #pragma mark - Touch handling /** @name Touch handling */ diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index d3414b894..a885aa01c 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -686,6 +686,8 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority; @property (getter=isExclusiveTouch) BOOL exclusiveTouch; // default=NO #endif +@property (nullable, copy) NSDictionary> *actions; // default = nil + /** * @abstract The node view's background color. * diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index b4034f291..fc541178f 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -64,11 +64,7 @@ static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; -// Forward declare CALayerDelegate protocol as the iOS 10 SDK moves CALayerDelegate from an informal delegate to a protocol. -// We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 -@protocol CALayerDelegate; - -@interface ASDisplayNode () +@interface ASDisplayNode () /** * See ASDisplayNodeInternal.h for ivars */ @@ -107,9 +103,10 @@ BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLaye return result; } -void StubImplementationWithNoArgs(id receiver) {} -void StubImplementationWithSizeRange(id receiver, ASSizeRange sr) {} -void StubImplementationWithTwoInterfaceStates(id receiver, ASInterfaceState s0, ASInterfaceState s1) {} +void StubImplementationWithNoArgs(id receiver, SEL _cmd) {} +void StubImplementationWithSizeRange(id receiver, SEL _cmd, ASSizeRange sr) {} +void StubImplementationWithTwoInterfaceStates(id receiver, SEL _cmd, ASInterfaceState s0, ASInterfaceState s1) {} +id StubLayerActionImplementation(id receiver, SEL _cmd, NSString *key) { return (id)kCFNull; } /** * Returns ASDisplayNodeFlags for the given class/instance. instance MAY BE NIL. @@ -281,6 +278,8 @@ + (void)initialize auto interfaceStateType = std::string(@encode(ASInterfaceState)); auto type1 = "v@:" + interfaceStateType + interfaceStateType; class_addMethod(self, @selector(interfaceStateDidChange:fromState:), (IMP)StubImplementationWithTwoInterfaceStates, type1.c_str()); + + class_addMethod(self, @selector(layerActionForKey:), (IMP)StubLayerActionImplementation, "@@:@"); } } @@ -1804,7 +1803,6 @@ - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode #pragma mark -// We are only the delegate for the layer when we are layer-backed, as UIView performs this function normally - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { if (event == kCAOnOrderIn) { @@ -1813,8 +1811,7 @@ - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode [self __exitHierarchy]; } - ASDisplayNodeAssert(_flags.layerBacked, @"We shouldn't get called back here unless we are layer-backed."); - return (id)kCFNull; + return [self layerActionForKey:event]; } #pragma mark - Error Handling diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 30d58a07a..8c4b2f55c 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL allowsGroupOpacity; @property (nonatomic) BOOL allowsEdgeAntialiasing; @property (nonatomic) unsigned int edgeAntialiasingMask; +@property (nonatomic, nullable, copy) NSDictionary> *actions; - (void)setNeedsDisplay; - (void)setNeedsLayout; diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 0bf1514e3..d692cd5f4 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -153,22 +153,22 @@ - (NSString *)description #pragma mark - UIView Overrides -- (void)willMoveToWindow:(UIWindow *)newWindow +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { - ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - BOOL visible = (newWindow != nil); - if (visible && !node.inHierarchy) { - [node __enterHierarchy]; + id uikitAction = [super actionForLayer:layer forKey:event]; + + // Even though the UIKit action will take precedence, we still unconditionally forward to the node so that it can + // track events like kCAOnOrderIn. + id nodeAction = (id)kCFNull; + if (ASDisplayNode *node = _asyncdisplaykit_node) { + nodeAction = [node actionForLayer:layer forKey:event]; } -} -- (void)didMoveToWindow -{ - ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - BOOL visible = (self.window != nil); - if (!visible && node.inHierarchy) { - [node __exitHierarchy]; + // If UIKit specifies an action, that takes precedence. That's an animation block so it's explicit. + if (uikitAction && uikitAction != (id)kCFNull) { + return uikitAction; } + return nodeAction; } - (void)willMoveToSuperview:(UIView *)newSuperview diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index ebc63071c..e52cdde74 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -944,6 +944,18 @@ - (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea } } +- (NSDictionary> *)actions +{ + _bridge_prologue_read; + return _getFromLayer(actions); +} + +- (void)setActions:(NSDictionary> *)actions +{ + _bridge_prologue_write; + _setToLayer(actions, [actions copy]); +} + - (void)safeAreaInsetsDidChange { ASDisplayNodeAssertMainThread(); diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 8cc49441e..55cef7579 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -76,7 +76,7 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest #define NUM_CLIP_CORNER_LAYERS 4 -@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate> +@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate, CALayerDelegate> { @package AS::RecursiveMutex __instanceLock__; diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 784d43550..cf575e7b7 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -86,6 +86,7 @@ int setLayoutMargins:1; int setPreservesSuperviewLayoutMargins:1; int setInsetsLayoutMarginsFromSafeArea:1; + int setActions:1; } ASPendingStateFlags; @implementation _ASPendingState @@ -140,6 +141,7 @@ @implementation _ASPendingState CGPoint accessibilityActivationPoint; UIBezierPath *accessibilityPath; UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); + NSDictionary> *actions; ASPendingStateFlags _flags; } @@ -209,6 +211,7 @@ ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *sta @synthesize layoutMargins=layoutMargins; @synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins; @synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea; +@synthesize actions=actions; static CGColorRef blackColorRef = NULL; static UIColor *defaultTintColor = nil; @@ -586,6 +589,12 @@ - (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AV _flags.setSemanticContentAttribute = YES; } +- (void)setActions:(NSDictionary> *)actionsArg +{ + actions = [actionsArg copy]; + _flags.setActions = YES; +} + - (BOOL)isAccessibilityElement { return isAccessibilityElement; @@ -917,6 +926,9 @@ - (void)applyToLayer:(CALayer *)layer if (flags.setOpaque) ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + if (flags.setActions) + layer.actions = actions; + ASPendingStateApplyMetricsToLayer(self, layer); if (flags.needsLayout) diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index f9ae561cb..e47646e33 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -9,6 +9,7 @@ #import #import +#import #import #import @@ -2697,4 +2698,17 @@ - (void)testCornerRoundingTypeClippingRoundedCornersIsUsingASDisplayNodeCornerLa } } +- (void)testLayerActionForKeyIsCalled +{ + UIWindow *window = [[UIWindow alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + + id mockNode = OCMPartialMock(node); + OCMExpect([mockNode layerActionForKey:kCAOnOrderIn]); + [window.layer addSublayer:node.layer]; + OCMExpect([mockNode layerActionForKey:@"position"]); + node.layer.position = CGPointMake(10, 10); + OCMVerifyAll(mockNode); +} + @end From 3fda6594afa7e35d3baaaf13459ed8b609b5929c Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 12 Mar 2019 11:04:21 -0700 Subject: [PATCH 2/6] Better pending state --- Source/Private/_ASPendingState.mm | 74 ++++--------------------------- 1 file changed, 9 insertions(+), 65 deletions(-) diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index cf575e7b7..4fc56f6b5 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -89,6 +89,9 @@ int setActions:1; } ASPendingStateFlags; + +static constexpr ASPendingStateFlags kZeroFlags = {0}; + @implementation _ASPendingState { @package //Expose all ivars for ASDisplayNode to bypass getters for efficiency @@ -948,7 +951,7 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr because a different setter would be called. */ - CALayer *layer = view.layer; + unowned CALayer *layer = view.layer; ASPendingStateFlags flags = _flags; if (__shouldSetNeedsDisplay(layer)) { @@ -991,6 +994,9 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr if (flags.setRasterizationScale) layer.rasterizationScale = rasterizationScale; + if (flags.setActions) + layer.actions = actions; + if (flags.setClipsToBounds) view.clipsToBounds = clipsToBounds; @@ -1284,7 +1290,7 @@ + (_ASPendingState *)pendingViewStateFromView:(UIView *)view - (void)clearChanges { - _flags = (ASPendingStateFlags){ 0 }; + _flags = kZeroFlags; } - (BOOL)hasSetNeedsLayout @@ -1299,69 +1305,7 @@ - (BOOL)hasSetNeedsDisplay - (BOOL)hasChanges { - ASPendingStateFlags flags = _flags; - - return (flags.setAnchorPoint - || flags.setPosition - || flags.setZPosition - || flags.setFrame - || flags.setBounds - || flags.setPosition - || flags.setTransform - || flags.setSublayerTransform - || flags.setContents - || flags.setContentsGravity - || flags.setContentsRect - || flags.setContentsCenter - || flags.setContentsScale - || flags.setRasterizationScale - || flags.setClipsToBounds - || flags.setBackgroundColor - || flags.setTintColor - || flags.setHidden - || flags.setAlpha - || flags.setCornerRadius - || flags.setContentMode - || flags.setUserInteractionEnabled - || flags.setExclusiveTouch - || flags.setShadowOpacity - || flags.setShadowOffset - || flags.setShadowRadius - || flags.setShadowColor - || flags.setBorderWidth - || flags.setBorderColor - || flags.setAutoresizingMask - || flags.setAutoresizesSubviews - || flags.setNeedsDisplayOnBoundsChange - || flags.setAllowsGroupOpacity - || flags.setAllowsEdgeAntialiasing - || flags.setEdgeAntialiasingMask - || flags.needsDisplay - || flags.needsLayout - || flags.setAsyncTransactionContainer - || flags.setOpaque - || flags.setSemanticContentAttribute - || flags.setLayoutMargins - || flags.setPreservesSuperviewLayoutMargins - || flags.setInsetsLayoutMarginsFromSafeArea - || flags.setIsAccessibilityElement - || flags.setAccessibilityLabel - || flags.setAccessibilityAttributedLabel - || flags.setAccessibilityHint - || flags.setAccessibilityAttributedHint - || flags.setAccessibilityValue - || flags.setAccessibilityAttributedValue - || flags.setAccessibilityTraits - || flags.setAccessibilityFrame - || flags.setAccessibilityLanguage - || flags.setAccessibilityElementsHidden - || flags.setAccessibilityViewIsModal - || flags.setShouldGroupAccessibilityChildren - || flags.setAccessibilityIdentifier - || flags.setAccessibilityNavigationStyle - || flags.setAccessibilityHeaderElements - || flags.setAccessibilityActivationPoint - || flags.setAccessibilityPath); + return !memcmp(&_flags, &kZeroFlags, sizeof(ASPendingStateFlags)); } - (void)dealloc From 8d5bbfe27417d681ede07b125b2d502904eab7e1 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 12 Mar 2019 11:31:25 -0700 Subject: [PATCH 3/6] Fix bool --- Source/Private/_ASPendingState.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 4fc56f6b5..5dca0ce5c 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -1305,7 +1305,7 @@ - (BOOL)hasSetNeedsDisplay - (BOOL)hasChanges { - return !memcmp(&_flags, &kZeroFlags, sizeof(ASPendingStateFlags)); + return memcmp(&_flags, &kZeroFlags, sizeof(ASPendingStateFlags)); } - (void)dealloc From 4726d3e71c65a9c397815ffb97ebc4677f1341a5 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 12 Mar 2019 12:58:31 -0700 Subject: [PATCH 4/6] Skip extra copy --- Source/Private/ASDisplayNode+UIViewBridge.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index e52cdde74..4f7e93140 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -953,7 +953,7 @@ - (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea - (void)setActions:(NSDictionary> *)actions { _bridge_prologue_write; - _setToLayer(actions, [actions copy]); + _setToLayer(actions, actions); } - (void)safeAreaInsetsDidChange From 90aaff6fa1b9b90c1a5be74a47aafb9902ec219f Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 12 Mar 2019 16:07:04 -0700 Subject: [PATCH 5/6] Never run default actions --- Source/Details/_ASDisplayLayer.mm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Details/_ASDisplayLayer.mm b/Source/Details/_ASDisplayLayer.mm index 96eda9e1f..6ced31685 100644 --- a/Source/Details/_ASDisplayLayer.mm +++ b/Source/Details/_ASDisplayLayer.mm @@ -115,6 +115,13 @@ - (void)setNeedsDisplay #pragma mark - ++ (id)defaultActionForKey:(NSString *)event +{ + // We never want to run one of CA's root default actions. So if we return nil from actionForLayer:forKey:, and let CA + // dig into the actions dictionary, and it doesn't find it there, it will check here and we need to stop the search. + return (id)kCFNull; +} + + (dispatch_queue_t)displayQueue { static dispatch_queue_t displayQueue = NULL; From 272d294a790b38b7181fbe8f0798dac7b609ff22 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 13 Mar 2019 09:48:32 -0700 Subject: [PATCH 6/6] Continue the search --- Source/Details/_ASDisplayView.mm | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index d692cd5f4..9d0af059c 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -159,10 +159,7 @@ - (NSString *)description // Even though the UIKit action will take precedence, we still unconditionally forward to the node so that it can // track events like kCAOnOrderIn. - id nodeAction = (id)kCFNull; - if (ASDisplayNode *node = _asyncdisplaykit_node) { - nodeAction = [node actionForLayer:layer forKey:event]; - } + id nodeAction = [_asyncdisplaykit_node actionForLayer:layer forKey:event]; // If UIKit specifies an action, that takes precedence. That's an animation block so it's explicit. if (uikitAction && uikitAction != (id)kCFNull) {