2019-11-11 16:39:27 +04:00

186 lines
5.4 KiB
Plaintext

//
// ASTipsController.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASTipsController.h"
#if AS_ENABLE_TIPS
#import <AsyncDisplayKit/ASDisplayNodeTipState.h>
#import <AsyncDisplayKit/AsyncDisplayKit+Tips.h>
#import <AsyncDisplayKit/ASTipNode.h>
#import <AsyncDisplayKit/ASTipProvider.h>
#import <AsyncDisplayKit/ASTipsWindow.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
@interface ASTipsController ()
/// Nil on init, updates to most recent visible window.
@property (nonatomic) UIWindow *appVisibleWindow;
/// Nil until an application window has become visible.
@property (nonatomic) ASTipsWindow *tipWindow;
/// Main-thread-only.
@property (nonatomic, readonly) NSMapTable<ASDisplayNode *, ASDisplayNodeTipState *> *nodeToTipStates;
@property (nonatomic) NSMutableArray<ASDisplayNode *> *nodesThatAppearedDuringRunLoop;
@end
@implementation ASTipsController
#pragma mark - Singleton
+ (void)load
{
[NSNotificationCenter.defaultCenter addObserver:self.shared
selector:@selector(windowDidBecomeVisibleWithNotification:)
name:UIWindowDidBecomeVisibleNotification
object:nil];
}
+ (ASTipsController *)shared
{
static ASTipsController *ctrl;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ctrl = [[ASTipsController alloc] init];
});
return ctrl;
}
#pragma mark - Lifecycle
- (instancetype)init
{
ASDisplayNodeAssertMainThread();
if (self = [super init]) {
_nodeToTipStates = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality) valueOptions:NSPointerFunctionsStrongMemory];
_nodesThatAppearedDuringRunLoop = [NSMutableArray array];
}
return self;
}
#pragma mark - Event Handling
- (void)nodeDidAppear:(ASDisplayNode *)node
{
ASDisplayNodeAssertMainThread();
// If they disabled tips on this class, bail.
if (![[node class] enableTips]) {
return;
}
// If this node appeared in some other window (like our tips window) ignore it.
if (ASFindWindowOfLayer(node.layer) != self.appVisibleWindow) {
return;
}
[_nodesThatAppearedDuringRunLoop addObject:node];
}
// If this is a main window, start watching it and clear out our tip window.
- (void)windowDidBecomeVisibleWithNotification:(NSNotification *)notification
{
ASDisplayNodeAssertMainThread();
UIWindow *window = notification.object;
// If this is the same window we're already watching, bail.
if (window == self.appVisibleWindow) {
return;
}
// Ignore windows that are not at the normal level or have empty bounds
if (window.windowLevel != UIWindowLevelNormal || CGRectIsEmpty(window.bounds)) {
return;
}
self.appVisibleWindow = window;
// Create the tip window if needed.
[self createTipWindowIfNeededWithFrame:window.bounds];
// Clear out our tip window and reset our states.
self.tipWindow.mainWindow = window;
[_nodeToTipStates removeAllObjects];
}
- (void)runLoopDidTick
{
NSArray *nodes = [_nodesThatAppearedDuringRunLoop copy];
[_nodesThatAppearedDuringRunLoop removeAllObjects];
// Go through the old array, removing any that have tips but aren't still visible.
for (ASDisplayNode *node in [_nodeToTipStates copy]) {
if (!node.visible) {
[_nodeToTipStates removeObjectForKey:node];
}
}
for (ASDisplayNode *node in nodes) {
// Get the tip state for the node.
ASDisplayNodeTipState *tipState = [_nodeToTipStates objectForKey:node];
// If the node already has a tip, bail. This could change.
if (tipState.tipNode != nil) {
return;
}
for (ASTipProvider *provider in ASTipProvider.all) {
ASTip *tip = [provider tipForNode:node];
if (!tip) { continue; }
if (!tipState) {
tipState = [self createTipStateForNode:node];
}
tipState.tipNode = [[ASTipNode alloc] initWithTip:tip];
}
}
self.tipWindow.nodeToTipStates = _nodeToTipStates;
[self.tipWindow setNeedsLayout];
}
#pragma mark - Internal
- (void)createTipWindowIfNeededWithFrame:(CGRect)tipWindowFrame
{
// Lots of property accesses, but simple safe code, only run once.
if (self.tipWindow == nil) {
self.tipWindow = [[ASTipsWindow alloc] initWithFrame:tipWindowFrame];
self.tipWindow.hidden = NO;
[self setupRunLoopObserver];
}
}
/**
* In order to keep the UI updated, the tips controller registers a run loop observer.
* Before the transaction commit happens, the tips controller calls -setNeedsLayout
* on the view controller's view. It will then layout the main window, and then update the frames
* for tip nodes accordingly.
*/
- (void)setupRunLoopObserver
{
CFRunLoopObserverRef o = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[self runLoopDidTick];
});
CFRunLoopAddObserver(CFRunLoopGetMain(), o, kCFRunLoopCommonModes);
}
- (ASDisplayNodeTipState *)createTipStateForNode:(ASDisplayNode *)node
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeTipState *tipState = [[ASDisplayNodeTipState alloc] initWithNode:node];
[_nodeToTipStates setObject:tipState forKey:node];
return tipState;
}
@end
#endif // AS_ENABLE_TIPS