Skip to content

Commit 34f1621

Browse files
authored
Add layer-action support to nodes (#1396)
* Add layer-action support to nodes, unify hierarchy notifications on it * Better pending state * Fix bool * Skip extra copy * Never run default actions * Continue the search
1 parent 9d77ef9 commit 34f1621

10 files changed

+83
-90
lines changed

Source/ASDisplayNode+Subclasses.h

+7
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,13 @@ AS_CATEGORY_IMPLEMENTABLE
376376
*/
377377
@property (readonly) CGFloat contentsScaleForDisplay;
378378

379+
/**
380+
* Called as part of actionForLayer:forKey:. Gives the node a chance to provide a custom action for its layer.
381+
*
382+
* The default implementation returns NSNull, indicating that no action should be taken.
383+
*/
384+
AS_CATEGORY_IMPLEMENTABLE
385+
- (nullable id<CAAction>)layerActionForKey:(NSString *)event;
379386

380387
#pragma mark - Touch handling
381388
/** @name Touch handling */

Source/ASDisplayNode.h

+2
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,8 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority;
686686
@property (getter=isExclusiveTouch) BOOL exclusiveTouch; // default=NO
687687
#endif
688688

689+
@property (nullable, copy) NSDictionary<NSString *, id<CAAction>> *actions; // default = nil
690+
689691
/**
690692
* @abstract The node view's background color.
691693
*

Source/ASDisplayNode.mm

+8-11
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,7 @@
6464

6565
static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil;
6666

67-
// Forward declare CALayerDelegate protocol as the iOS 10 SDK moves CALayerDelegate from an informal delegate to a protocol.
68-
// We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10
69-
@protocol CALayerDelegate;
70-
71-
@interface ASDisplayNode () <UIGestureRecognizerDelegate, CALayerDelegate, _ASDisplayLayerDelegate, ASCATransactionQueueObserving>
67+
@interface ASDisplayNode () <UIGestureRecognizerDelegate, _ASDisplayLayerDelegate, ASCATransactionQueueObserving>
7268
/**
7369
* See ASDisplayNodeInternal.h for ivars
7470
*/
@@ -107,9 +103,10 @@ BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLaye
107103
return result;
108104
}
109105

110-
void StubImplementationWithNoArgs(id receiver) {}
111-
void StubImplementationWithSizeRange(id receiver, ASSizeRange sr) {}
112-
void StubImplementationWithTwoInterfaceStates(id receiver, ASInterfaceState s0, ASInterfaceState s1) {}
106+
void StubImplementationWithNoArgs(id receiver, SEL _cmd) {}
107+
void StubImplementationWithSizeRange(id receiver, SEL _cmd, ASSizeRange sr) {}
108+
void StubImplementationWithTwoInterfaceStates(id receiver, SEL _cmd, ASInterfaceState s0, ASInterfaceState s1) {}
109+
id StubLayerActionImplementation(id receiver, SEL _cmd, NSString *key) { return (id)kCFNull; }
113110

114111
/**
115112
* Returns ASDisplayNodeFlags for the given class/instance. instance MAY BE NIL.
@@ -281,6 +278,8 @@ + (void)initialize
281278
auto interfaceStateType = std::string(@encode(ASInterfaceState));
282279
auto type1 = "v@:" + interfaceStateType + interfaceStateType;
283280
class_addMethod(self, @selector(interfaceStateDidChange:fromState:), (IMP)StubImplementationWithTwoInterfaceStates, type1.c_str());
281+
282+
class_addMethod(self, @selector(layerActionForKey:), (IMP)StubLayerActionImplementation, "@@:@");
284283
}
285284
}
286285

@@ -1804,7 +1803,6 @@ - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode
18041803

18051804
#pragma mark <CALayerDelegate>
18061805

1807-
// We are only the delegate for the layer when we are layer-backed, as UIView performs this function normally
18081806
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
18091807
{
18101808
if (event == kCAOnOrderIn) {
@@ -1813,8 +1811,7 @@ - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode
18131811
[self __exitHierarchy];
18141812
}
18151813

1816-
ASDisplayNodeAssert(_flags.layerBacked, @"We shouldn't get called back here unless we are layer-backed.");
1817-
return (id)kCFNull;
1814+
return [self layerActionForKey:event];
18181815
}
18191816

18201817
#pragma mark - Error Handling

Source/Details/UIView+ASConvenience.h

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
4242
@property (nonatomic) BOOL allowsGroupOpacity;
4343
@property (nonatomic) BOOL allowsEdgeAntialiasing;
4444
@property (nonatomic) unsigned int edgeAntialiasingMask;
45+
@property (nonatomic, nullable, copy) NSDictionary<NSString *, id<CAAction>> *actions;
4546

4647
- (void)setNeedsDisplay;
4748
- (void)setNeedsLayout;

Source/Details/_ASDisplayLayer.mm

+7
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ - (void)setNeedsDisplay
115115

116116
#pragma mark -
117117

118+
+ (id<CAAction>)defaultActionForKey:(NSString *)event
119+
{
120+
// We never want to run one of CA's root default actions. So if we return nil from actionForLayer:forKey:, and let CA
121+
// dig into the actions dictionary, and it doesn't find it there, it will check here and we need to stop the search.
122+
return (id)kCFNull;
123+
}
124+
118125
+ (dispatch_queue_t)displayQueue
119126
{
120127
static dispatch_queue_t displayQueue = NULL;

Source/Details/_ASDisplayView.mm

+10-13
Original file line numberDiff line numberDiff line change
@@ -153,22 +153,19 @@ - (NSString *)description
153153

154154
#pragma mark - UIView Overrides
155155

156-
- (void)willMoveToWindow:(UIWindow *)newWindow
156+
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
157157
{
158-
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
159-
BOOL visible = (newWindow != nil);
160-
if (visible && !node.inHierarchy) {
161-
[node __enterHierarchy];
162-
}
163-
}
158+
id<CAAction> uikitAction = [super actionForLayer:layer forKey:event];
164159

165-
- (void)didMoveToWindow
166-
{
167-
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
168-
BOOL visible = (self.window != nil);
169-
if (!visible && node.inHierarchy) {
170-
[node __exitHierarchy];
160+
// Even though the UIKit action will take precedence, we still unconditionally forward to the node so that it can
161+
// track events like kCAOnOrderIn.
162+
id<CAAction> nodeAction = [_asyncdisplaykit_node actionForLayer:layer forKey:event];
163+
164+
// If UIKit specifies an action, that takes precedence. That's an animation block so it's explicit.
165+
if (uikitAction && uikitAction != (id)kCFNull) {
166+
return uikitAction;
171167
}
168+
return nodeAction;
172169
}
173170

174171
- (void)willMoveToSuperview:(UIView *)newSuperview

Source/Private/ASDisplayNode+UIViewBridge.mm

+12
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,18 @@ - (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea
944944
}
945945
}
946946

947+
- (NSDictionary<NSString *,id<CAAction>> *)actions
948+
{
949+
_bridge_prologue_read;
950+
return _getFromLayer(actions);
951+
}
952+
953+
- (void)setActions:(NSDictionary<NSString *,id<CAAction>> *)actions
954+
{
955+
_bridge_prologue_write;
956+
_setToLayer(actions, actions);
957+
}
958+
947959
- (void)safeAreaInsetsDidChange
948960
{
949961
ASDisplayNodeAssertMainThread();

Source/Private/ASDisplayNodeInternal.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest
7676

7777
#define NUM_CLIP_CORNER_LAYERS 4
7878

79-
@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate>
79+
@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate, CALayerDelegate>
8080
{
8181
@package
8282
AS::RecursiveMutex __instanceLock__;

Source/Private/_ASPendingState.mm

+21-65
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,12 @@
8686
int setLayoutMargins:1;
8787
int setPreservesSuperviewLayoutMargins:1;
8888
int setInsetsLayoutMarginsFromSafeArea:1;
89+
int setActions:1;
8990
} ASPendingStateFlags;
9091

92+
93+
static constexpr ASPendingStateFlags kZeroFlags = {0};
94+
9195
@implementation _ASPendingState
9296
{
9397
@package //Expose all ivars for ASDisplayNode to bypass getters for efficiency
@@ -140,6 +144,7 @@ @implementation _ASPendingState
140144
CGPoint accessibilityActivationPoint;
141145
UIBezierPath *accessibilityPath;
142146
UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0));
147+
NSDictionary<NSString *, id<CAAction>> *actions;
143148

144149
ASPendingStateFlags _flags;
145150
}
@@ -209,6 +214,7 @@ ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *sta
209214
@synthesize layoutMargins=layoutMargins;
210215
@synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins;
211216
@synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea;
217+
@synthesize actions=actions;
212218

213219
static CGColorRef blackColorRef = NULL;
214220
static UIColor *defaultTintColor = nil;
@@ -586,6 +592,12 @@ - (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AV
586592
_flags.setSemanticContentAttribute = YES;
587593
}
588594

595+
- (void)setActions:(NSDictionary<NSString *,id<CAAction>> *)actionsArg
596+
{
597+
actions = [actionsArg copy];
598+
_flags.setActions = YES;
599+
}
600+
589601
- (BOOL)isAccessibilityElement
590602
{
591603
return isAccessibilityElement;
@@ -917,6 +929,9 @@ - (void)applyToLayer:(CALayer *)layer
917929
if (flags.setOpaque)
918930
ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired");
919931

932+
if (flags.setActions)
933+
layer.actions = actions;
934+
920935
ASPendingStateApplyMetricsToLayer(self, layer);
921936

922937
if (flags.needsLayout)
@@ -936,7 +951,7 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr
936951
because a different setter would be called.
937952
*/
938953

939-
CALayer *layer = view.layer;
954+
unowned CALayer *layer = view.layer;
940955

941956
ASPendingStateFlags flags = _flags;
942957
if (__shouldSetNeedsDisplay(layer)) {
@@ -979,6 +994,9 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr
979994
if (flags.setRasterizationScale)
980995
layer.rasterizationScale = rasterizationScale;
981996

997+
if (flags.setActions)
998+
layer.actions = actions;
999+
9821000
if (flags.setClipsToBounds)
9831001
view.clipsToBounds = clipsToBounds;
9841002

@@ -1272,7 +1290,7 @@ + (_ASPendingState *)pendingViewStateFromView:(UIView *)view
12721290

12731291
- (void)clearChanges
12741292
{
1275-
_flags = (ASPendingStateFlags){ 0 };
1293+
_flags = kZeroFlags;
12761294
}
12771295

12781296
- (BOOL)hasSetNeedsLayout
@@ -1287,69 +1305,7 @@ - (BOOL)hasSetNeedsDisplay
12871305

12881306
- (BOOL)hasChanges
12891307
{
1290-
ASPendingStateFlags flags = _flags;
1291-
1292-
return (flags.setAnchorPoint
1293-
|| flags.setPosition
1294-
|| flags.setZPosition
1295-
|| flags.setFrame
1296-
|| flags.setBounds
1297-
|| flags.setPosition
1298-
|| flags.setTransform
1299-
|| flags.setSublayerTransform
1300-
|| flags.setContents
1301-
|| flags.setContentsGravity
1302-
|| flags.setContentsRect
1303-
|| flags.setContentsCenter
1304-
|| flags.setContentsScale
1305-
|| flags.setRasterizationScale
1306-
|| flags.setClipsToBounds
1307-
|| flags.setBackgroundColor
1308-
|| flags.setTintColor
1309-
|| flags.setHidden
1310-
|| flags.setAlpha
1311-
|| flags.setCornerRadius
1312-
|| flags.setContentMode
1313-
|| flags.setUserInteractionEnabled
1314-
|| flags.setExclusiveTouch
1315-
|| flags.setShadowOpacity
1316-
|| flags.setShadowOffset
1317-
|| flags.setShadowRadius
1318-
|| flags.setShadowColor
1319-
|| flags.setBorderWidth
1320-
|| flags.setBorderColor
1321-
|| flags.setAutoresizingMask
1322-
|| flags.setAutoresizesSubviews
1323-
|| flags.setNeedsDisplayOnBoundsChange
1324-
|| flags.setAllowsGroupOpacity
1325-
|| flags.setAllowsEdgeAntialiasing
1326-
|| flags.setEdgeAntialiasingMask
1327-
|| flags.needsDisplay
1328-
|| flags.needsLayout
1329-
|| flags.setAsyncTransactionContainer
1330-
|| flags.setOpaque
1331-
|| flags.setSemanticContentAttribute
1332-
|| flags.setLayoutMargins
1333-
|| flags.setPreservesSuperviewLayoutMargins
1334-
|| flags.setInsetsLayoutMarginsFromSafeArea
1335-
|| flags.setIsAccessibilityElement
1336-
|| flags.setAccessibilityLabel
1337-
|| flags.setAccessibilityAttributedLabel
1338-
|| flags.setAccessibilityHint
1339-
|| flags.setAccessibilityAttributedHint
1340-
|| flags.setAccessibilityValue
1341-
|| flags.setAccessibilityAttributedValue
1342-
|| flags.setAccessibilityTraits
1343-
|| flags.setAccessibilityFrame
1344-
|| flags.setAccessibilityLanguage
1345-
|| flags.setAccessibilityElementsHidden
1346-
|| flags.setAccessibilityViewIsModal
1347-
|| flags.setShouldGroupAccessibilityChildren
1348-
|| flags.setAccessibilityIdentifier
1349-
|| flags.setAccessibilityNavigationStyle
1350-
|| flags.setAccessibilityHeaderElements
1351-
|| flags.setAccessibilityActivationPoint
1352-
|| flags.setAccessibilityPath);
1308+
return memcmp(&_flags, &kZeroFlags, sizeof(ASPendingStateFlags));
13531309
}
13541310

13551311
- (void)dealloc

Tests/ASDisplayNodeTests.mm

+14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#import <QuartzCore/QuartzCore.h>
1111
#import <XCTest/XCTest.h>
12+
#import <OCMock/OCMock.h>
1213

1314
#import <AsyncDisplayKit/_ASDisplayLayer.h>
1415
#import <AsyncDisplayKit/_ASDisplayView.h>
@@ -2697,4 +2698,17 @@ - (void)testCornerRoundingTypeClippingRoundedCornersIsUsingASDisplayNodeCornerLa
26972698
}
26982699
}
26992700

2701+
- (void)testLayerActionForKeyIsCalled
2702+
{
2703+
UIWindow *window = [[UIWindow alloc] init];
2704+
ASDisplayNode *node = [[ASDisplayNode alloc] init];
2705+
2706+
id mockNode = OCMPartialMock(node);
2707+
OCMExpect([mockNode layerActionForKey:kCAOnOrderIn]);
2708+
[window.layer addSublayer:node.layer];
2709+
OCMExpect([mockNode layerActionForKey:@"position"]);
2710+
node.layer.position = CGPointMake(10, 10);
2711+
OCMVerifyAll(mockNode);
2712+
}
2713+
27002714
@end

0 commit comments

Comments
 (0)