Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add layer-action support to nodes #1396

Merged
merged 6 commits into from
Mar 13, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Source/ASDisplayNode+Subclasses.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<CAAction>)layerActionForKey:(NSString *)event;

#pragma mark - Touch handling
/** @name Touch handling */
Expand Down
2 changes: 2 additions & 0 deletions Source/ASDisplayNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,8 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority;
@property (getter=isExclusiveTouch) BOOL exclusiveTouch; // default=NO
#endif

@property (nullable, copy) NSDictionary<NSString *, id<CAAction>> *actions; // default = nil

/**
* @abstract The node view's background color.
*
Expand Down
19 changes: 8 additions & 11 deletions Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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 () <UIGestureRecognizerDelegate, CALayerDelegate, _ASDisplayLayerDelegate, ASCATransactionQueueObserving>
@interface ASDisplayNode () <UIGestureRecognizerDelegate, _ASDisplayLayerDelegate, ASCATransactionQueueObserving>
/**
* See ASDisplayNodeInternal.h for ivars
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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, "@@:@");
}
}

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

#pragma mark <CALayerDelegate>

// We are only the delegate for the layer when we are layer-backed, as UIView performs this function normally
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
if (event == kCAOnOrderIn) {
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions Source/Details/UIView+ASConvenience.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *, id<CAAction>> *actions;

- (void)setNeedsDisplay;
- (void)setNeedsLayout;
Expand Down
7 changes: 7 additions & 0 deletions Source/Details/_ASDisplayLayer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ - (void)setNeedsDisplay

#pragma mark -

+ (id<CAAction>)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;
Expand Down
24 changes: 12 additions & 12 deletions Source/Details/_ASDisplayView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -153,22 +153,22 @@ - (NSString *)description

#pragma mark - UIView Overrides

- (void)willMoveToWindow:(UIWindow *)newWindow
- (id<CAAction>)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<CAAction> 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<CAAction> nodeAction = (id)kCFNull;
Adlai-Holler marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
12 changes: 12 additions & 0 deletions Source/Private/ASDisplayNode+UIViewBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,18 @@ - (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea
}
}

- (NSDictionary<NSString *,id<CAAction>> *)actions
{
_bridge_prologue_read;
return _getFromLayer(actions);
}

- (void)setActions:(NSDictionary<NSString *,id<CAAction>> *)actions
{
_bridge_prologue_write;
_setToLayer(actions, actions);
}

- (void)safeAreaInsetsDidChange
{
ASDisplayNodeAssertMainThread();
Expand Down
2 changes: 1 addition & 1 deletion Source/Private/ASDisplayNodeInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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__;
Expand Down
86 changes: 21 additions & 65 deletions Source/Private/_ASPendingState.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -140,6 +144,7 @@ @implementation _ASPendingState
CGPoint accessibilityActivationPoint;
UIBezierPath *accessibilityPath;
UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0));
NSDictionary<NSString *, id<CAAction>> *actions;

ASPendingStateFlags _flags;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -586,6 +592,12 @@ - (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AV
_flags.setSemanticContentAttribute = YES;
}

- (void)setActions:(NSDictionary<NSString *,id<CAAction>> *)actionsArg
{
actions = [actionsArg copy];
_flags.setActions = YES;
}

- (BOOL)isAccessibilityElement
{
return isAccessibilityElement;
Expand Down Expand Up @@ -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)
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1272,7 +1290,7 @@ + (_ASPendingState *)pendingViewStateFromView:(UIView *)view

- (void)clearChanges
{
_flags = (ASPendingStateFlags){ 0 };
_flags = kZeroFlags;
}

- (BOOL)hasSetNeedsLayout
Expand All @@ -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));
Adlai-Holler marked this conversation as resolved.
Show resolved Hide resolved
}

- (void)dealloc
Expand Down
14 changes: 14 additions & 0 deletions Tests/ASDisplayNodeTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#import <QuartzCore/QuartzCore.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>

#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/_ASDisplayView.h>
Expand Down Expand Up @@ -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