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/_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; diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 0bf1514e3..9d0af059c 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -153,22 +153,19 @@ - (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]; -- (void)didMoveToWindow -{ - ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - BOOL visible = (self.window != nil); - if (!visible && node.inHierarchy) { - [node __exitHierarchy]; + // 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 = [_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) { + return uikitAction; } + return nodeAction; } - (void)willMoveToSuperview:(UIView *)newSuperview diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index ebc63071c..4f7e93140 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); +} + - (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..5dca0ce5c 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -86,8 +86,12 @@ int setLayoutMargins:1; int setPreservesSuperviewLayoutMargins:1; int setInsetsLayoutMarginsFromSafeArea:1; + int setActions:1; } ASPendingStateFlags; + +static constexpr ASPendingStateFlags kZeroFlags = {0}; + @implementation _ASPendingState { @package //Expose all ivars for ASDisplayNode to bypass getters for efficiency @@ -140,6 +144,7 @@ @implementation _ASPendingState CGPoint accessibilityActivationPoint; UIBezierPath *accessibilityPath; UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); + NSDictionary> *actions; ASPendingStateFlags _flags; } @@ -209,6 +214,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 +592,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 +929,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) @@ -936,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)) { @@ -979,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; @@ -1272,7 +1290,7 @@ + (_ASPendingState *)pendingViewStateFromView:(UIView *)view - (void)clearChanges { - _flags = (ASPendingStateFlags){ 0 }; + _flags = kZeroFlags; } - (BOOL)hasSetNeedsLayout @@ -1287,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 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