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

Create a Pluggable "Tips" System to Help in Development #3270

Closed
wants to merge 1 commit into from

Conversation

Adlai-Holler
Copy link
Contributor

@Adlai-Holler Adlai-Holler commented Apr 13, 2017

  • To activate: #define AS_ENABLE_TIPS=1
  • ASDK will create a new window on top of the application window.
  • At the end of each run loop, tips controller goes through all nodes that appeared during the turn.
  • Runs all ASTipProvider subclasses against them. Currently we have ASLayerBackingTipProvider.
  • If the provider generates a tip for that node, a node is added to the overlay window.
  • If the user taps on a tip, we call a custom tipDisplayBlock with a message. Default is to log a message using NSLog.
  • You can disable tips on a per-class level. MYDisplayNode.enableTips = NO.
  • Only requires one hook from ASDisplayNode right now – nodeDidAppear:.

Example Screenshot:
screen shot 2017-04-13 at 10 36 34 am

Tapping on one of the big images causes this message to get logged (or shown, if the app sets a custom tipDisplayBlock):

Enable layer backing to improve performance. Node ancestry: (
    "<PIRoundedImageNode: 0x7ff89a031c00>",
    "<PIFlexibleArticleNode: 0x7ff8984a4430>",
    "<PIWrapperCellNode: 0x7ff8985ace40>"
)

Caveats:

  • As before, you can't have view-backed nodes inside layer-backed nodes, so you need to set layerBacked=YES on the leaf nodes first and work your way up.
  • When tips are enabled, touches on green areas are not passed to the application window. This is annoying but not too annoying.

Results:

  • No false positives in Pinterest yet, and all the green is gone in one diff!

@@ -245,11 +246,6 @@ + (void)load
ASScreenScale();
}

+ (BOOL)layerBackedNodesEnabled
{
return YES;
Copy link
Contributor Author

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.

}
view = [[_viewClass alloc] init];
Class c = _viewClass ?: [_ASDisplayView class];
view = [[c alloc] init];
Copy link
Contributor Author

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;
Copy link
Contributor Author

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.

@levi
Copy link
Contributor

levi commented Apr 13, 2017

WOW THIS IS AMAZING

ASTestDisplayNode *node = [[ASTestResponderNode alloc] init];
node.layerBacked = YES;
XCTAssertTrue([node canBecomeFirstResponder]);
XCTAssertFalse([node becomeFirstResponder]);
Copy link
Contributor Author

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];
}
Copy link
Contributor Author

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.


if (isLayerBacked != _flags.layerBacked && !_view && !_layer) {
_flags.layerBacked = isLayerBacked;
if ([self _locked_isNodeLoaded]) {
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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.


@implementation ASNodeAncestryEnumerator {
// Enumerators are one-shot optimized – avoid retains/releases/weak
__unsafe_unretained ASDisplayNode * _Nullable _node;
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

@Adlai-Holler Adlai-Holler Apr 13, 2017

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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.


// Prevent compiler from complaining about undeclared selector.
@interface UIResponder (TipNodeActions)
- (void)didTapTipNode:(id)sender;
Copy link
Contributor

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?

Copy link
Contributor Author

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];
Copy link
Contributor

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.

Copy link
Contributor Author

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] ];
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@Adlai-Holler
Copy link
Contributor Author

PR moved!

@Adlai-Holler Adlai-Holler deleted the AHTips branch April 17, 2017 19:07
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants