-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Create a Pluggable "Tips" System to Help in Development #3270
Conversation
@@ -245,11 +246,6 @@ + (void)load | |||
ASScreenScale(); | |||
} | |||
|
|||
+ (BOOL)layerBackedNodesEnabled | |||
{ | |||
return YES; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method was never made public or used at all. It's been replaced by the instance method -supportsLayerBacking
now.
Source/ASDisplayNode.mm
Outdated
} | ||
view = [[_viewClass alloc] init]; | ||
Class c = _viewClass ?: [_ASDisplayView class]; | ||
view = [[c alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously _viewClass
would be nil
until the view loaded, then it would go to _ASDisplayView
. Now _viewClass
stays nil unless they used a custom view class.
- (void)setSynchronous:(BOOL)flag | ||
{ | ||
ASDN::MutexLocker l(__instanceLock__); | ||
_flags.synchronous = flag; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was unused so I tossed it in.
WOW THIS IS AMAZING |
ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; | ||
node.layerBacked = YES; | ||
XCTAssertTrue([node canBecomeFirstResponder]); | ||
XCTAssertFalse([node becomeFirstResponder]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test now is invalid because it sets layerBacked=YES
on a node that has a custom view class. Can't be done!
@@ -574,9 +572,6 @@ - (UIView *)_locked_viewToLoad | |||
_viewBlock = nil; | |||
_viewClass = [view class]; | |||
} else { | |||
if (!_viewClass) { | |||
_viewClass = [self.class viewClass]; | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are now established during init
so that the values are invariant across the node's lifetime.
Source/ASDisplayNode.mm
Outdated
|
||
if (isLayerBacked != _flags.layerBacked && !_view && !_layer) { | ||
_flags.layerBacked = isLayerBacked; | ||
if ([self _locked_isNodeLoaded]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice cleanup :)
} | ||
|
||
// If the text contains any links, return NO. | ||
NSAttributedString *attributedText = self.attributedText; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is their any performance concerns that this is called when setting layer backed to YES? Is it called in other paths?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, will modify to only call it if assertions are on.
Source/Base/ASDisplayNode+Ancestry.m
Outdated
|
||
@implementation ASNodeAncestryEnumerator { | ||
// Enumerators are one-shot optimized – avoid retains/releases/weak | ||
__unsafe_unretained ASDisplayNode * _Nullable _node; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a helluva optimization, worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm under normal circumstances I would say yes, but since we allow you to modify node hierarchies from any thread, I guess we have to use weak
here, since the "collection" we are enumerating can't be held still.
* If nil, the default, the message is just logged to the console with the | ||
* ancestry of the node. | ||
*/ | ||
@property (class, nonatomic, copy, null_resettable) ASTipDisplayBlock tipDisplayBlock; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know all the fancy annotations.
{ | ||
NSNumber *result = objc_getAssociatedObject(self, &ASDisplayNodeEnableTipsKey); | ||
if (result == nil) { | ||
return YES; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be reading this wrong, but shouldn't the default be NO?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No the default is YES
(RTFM! 😛). The global enable is whether they define AS_ENABLE_TIPS=1
. This property is only for disabling tips on a per-class basis, say if we make a false-positive that they find annoying when tips are on.
- (instancetype)init NS_UNAVAILABLE; | ||
|
||
/// Unsafe because once the node is deallocated, we will not be able to access the tip state. | ||
@property (nonatomic, unsafe_unretained, readonly) ASDisplayNode *node; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand why not to use weak here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that the node
property is truly immutable. If we keep tip states around after nodes are deallocated, then we should crash so that we stop doing that. This acts like an assertion, similar to Swift's unowned
storage class.
Source/Private/ASTipNode.m
Outdated
|
||
// Prevent compiler from complaining about undeclared selector. | ||
@interface UIResponder (TipNodeActions) | ||
- (void)didTapTipNode:(id)sender; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mind updating the comment saying this is defined on the tip window?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a protocol to contain this method.
if (self = [super init]) { | ||
self.backgroundColor = [UIColor colorWithRed:0 green:0.7 blue:0.2 alpha:0.3]; | ||
_tip = tip; | ||
[self addTarget:nil action:@selector(didTapTipNode:) forControlEvents:ASControlNodeEventTouchUpInside]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious, is this standard behavior to walk the chain if the target is nil? I see that's what it does in ASControlNode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! This is the best-kept secret of UIKit – you can set target nil and use the responder chain, instead of tightly binding all your controls to their controllers.
static NSArray<ASTipProvider *> *providers; | ||
static dispatch_once_t onceToken; | ||
dispatch_once(&onceToken, ^{ | ||
providers = @[ [ASLayerBackingTipProvider new] ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the idea is that new tip providers would be added here? Any thoughts on if we wanted to pull these out of ASDK into a separate project?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could do that in the future, and we have a ton of options. We could scan the runtime for subclasses of ASTipProvider, or we could require subclasses to register themselves, etc.
PR moved! |
#define AS_ENABLE_TIPS=1
ASTipProvider
subclasses against them. Currently we haveASLayerBackingTipProvider
.tipDisplayBlock
with a message. Default is to log a message using NSLog.MYDisplayNode.enableTips = NO
.nodeDidAppear:
.Example Screenshot:
Tapping on one of the big images causes this message to get logged (or shown, if the app sets a custom
tipDisplayBlock
):Caveats:
layerBacked=YES
on the leaf nodes first and work your way up.Results: