Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit a18ffc6

Browse files
committed
[AsyncDisplayKit+Debug.h] Moving hitTestDebug tool code out of ASControlNode and into debug file
- unifying ASControlNode, ASImageNode debugging categories into debug file to simplify base classes
1 parent ae29cd2 commit a18ffc6

3 files changed

Lines changed: 190 additions & 149 deletions

File tree

AsyncDisplayKit/ASControlNode.mm

Lines changed: 13 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#import "ASThread.h"
1212
#import "ASDisplayNodeExtras.h"
1313
#import "ASImageNode.h"
14+
#import "AsyncDisplayKit+Debug.h"
15+
#import "ASInternalHelpers.h"
1416

1517
// UIControl allows dragging some distance outside of the control itself during
1618
// tracking. This value depends on the device idiom (25 or 70 points), so
@@ -71,8 +73,6 @@ @interface ASControlNode ()
7173

7274
@end
7375

74-
static BOOL _enableHitTestDebug = NO;
75-
7676
@implementation ASControlNode
7777
{
7878
ASImageNode *_debugHighlightOverlay;
@@ -262,14 +262,15 @@ - (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeE
262262
_controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries.
263263

264264
// only show tap-able areas for views with 1 or more addTarget:action: pairs
265-
if (_enableHitTestDebug) {
266-
267-
// add a highlight overlay node with area of ASControlNode + UIEdgeInsets
268-
self.clipsToBounds = NO;
269-
_debugHighlightOverlay = [[ASImageNode alloc] init];
270-
_debugHighlightOverlay.zPosition = 1000; // CALayer doesn't have -moveSublayerToFront, but this will ensure we're over the top of any siblings.
271-
_debugHighlightOverlay.layerBacked = YES;
272-
[self addSubnode:_debugHighlightOverlay];
265+
if ([ASControlNode enableHitTestDebug]) {
266+
ASPerformBlockOnMainThread(^{
267+
// add a highlight overlay node with area of ASControlNode + UIEdgeInsets
268+
self.clipsToBounds = NO;
269+
_debugHighlightOverlay = [[ASImageNode alloc] init];
270+
_debugHighlightOverlay.zPosition = 1000; // ensure we're over the top of any siblings
271+
_debugHighlightOverlay.layerBacked = YES;
272+
[self addSubnode:_debugHighlightOverlay];
273+
});
273274
}
274275
}
275276

@@ -470,134 +471,9 @@ - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
470471
}
471472

472473
#pragma mark - Debug
473-
// Layout method required when _enableHitTestDebug is enabled.
474-
- (void)layout
475-
{
476-
[super layout];
477-
478-
if (_debugHighlightOverlay) {
479-
480-
// Even if our parents don't have clipsToBounds set and would allow us to display the debug overlay, UIKit event delivery (hitTest:)
481-
// will not search sub-hierarchies if one of our parents does not return YES for pointInside:. In such a scenario, hitTestSlop
482-
// may not be able to expand the tap target as much as desired without also setting some hitTestSlop on the limiting parents.
483-
CGRect intersectRect = UIEdgeInsetsInsetRect(self.bounds, [self hitTestSlop]);
484-
UIRectEdge clippedEdges = UIRectEdgeNone;
485-
UIRectEdge clipsToBoundsClippedEdges = UIRectEdgeNone;
486-
CALayer *layer = self.layer;
487-
CALayer *intersectLayer = layer;
488-
CALayer *intersectSuperlayer = layer.superlayer;
489-
490-
// Stop climbing if we encounter a UIScrollView, as its offset bounds origin may make it seem like our events will be clipped when
491-
// scrolling will actually reveal them (because this process will not re-run due to scrolling)
492-
while (intersectSuperlayer && ![intersectSuperlayer.delegate respondsToSelector:@selector(contentOffset)]) {
493-
// Get our parent's tappable bounds. If the parent has an associated node, consider hitTestSlop, as it will extend its pointInside:.
494-
CGRect parentHitRect = intersectSuperlayer.bounds;
495-
BOOL parentClipsToBounds = NO;
496-
497-
ASDisplayNode *parentNode = ASLayerToDisplayNode(intersectSuperlayer);
498-
if (parentNode) {
499-
UIEdgeInsets parentSlop = [parentNode hitTestSlop];
500-
501-
// if parent has a hitTestSlop as well, we need to account for the fact that events will be routed towards us in that area too.
502-
if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, parentSlop)) {
503-
parentClipsToBounds = parentNode.clipsToBounds;
504-
// if the parent is clipping, this will prevent us from showing the overlay outside that area.
505-
// in this case, we will make the overlay smaller so that the special highlight to indicate the overlay
506-
// cannot accurately display the true tappable area is shown.
507-
if (!parentClipsToBounds) {
508-
parentHitRect = UIEdgeInsetsInsetRect(parentHitRect, [parentNode hitTestSlop]);
509-
}
510-
}
511-
}
512-
513-
// Convert our current rectangle to parent coordinates, and intersect with the parent's hit rect.
514-
CGRect intersectRectInParentCoordinates = [intersectSuperlayer convertRect:intersectRect fromLayer:intersectLayer];
515-
intersectRect = CGRectIntersection(parentHitRect, intersectRectInParentCoordinates);
516-
if (!CGSizeEqualToSize(parentHitRect.size, intersectRectInParentCoordinates.size)) {
517-
clippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates
518-
parentRect:parentHitRect rectEdge:clippedEdges];
519-
if (parentClipsToBounds) {
520-
clipsToBoundsClippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates
521-
parentRect:parentHitRect rectEdge:clipsToBoundsClippedEdges];
522-
}
523-
}
524-
525-
// Advance up the tree.
526-
intersectLayer = intersectSuperlayer;
527-
intersectSuperlayer = intersectLayer.superlayer;
528-
}
529-
530-
CGRect finalRect = [intersectLayer convertRect:intersectRect toLayer:layer];
531-
UIColor *fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.4];
532-
533-
// determine if edges are clipped
534-
if (clippedEdges == UIRectEdgeNone) {
535-
_debugHighlightOverlay.backgroundColor = fillColor;
536-
} else {
537-
const CGFloat borderWidth = 2.0;
538-
UIColor *borderColor = [[UIColor orangeColor] colorWithAlphaComponent:0.8];
539-
UIColor *clipsBorderColor = [UIColor colorWithRed:30/255.0 green:90/255.0 blue:50/255.0 alpha:0.7];
540-
CGRect imgRect = CGRectMake(0, 0, 2.0 * borderWidth + 1.0, 2.0 * borderWidth + 1.0);
541-
UIGraphicsBeginImageContext(imgRect.size);
542-
543-
[fillColor setFill];
544-
UIRectFill(imgRect);
545-
546-
[self drawEdgeIfClippedWithEdges:clippedEdges color:clipsBorderColor borderWidth:borderWidth imgRect:imgRect];
547-
[self drawEdgeIfClippedWithEdges:clipsToBoundsClippedEdges color:borderColor borderWidth:borderWidth imgRect:imgRect];
548-
549-
UIImage *debugHighlightImage = UIGraphicsGetImageFromCurrentImageContext();
550-
UIGraphicsEndImageContext();
551-
552-
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(borderWidth, borderWidth, borderWidth, borderWidth);
553-
_debugHighlightOverlay.image = [debugHighlightImage resizableImageWithCapInsets:edgeInsets
554-
resizingMode:UIImageResizingModeStretch];
555-
_debugHighlightOverlay.backgroundColor = nil;
556-
}
557-
558-
_debugHighlightOverlay.frame = finalRect;
559-
}
560-
}
561-
562-
- (UIRectEdge)setEdgesOfIntersectionForChildRect:(CGRect)childRect parentRect:(CGRect)parentRect rectEdge:(UIRectEdge)rectEdge
563-
{
564-
if (childRect.origin.y < parentRect.origin.y) {
565-
rectEdge |= UIRectEdgeTop;
566-
}
567-
if (childRect.origin.x < parentRect.origin.x) {
568-
rectEdge |= UIRectEdgeLeft;
569-
}
570-
if (CGRectGetMaxY(childRect) > CGRectGetMaxY(parentRect)) {
571-
rectEdge |= UIRectEdgeBottom;
572-
}
573-
if (CGRectGetMaxX(childRect) > CGRectGetMaxX(parentRect)) {
574-
rectEdge |= UIRectEdgeRight;
575-
}
576-
577-
return rectEdge;
578-
}
579-
580-
- (void)drawEdgeIfClippedWithEdges:(UIRectEdge)rectEdge color:(UIColor *)color borderWidth:(CGFloat)borderWidth imgRect:(CGRect)imgRect
581-
{
582-
[color setFill];
583-
584-
if (rectEdge & UIRectEdgeTop) {
585-
UIRectFill(CGRectMake(0.0, 0.0, imgRect.size.width, borderWidth));
586-
}
587-
if (rectEdge & UIRectEdgeLeft) {
588-
UIRectFill(CGRectMake(0.0, 0.0, borderWidth, imgRect.size.height));
589-
}
590-
if (rectEdge & UIRectEdgeBottom) {
591-
UIRectFill(CGRectMake(0.0, imgRect.size.height - borderWidth, imgRect.size.width, borderWidth));
592-
}
593-
if (rectEdge & UIRectEdgeRight) {
594-
UIRectFill(CGRectMake(imgRect.size.width - borderWidth, 0.0, borderWidth, imgRect.size.height));
595-
}
596-
}
597-
598-
+ (void)setEnableHitTestDebug:(BOOL)enable
474+
- (ASImageNode *)debugHighlightOverlay
599475
{
600-
_enableHitTestDebug = enable;
476+
return _debugHighlightOverlay;
601477
}
602478

603479
@end

AsyncDisplayKit/AsyncDisplayKit+Debug.h

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99
#import "ASControlNode.h"
1010
#import "ASImageNode.h"
1111

12+
@interface ASImageNode (Debugging)
13+
14+
/**
15+
* Enables an ASImageNode debug label that shows the ratio of pixels in the source image to those in
16+
* the displayed bounds (including cropRect). This helps detect excessive image fetching / downscaling,
17+
* as well as upscaling (such as providing a URL not suitable for a Retina device). For dev purposes only.
18+
* @param enabled Specify YES to show the label on all ASImageNodes with non-1.0x source-to-bounds pixel ratio.
19+
*/
20+
+ (void)setShouldShowImageScalingOverlay:(BOOL)show;
21+
+ (BOOL)shouldShowImageScalingOverlay;
22+
23+
@end
24+
1225
@interface ASControlNode (Debugging)
1326

1427
/**
@@ -21,18 +34,7 @@
2134
@param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class.
2235
*/
2336
+ (void)setEnableHitTestDebug:(BOOL)enable;
37+
+ (BOOL)enableHitTestDebug;
2438

2539
@end
2640

27-
@interface ASImageNode (Debugging)
28-
29-
/**
30-
* Enables an ASImageNode debug label that shows the ratio of pixels in the source image to those in
31-
* the displayed bounds (including cropRect). This helps detect excessive image fetching / downscaling,
32-
* as well as upscaling (such as providing a URL not suitable for a Retina device). For dev purposes only.
33-
* @param enabled Specify YES to show the label on all ASImageNodes with non-1.0x source-to-bounds pixel ratio.
34-
*/
35-
+ (void)setShouldShowImageScalingOverlay:(BOOL)show;
36-
+ (BOOL)shouldShowImageScalingOverlay;
37-
38-
@end

AsyncDisplayKit/AsyncDisplayKit+Debug.m

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#import "AsyncDisplayKit+Debug.h"
1010
#import "ASDisplayNode+Subclasses.h"
11+
#import "ASDisplayNodeExtras.h"
1112

1213
static BOOL __shouldShowImageScalingOverlay = NO;
1314

@@ -24,3 +25,165 @@ + (BOOL)shouldShowImageScalingOverlay
2425
}
2526

2627
@end
28+
29+
static BOOL __enableHitTestDebug = NO;
30+
31+
@interface ASControlNode (Debugging)
32+
33+
- (ASImageNode *)debugHighlightOverlay;
34+
35+
@end
36+
37+
@implementation ASControlNode (Debugging)
38+
39+
+ (void)setEnableHitTestDebug:(BOOL)enable
40+
{
41+
__enableHitTestDebug = enable;
42+
}
43+
44+
+ (BOOL)enableHitTestDebug
45+
{
46+
return __enableHitTestDebug;
47+
}
48+
49+
// layout method required ONLY when hitTestDebug is enabled
50+
- (void)layout
51+
{
52+
[super layout];
53+
54+
if ([ASControlNode enableHitTestDebug]) {
55+
56+
// Construct hitTestDebug highlight overlay frame indicating tappable area of a node, which can be restricted by two things:
57+
58+
// (1) Any parent's tapable area (its own bounds + hitTestSlop) may restrict the desired tappable area expansion using
59+
// hitTestSlop of a child as UIKit event delivery (hitTest:) will not search sub-hierarchies if one of our parents does
60+
// not return YES for pointInside:. To circumvent this restriction, a developer will need to set / adjust the hitTestSlop
61+
// on the limiting parent. This is indicated in the overlay by a dark GREEN edge. This is an ACTUAL restriction.
62+
63+
// (2) Any parent's .clipToBounds. If a parent is clipping, we cannot show the overlay outside that area
64+
// (although it still respond to touch). To indicate that the overlay cannot accurately display the true tappable area,
65+
// the overlay will have an ORANGE edge. This is a VISUALIZATION restriction.
66+
67+
CGRect intersectRect = UIEdgeInsetsInsetRect(self.bounds, [self hitTestSlop]);
68+
UIRectEdge clippedEdges = UIRectEdgeNone;
69+
UIRectEdge clipsToBoundsClippedEdges = UIRectEdgeNone;
70+
CALayer *layer = self.layer;
71+
CALayer *intersectLayer = layer;
72+
CALayer *intersectSuperlayer = layer.superlayer;
73+
74+
// FIXED: Stop climbing hierarchy if UIScrollView is encountered (its offset bounds origin may make it seem like our events
75+
// will be clipped when scrolling will actually reveal them (because this process will not re-run due to scrolling))
76+
while (intersectSuperlayer && ![intersectSuperlayer.delegate respondsToSelector:@selector(contentOffset)]) {
77+
78+
// Get parent's tappable area
79+
CGRect parentHitRect = intersectSuperlayer.bounds;
80+
BOOL parentClipsToBounds = NO;
81+
82+
// If parent is a node, tappable area may be expanded by hitTestSlop
83+
ASDisplayNode *parentNode = ASLayerToDisplayNode(intersectSuperlayer);
84+
if (parentNode) {
85+
UIEdgeInsets parentSlop = [parentNode hitTestSlop];
86+
87+
// If parent has hitTestSlop, expand tappable area (if parent doesn't clipToBounds)
88+
if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, parentSlop)) {
89+
parentClipsToBounds = parentNode.clipsToBounds;
90+
if (!parentClipsToBounds) {
91+
parentHitRect = UIEdgeInsetsInsetRect(parentHitRect, [parentNode hitTestSlop]);
92+
}
93+
}
94+
}
95+
96+
// Convert our current rect to parent coordinates
97+
CGRect intersectRectInParentCoordinates = [intersectSuperlayer convertRect:intersectRect fromLayer:intersectLayer];
98+
99+
// Intersect rect with the parent's tappable area rect
100+
intersectRect = CGRectIntersection(parentHitRect, intersectRectInParentCoordinates);
101+
if (!CGSizeEqualToSize(parentHitRect.size, intersectRectInParentCoordinates.size)) {
102+
clippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates
103+
parentRect:parentHitRect rectEdge:clippedEdges];
104+
if (parentClipsToBounds) {
105+
clipsToBoundsClippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates
106+
parentRect:parentHitRect rectEdge:clipsToBoundsClippedEdges];
107+
}
108+
}
109+
110+
// move up hierarchy
111+
intersectLayer = intersectSuperlayer;
112+
intersectSuperlayer = intersectLayer.superlayer;
113+
}
114+
115+
// produce final overlay image (or fill background if edges aren't restricted)
116+
CGRect finalRect = [intersectLayer convertRect:intersectRect toLayer:layer];
117+
UIColor *fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.4];
118+
119+
ASImageNode *debugOverlay = [self debugHighlightOverlay];
120+
121+
// determine if edges are clipped and if so, highlight the restricted edges
122+
if (clippedEdges == UIRectEdgeNone) {
123+
debugOverlay.backgroundColor = fillColor;
124+
} else {
125+
const CGFloat borderWidth = 2.0;
126+
UIColor *borderColor = [[UIColor orangeColor] colorWithAlphaComponent:0.8];
127+
UIColor *clipsBorderColor = [UIColor colorWithRed:30/255.0 green:90/255.0 blue:50/255.0 alpha:0.7];
128+
CGRect imgRect = CGRectMake(0, 0, 2.0 * borderWidth + 1.0, 2.0 * borderWidth + 1.0);
129+
130+
UIGraphicsBeginImageContext(imgRect.size);
131+
132+
[fillColor setFill];
133+
UIRectFill(imgRect);
134+
135+
[self drawEdgeIfClippedWithEdges:clippedEdges color:clipsBorderColor borderWidth:borderWidth imgRect:imgRect];
136+
[self drawEdgeIfClippedWithEdges:clipsToBoundsClippedEdges color:borderColor borderWidth:borderWidth imgRect:imgRect];
137+
138+
UIImage *debugHighlightImage = UIGraphicsGetImageFromCurrentImageContext();
139+
UIGraphicsEndImageContext();
140+
141+
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(borderWidth, borderWidth, borderWidth, borderWidth);
142+
debugOverlay.image = [debugHighlightImage resizableImageWithCapInsets:edgeInsets resizingMode:UIImageResizingModeStretch];
143+
debugOverlay.backgroundColor = nil;
144+
}
145+
146+
debugOverlay.frame = finalRect;
147+
}
148+
}
149+
150+
- (UIRectEdge)setEdgesOfIntersectionForChildRect:(CGRect)childRect parentRect:(CGRect)parentRect rectEdge:(UIRectEdge)rectEdge
151+
{
152+
// determine which edges of childRect are outside parentRect (and thus will be clipped)
153+
if (childRect.origin.y < parentRect.origin.y) {
154+
rectEdge |= UIRectEdgeTop;
155+
}
156+
if (childRect.origin.x < parentRect.origin.x) {
157+
rectEdge |= UIRectEdgeLeft;
158+
}
159+
if (CGRectGetMaxY(childRect) > CGRectGetMaxY(parentRect)) {
160+
rectEdge |= UIRectEdgeBottom;
161+
}
162+
if (CGRectGetMaxX(childRect) > CGRectGetMaxX(parentRect)) {
163+
rectEdge |= UIRectEdgeRight;
164+
}
165+
166+
return rectEdge;
167+
}
168+
169+
- (void)drawEdgeIfClippedWithEdges:(UIRectEdge)rectEdge color:(UIColor *)color borderWidth:(CGFloat)borderWidth imgRect:(CGRect)imgRect
170+
{
171+
[color setFill];
172+
173+
// highlight individual edges of overlay if edge is restricted by parentRect
174+
// so that the developer is aware that increasing hitTestSlop will not result in an expanded tappable area
175+
if (rectEdge & UIRectEdgeTop) {
176+
UIRectFill(CGRectMake(0.0, 0.0, imgRect.size.width, borderWidth));
177+
}
178+
if (rectEdge & UIRectEdgeLeft) {
179+
UIRectFill(CGRectMake(0.0, 0.0, borderWidth, imgRect.size.height));
180+
}
181+
if (rectEdge & UIRectEdgeBottom) {
182+
UIRectFill(CGRectMake(0.0, imgRect.size.height - borderWidth, imgRect.size.width, borderWidth));
183+
}
184+
if (rectEdge & UIRectEdgeRight) {
185+
UIRectFill(CGRectMake(imgRect.size.width - borderWidth, 0.0, borderWidth, imgRect.size.height));
186+
}
187+
}
188+
189+
@end

0 commit comments

Comments
 (0)