Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- Make `ASCellNode` tint color apply to table view cell accessories. [Vladyslav Chapaev](https://github.com/ShogunPhyched) [#764](https://github.com/TextureGroup/Texture/pull/764)
- Fix ASTextNode2 is accessing backgroundColor off main while sizing / layout is happening. [Michael Schneider](https://github.com/maicki) [#794](https://github.com/TextureGroup/Texture/pull/778/)
- Pass scrollViewWillEndDragging delegation through in ASIGListAdapterDataSource for IGListKit integration. [#796](https://github.com/TextureGroup/Texture/pull/796)
- Fix UIResponder handling with view backing ASDisplayNode. [Michael Schneider](https://github.com/maicki) [#789] (https://github.com/TextureGroup/Texture/pull/789/)

## 2.6
- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon)
Expand Down
93 changes: 93 additions & 0 deletions Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@ - (UIView *)_locked_viewToLoad

// Special handling of wrapping UIKit components
if (checkFlag(Synchronous)) {
[self checkResponderCompatibility];

// UIImageView layers. More details on the flags
if ([_viewClass isSubclassOfClass:[UIImageView class]]) {
_flags.canClearContentsOfLayer = NO;
Expand Down Expand Up @@ -828,6 +830,97 @@ - (void)nodeViewDidAddGestureRecognizer
_flags.viewEverHadAGestureRecognizerAttached = YES;
}

#pragma mark UIResponder

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this should go in another file, since ASDisplayNode.mm is quite large - it seems very appropriate for UIViewBridge or perhaps we could introduce another one.

Not a blocking change (feel free to land this if you're just eager to get it in!)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are currently have the event handling touchesBegan: etc. in ASDisplayNode.mm, eventually we should move both of them out too in a follow up diff.


#define HANDLE_NODE_RESPONDER_METHOD(__sel) \
/* All responder methods should be called on the main thread */ \
ASDisplayNodeAssertMainThread(); \
if (checkFlag(Synchronous)) { \
/* If the view is not a _ASDisplayView subclass (Synchronous) just call through to the view as we
expect it's a non _ASDisplayView subclass that will respond */ \
return [_view __sel]; \
} else { \
if (ASSubclassOverridesSelector([_ASDisplayView class], _viewClass, @selector(__sel))) { \
/* If the subclass overwrites canBecomeFirstResponder just call through
to it as we expect it will handle it */ \
return [_view __sel]; \
} else { \
/* Call through to _ASDisplayView's superclass to get it handled */ \
return [(_ASDisplayView *)_view __##__sel]; \
} \
} \

- (void)checkResponderCompatibility
{
#if ASDISPLAYNODE_ASSERTIONS_ENABLED
// There are certain cases we cannot handle and are not supported:
// 1. If the _view class is not a subclass of _ASDisplayView
if (checkFlag(Synchronous)) {
// 2. At least one UIResponder methods are overwritten in the node subclass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any concerns with touchesBegan:, touchesMoved:, touchesEnded:, or touchesCancelled: ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't look into them or saw any issues related to them.

NSString *message = @"Overwritting %@ and having a backing view that is not an _ASDisplayView is not supported.";
ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canBecomeFirstResponder)), ([NSString stringWithFormat:message, @"canBecomeFirstResponder"]));
ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(becomeFirstResponder)), ([NSString stringWithFormat:message, @"becomeFirstResponder"]));
ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canResignFirstResponder)), ([NSString stringWithFormat:message, @"canResignFirstResponder"]));
ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(resignFirstResponder)), ([NSString stringWithFormat:message, @"resignFirstResponder"]));
ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(isFirstResponder)), ([NSString stringWithFormat:message, @"isFirstResponder"]));
}
#endif
}

- (BOOL)__canBecomeFirstResponder
{
if (_view == nil) {
// By default we return NO if not view is created yet
return NO;
}

HANDLE_NODE_RESPONDER_METHOD(canBecomeFirstResponder);
}

- (BOOL)__becomeFirstResponder
{
if (![self canBecomeFirstResponder]) {
return NO;
}

// Note: This implicitly loads the view if it hasn't been loaded yet.
[self view];

HANDLE_NODE_RESPONDER_METHOD(becomeFirstResponder);
}

- (BOOL)__canResignFirstResponder
{
if (_view == nil) {
// By default we return YES if no view is created yet
return YES;
}

HANDLE_NODE_RESPONDER_METHOD(canResignFirstResponder);
}

- (BOOL)__resignFirstResponder
{
if (![self canResignFirstResponder]) {
return NO;
}

// Note: This implicitly loads the view if it hasn't been loaded yet.
[self view];

HANDLE_NODE_RESPONDER_METHOD(resignFirstResponder);
}

- (BOOL)__isFirstResponder
{
if (_view == nil) {
// If no view is created yet we can just return NO as it's unlikely it's the first responder
return NO;
}

HANDLE_NODE_RESPONDER_METHOD(isFirstResponder);
}

#pragma mark <ASDebugNameProvider>

- (NSString *)debugName
Expand Down
8 changes: 8 additions & 0 deletions Source/Details/_ASDisplayView.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ NS_ASSUME_NONNULL_BEGIN
- (void)__forwardTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)__forwardTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

// These methods expose a way for ASDisplayNode responder methods to let the view call super responder methods

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This first sentence isn't quite clear -- "for ASDisplayNode responder methods to let the view call super responder methods".

Clarify if possible, otherwise not exactly sure how to improve it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentence is kind of the same as the one for all of the forwarding touch events. In that case we should find a better way for both.

// They are called from ASDisplayNode to pass through UIResponder methods to the view
- (BOOL)__canBecomeFirstResponder;
- (BOOL)__becomeFirstResponder;
- (BOOL)__canResignFirstResponder;
- (BOOL)__resignFirstResponder;
- (BOOL)__isFirstResponder;

@end

NS_ASSUME_NONNULL_END
124 changes: 115 additions & 9 deletions Source/Details/_ASDisplayView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,54 @@
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASLayout.h>

#pragma mark - _ASDisplayViewMethodOverrides

typedef NS_OPTIONS(NSUInteger, _ASDisplayViewMethodOverrides)
{
_ASDisplayViewMethodOverrideNone = 0,
_ASDisplayViewMethodOverrideCanBecomeFirstResponder = 1 << 0,
_ASDisplayViewMethodOverrideBecomeFirstResponder = 1 << 1,
_ASDisplayViewMethodOverrideCanResignFirstResponder = 1 << 2,
_ASDisplayViewMethodOverrideResignFirstResponder = 1 << 3,
_ASDisplayViewMethodOverrideIsFirstResponder = 1 << 4,
};

/**
* Returns _ASDisplayViewMethodOverrides for the given class
*
* @param c the class, required.
*
* @return _ASDisplayViewMethodOverrides.
*/
static _ASDisplayViewMethodOverrides GetASDisplayViewMethodOverrides(Class c)
{
ASDisplayNodeCAssertNotNil(c, @"class is required");

_ASDisplayViewMethodOverrides overrides = _ASDisplayViewMethodOverrideNone;
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canBecomeFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideCanBecomeFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(becomeFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideBecomeFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canResignFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideCanResignFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(resignFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideResignFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(isFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideIsFirstResponder;
}
return overrides;
}

#pragma mark - _ASDisplayView

@interface _ASDisplayView ()

// Keep the node alive while its view is active. If you create a view, add its layer to a layer hierarchy, then release
Expand All @@ -40,6 +85,21 @@ @implementation _ASDisplayView

NSArray *_accessibleElements;
CGRect _lastAccessibleElementsFrame;

_ASDisplayViewMethodOverrides _methodOverrides;
}

#pragma mark - Class

+ (void)initialize
{
__unused Class initializeSelf = self;
IMP staticInitialize = imp_implementationWithBlock(^(_ASDisplayView *view) {
ASDisplayNodeAssert(view.class == initializeSelf, @"View class %@ does not have a matching _staticInitialize method; check to ensure [super initialize] is called within any custom +initialize implementations! Overridden methods will not be called unless they are also implemented by superclass %@", view.class, initializeSelf);
view->_methodOverrides = GetASDisplayViewMethodOverrides(view.class);
});

class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@");
}

+ (Class)layerClass
Expand All @@ -49,6 +109,26 @@ + (Class)layerClass

#pragma mark - NSObject Overrides

- (instancetype)init
{
if (!(self = [super init]))
return nil;

[self _initializeInstance];

return self;
}

- (void)_initializeInstance
{
[self _staticInitialize];
}

- (void)_staticInitialize
{
ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden");
}

// e.g. <MYPhotoNodeView: 0xFFFFFF; node = <MYPhotoNode: 0xFFFFFE>; frame = ...>
- (NSString *)description
{
Expand Down Expand Up @@ -358,15 +438,41 @@ - (void)tintColorDidChange
[node tintColorDidChange];
}

- (BOOL)canBecomeFirstResponder {
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node canBecomeFirstResponder];
}

- (BOOL)canResignFirstResponder {
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node canResignFirstResponder];
}
#pragma mark UIResponder Handling

#define IMPLEMENT_RESPONDER_METHOD(__sel, __methodOverride) \
- (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 */ \
return [self __##__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 \
are not overridden by a ASDisplayNode subclass */ \
- (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);

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
Expand Down
29 changes: 15 additions & 14 deletions Source/Private/ASDisplayNode+UIViewBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,6 @@ ASDISPLAYNODE_INLINE BOOL ASDisplayNodeShouldApplyBridgedWriteToView(ASDisplayNo
*/
@implementation ASDisplayNode (UIViewBridge)

- (BOOL)canBecomeFirstResponder
{
return NO;
}

- (BOOL)canResignFirstResponder
{
return YES;
}

#if TARGET_OS_TV
// Focus Engine
- (BOOL)canBecomeFocused
Expand Down Expand Up @@ -146,23 +136,34 @@ - (UIView *)preferredFocusedView
}
#endif

- (BOOL)canBecomeFirstResponder
{
ASDisplayNodeAssertMainThread();
return [self __canBecomeFirstResponder];
}

- (BOOL)canResignFirstResponder
{
ASDisplayNodeAssertMainThread();
return [self __canResignFirstResponder];
}

- (BOOL)isFirstResponder
{
ASDisplayNodeAssertMainThread();
return _view != nil && [_view isFirstResponder];
return [self __isFirstResponder];
}

// Note: this implicitly loads the view if it hasn't been loaded yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a good comment to keep.

@maicki maicki Feb 21, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had this kind of comment in __becomeFirstResponder now:

// Note: this implicitly loads the view if it hasn't been loaded yet.

I changed it to the old comment in there though. Let me know if I should also add it to this place back again.

- (BOOL)becomeFirstResponder
{
ASDisplayNodeAssertMainThread();
return !self.layerBacked && [self canBecomeFirstResponder] && [self.view becomeFirstResponder];
return [self __becomeFirstResponder];
}

- (BOOL)resignFirstResponder
{
ASDisplayNodeAssertMainThread();
return !self.layerBacked && [self canResignFirstResponder] && [_view resignFirstResponder];
return [self __resignFirstResponder];
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
Expand Down
7 changes: 7 additions & 0 deletions Source/Private/ASDisplayNodeInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
- (void)__incrementVisibilityNotificationsDisabled;
- (void)__decrementVisibilityNotificationsDisabled;

// Helper methods for UIResponder forwarding
- (BOOL)__canBecomeFirstResponder;
- (BOOL)__becomeFirstResponder;
- (BOOL)__canResignFirstResponder;
- (BOOL)__resignFirstResponder;
- (BOOL)__isFirstResponder;

/// Helper method to summarize whether or not the node run through the display process
- (BOOL)_implementsDisplay;

Expand Down
Loading