From 0c12a2fb79e2623ccaf610cb50b41825f2384219 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Fri, 6 Jul 2018 08:32:03 -0700 Subject: [PATCH] Properly consider node for responder methods (#1008) * Properly consider node for responder methods * Add changelog --- CHANGELOG.md | 1 + Source/ASDisplayNode.mm | 25 ++++++++++- Source/Details/_ASDisplayView.mm | 61 ++++++++++++++++++-------- Source/Private/ASDisplayNodeInternal.h | 21 +++++---- Tests/ASDisplayNodeTests.mm | 23 ++++++++++ 5 files changed, 102 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47a52ecc..3e027a04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Optimize layout flattening, particularly reducing retain/release operations. [Adlai Holler](https://github.com/Adlai-Holler) - Create a method to transfer strong C-arrays into immutable NSArrays, reducing retain/release traffic. [Adlai Holler](https://github.com/Adlai-Holler) - Remove yoga layout spec, which has been superseded by tighter Yoga integration (`ASDisplayNode+Yoga.h`) +- Properly consider node for responder methods [Michael Schneider](https://github.com/maicki) ## 2.7 - Fix pager node for interface coalescing. [Max Wang](https://github.com/wsdwsd0829) [#877](https://github.com/TextureGroup/Texture/pull/877) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 81ab1cd61..d29fa61c4 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -154,6 +154,8 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDisplayNodeCAssertNotNil(c, @"class is required"); ASDisplayNodeMethodOverrides overrides = ASDisplayNodeMethodOverrideNone; + + // Handling touches if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesBegan:withEvent:))) { overrides |= ASDisplayNodeMethodOverrideTouchesBegan; } @@ -166,13 +168,32 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesEnded:withEvent:))) { overrides |= ASDisplayNodeMethodOverrideTouchesEnded; } + + // Responder chain + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canBecomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(becomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canResignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(resignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(isFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideIsFirstResponder; + } + + // Layout related methods if (ASDisplayNodeSubclassOverridesSelector(c, @selector(layoutSpecThatFits:))) { overrides |= ASDisplayNodeMethodOverrideLayoutSpecThatFits; } if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits:)) || ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits: - restrictedToSize: - relativeToParentSize:))) { + restrictedToSize: + relativeToParentSize:))) { overrides |= ASDisplayNodeMethodOverrideCalcLayoutThatFits; } if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateSizeThatFits:))) { diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 2eac06bb3..21fdd17f1 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -29,6 +29,20 @@ #import #import +#pragma mark - ASDisplayNode + +/** + * Open access to the method overrides struct for ASDisplayView + */ +@implementation ASDisplayNode (ASDisplayNodeMethodOverrides_ASDisplayView) + +- (ASDisplayNodeMethodOverrides)methodOverrides +{ + return _methodOverrides; +} + +@end + #pragma mark - _ASDisplayViewMethodOverrides typedef NS_OPTIONS(NSUInteger, _ASDisplayViewMethodOverrides) @@ -452,25 +466,24 @@ - (void)tintColorDidChange #pragma mark UIResponder Handling -#define IMPLEMENT_RESPONDER_METHOD(__sel, __methodOverride) \ +#define IMPLEMENT_RESPONDER_METHOD(__sel, __nodeMethodOverride, __viewMethodOverride) \ - (BOOL)__sel\ {\ ASDisplayNode *node = _asyncdisplaykit_node; /* Create strong reference to weak ivar. */ \ - SEL sel = @selector(__sel); \ - /* Prevent an infinite loop in here if [super canBecomeFirstResponder] was called on a - / _ASDisplayView subclass */ \ - if (self->_methodOverrides & __methodOverride) { \ - /* Check if we can call through to ASDisplayNode subclass directly */ \ - if (ASDisplayNodeSubclassOverridesSelector([node class], sel)) { \ - return [node __sel]; \ - } else { \ - /* Call through to views superclass as we expect super was called from the - _ASDisplayView subclass and a node subclass does not overwrite canBecomeFirstResponder */ \ + /* Check if we can call through to ASDisplayNode subclass directly */ \ + if (node.methodOverrides & __nodeMethodOverride) { \ + return [node __sel]; \ + } else { \ + /* Prevent an infinite loop in here if [super __sel] was called on a \ + / _ASDisplayView subclass */ \ + if (self->_methodOverrides & __viewMethodOverride) { \ + /* Call through to views superclass as we expect super was called from the + _ASDisplayView subclass and a node subclass does not overwrite __sel */ \ return [self __##__sel]; \ + } else { \ + /* Call through to internal node __sel method that will consider the view in responding */ \ + return [node __##__sel]; \ } \ - } else { \ - /* Call through to internal node __canBecomeFirstResponder that will consider the view in responding */ \ - return [node __##__sel]; \ } \ }\ /* All __ prefixed methods are called from ASDisplayNode to let the view decide in what UIResponder state they \ @@ -480,11 +493,21 @@ - (BOOL)__##__sel \ return [super __sel]; \ } \ -IMPLEMENT_RESPONDER_METHOD(canBecomeFirstResponder, _ASDisplayViewMethodOverrideCanBecomeFirstResponder); -IMPLEMENT_RESPONDER_METHOD(becomeFirstResponder, _ASDisplayViewMethodOverrideBecomeFirstResponder); -IMPLEMENT_RESPONDER_METHOD(canResignFirstResponder, _ASDisplayViewMethodOverrideCanResignFirstResponder); -IMPLEMENT_RESPONDER_METHOD(resignFirstResponder, _ASDisplayViewMethodOverrideResignFirstResponder); -IMPLEMENT_RESPONDER_METHOD(isFirstResponder, _ASDisplayViewMethodOverrideIsFirstResponder); +IMPLEMENT_RESPONDER_METHOD(canBecomeFirstResponder, + ASDisplayNodeMethodOverrideCanBecomeFirstResponder, + _ASDisplayViewMethodOverrideCanBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(becomeFirstResponder, + ASDisplayNodeMethodOverrideBecomeFirstResponder, + _ASDisplayViewMethodOverrideBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(canResignFirstResponder, + ASDisplayNodeMethodOverrideCanResignFirstResponder, + _ASDisplayViewMethodOverrideCanResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(resignFirstResponder, + ASDisplayNodeMethodOverrideResignFirstResponder, + _ASDisplayViewMethodOverrideResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(isFirstResponder, + ASDisplayNodeMethodOverrideIsFirstResponder, + _ASDisplayViewMethodOverrideIsFirstResponder); - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 9146a4f1e..252202d51 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -44,14 +44,19 @@ _ASPendingState * ASDisplayNodeGetPendingState(ASDisplayNode * node); typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { - ASDisplayNodeMethodOverrideNone = 0, - ASDisplayNodeMethodOverrideTouchesBegan = 1 << 0, - ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1, - ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2, - ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3, - ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4, - ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5, - ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6, + ASDisplayNodeMethodOverrideNone = 0, + ASDisplayNodeMethodOverrideTouchesBegan = 1 << 0, + ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1, + ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2, + ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3, + ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4, + ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5, + ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6, + ASDisplayNodeMethodOverrideCanBecomeFirstResponder= 1 << 7, + ASDisplayNodeMethodOverrideBecomeFirstResponder = 1 << 8, + ASDisplayNodeMethodOverrideCanResignFirstResponder= 1 << 9, + ASDisplayNodeMethodOverrideResignFirstResponder = 1 << 10, + ASDisplayNodeMethodOverrideIsFirstResponder = 1 << 11, }; typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags) diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index 989997007..a75f16f21 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -263,6 +263,14 @@ - (BOOL)resignFirstResponder { @end +@interface ASTestResponderNodeWithOverride : ASDisplayNode +@end +@implementation ASTestResponderNodeWithOverride +- (BOOL)canBecomeFirstResponder { + return YES; +} +@end + @interface ASTestViewController: ASViewController @end @implementation ASTestViewController @@ -353,6 +361,21 @@ - (void)testResponderMethodsBehavior XCTAssertFalse([textNode.view isFirstResponder]); } +- (void)testResponderOverrrideCanBecomeFirstResponder +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASTestResponderNodeWithOverride *node = [[ASTestResponderNodeWithOverride alloc] init]; + + // We have to add the text node to a window otherwise the responder methods responses are undefined + // This will also create the backing view of the node + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); + XCTAssertTrue([window firstResponder] == node.view); +} + - (void)testUnsupportedResponderSetupWillThrow { ASTestResponderNode *node = [[ASTestResponderNode alloc] init];