/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "RCTRootView.h" #import "RCTRootViewDelegate.h" #import "RCTRootViewInternal.h" #import #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTBridge+Private.h" #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" #import "RCTPerformanceLogger.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" #import "RCTUtils.h" #import "RCTView.h" #import "UIView+React.h" #import "RCTProfile.h" #if TARGET_OS_TV #import "RCTTVRemoteHandler.h" #import "RCTTVNavigationEventEmitter.h" #endif NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification"; @interface RCTUIManager (RCTRootView) - (NSNumber *)allocateRootTag; @end @interface RCTRootContentView : RCTView @property (nonatomic, readonly) BOOL contentHasAppeared; @property (nonatomic, readonly, strong) RCTTouchHandler *touchHandler; @property (nonatomic, assign) BOOL passThroughTouches; @property (nonatomic, assign) RCTRootViewSizeFlexibility sizeFlexibility; - (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge reactTag:(NSNumber *)reactTag sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility NS_DESIGNATED_INITIALIZER; @end @implementation RCTRootView { RCTBridge *_bridge; NSString *_moduleName; NSDictionary *_launchOptions; RCTRootContentView *_contentView; BOOL _passThroughTouches; } - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties { RCTAssertMainQueue(); RCTAssert(bridge, @"A bridge instance is required to create an RCTRootView"); RCTAssert(moduleName, @"A moduleName is required to create an RCTRootView"); RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTRootView init]", nil); if ((self = [super initWithFrame:CGRectZero])) { self.backgroundColor = [UIColor whiteColor]; _bridge = bridge; _moduleName = moduleName; _appProperties = [initialProperties copy]; _loadingViewFadeDelay = 0.25; _loadingViewFadeDuration = 0.25; _sizeFlexibility = RCTRootViewSizeFlexibilityNone; self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bridgeDidReload) name:RCTJavaScriptWillStartLoadingNotification object:_bridge]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(javaScriptDidLoad:) name:RCTJavaScriptDidLoadNotification object:_bridge]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hideLoadingView) name:RCTContentDidAppearNotification object:self]; #if TARGET_OS_TV self.tvRemoteHandler = [RCTTVRemoteHandler new]; for (UIGestureRecognizer *gr in self.tvRemoteHandler.tvRemoteGestureRecognizers) { [self addGestureRecognizer:gr]; } #endif if (!_bridge.loading) { [self bundleFinishedLoading:[_bridge batchedBridge]]; } [self showLoadingView]; } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); return self; } - (instancetype)initWithBundleURL:(NSURL *)bundleURL moduleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties launchOptions:(NSDictionary *)launchOptions { RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL moduleProvider:nil launchOptions:launchOptions]; return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties]; } RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) #if TARGET_OS_TV - (UIView *)preferredFocusedView { if (self.reactPreferredFocusedView) { return self.reactPreferredFocusedView; } return [super preferredFocusedView]; } #endif - (void)setBackgroundColor:(UIColor *)backgroundColor { super.backgroundColor = backgroundColor; _contentView.backgroundColor = backgroundColor; } - (BOOL)passThroughTouches { return _contentView.passThroughTouches; } - (void)setPassThroughTouches:(BOOL)passThroughTouches { _passThroughTouches = passThroughTouches; _contentView.passThroughTouches = passThroughTouches; } - (UIViewController *)reactViewController { return _reactViewController ?: [super reactViewController]; } - (BOOL)canBecomeFirstResponder { return YES; } - (void)setLoadingView:(UIView *)loadingView { _loadingView = loadingView; if (!_contentView.contentHasAppeared) { [self showLoadingView]; } } - (void)showLoadingView { if (_loadingView && !_contentView.contentHasAppeared) { _loadingView.hidden = NO; [self addSubview:_loadingView]; } } - (void)hideLoadingView { if (_loadingView.superview == self && _contentView.contentHasAppeared) { if (_loadingViewFadeDuration > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_loadingViewFadeDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [UIView transitionWithView:self duration:self->_loadingViewFadeDuration options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ self->_loadingView.hidden = YES; } completion:^(__unused BOOL finished) { [self->_loadingView removeFromSuperview]; }]; }); } else { _loadingView.hidden = YES; [_loadingView removeFromSuperview]; } } } - (NSNumber *)reactTag { RCTAssertMainQueue(); if (!super.reactTag) { /** * Every root view that is created must have a unique react tag. * Numbering of these tags goes from 1, 11, 21, 31, etc * * NOTE: Since the bridge persists, the RootViews might be reused, so the * react tag must be re-assigned every time a new UIManager is created. */ self.reactTag = [_bridge.uiManager allocateRootTag]; } return super.reactTag; } - (void)bridgeDidReload { RCTAssertMainQueue(); // Clear the reactTag so it can be re-assigned self.reactTag = nil; } - (void)javaScriptDidLoad:(NSNotification *)notification { RCTAssertMainQueue(); // Use the (batched) bridge that's sent in the notification payload, so the // RCTRootContentView is scoped to the right bridge RCTBridge *bridge = notification.userInfo[@"bridge"]; [self bundleFinishedLoading:bridge]; } - (void)bundleFinishedLoading:(RCTBridge *)bridge { if (!bridge.valid) { return; } [_contentView removeFromSuperview]; _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds bridge:bridge reactTag:self.reactTag sizeFlexiblity:_sizeFlexibility]; [self runApplication:bridge]; _contentView.backgroundColor = self.backgroundColor; _contentView.passThroughTouches = _passThroughTouches; [self insertSubview:_contentView atIndex:0]; if (_sizeFlexibility == RCTRootViewSizeFlexibilityNone) { self.intrinsicSize = self.bounds.size; } } - (void)runApplication:(RCTBridge *)bridge { NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": _contentView.reactTag, @"initialProps": _appProperties ?: @{}, }; RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters); [bridge enqueueJSCall:@"AppRegistry" method:@"runApplication" args:@[moduleName, appParameters] completion:NULL]; } - (void)setSizeFlexibility:(RCTRootViewSizeFlexibility)sizeFlexibility { if (_sizeFlexibility == sizeFlexibility) { return; } _sizeFlexibility = sizeFlexibility; [self setNeedsLayout]; _contentView.sizeFlexibility = _sizeFlexibility; } - (void)layoutSubviews { [super layoutSubviews]; _contentView.frame = self.bounds; _loadingView.center = (CGPoint){ CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds) }; } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // The root view itself should never receive touches UIView *hitView = [super hitTest:point withEvent:event]; if (self.passThroughTouches && hitView == self) { return nil; } return hitView; } - (void)setAppProperties:(NSDictionary *)appProperties { RCTAssertMainQueue(); if ([_appProperties isEqualToDictionary:appProperties]) { return; } _appProperties = [appProperties copy]; if (_contentView && _bridge.valid && !_bridge.loading) { [self runApplication:_bridge]; } } - (void)setIntrinsicSize:(CGSize)intrinsicSize { BOOL oldSizeHasAZeroDimension = _intrinsicSize.height == 0 || _intrinsicSize.width == 0; BOOL newSizeHasAZeroDimension = intrinsicSize.height == 0 || intrinsicSize.width == 0; BOOL bothSizesHaveAZeroDimension = oldSizeHasAZeroDimension && newSizeHasAZeroDimension; BOOL sizesAreEqual = CGSizeEqualToSize(_intrinsicSize, intrinsicSize); _intrinsicSize = intrinsicSize; // Don't notify the delegate if the content remains invisible or its size has not changed if (bothSizesHaveAZeroDimension || sizesAreEqual) { return; } [_delegate rootViewDidChangeIntrinsicSize:self]; } - (void)contentViewInvalidated { [_contentView removeFromSuperview]; _contentView = nil; [self showLoadingView]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [_contentView invalidate]; } - (void)cancelTouches { [[_contentView touchHandler] cancel]; } @end @implementation RCTUIManager (RCTRootView) - (NSNumber *)allocateRootTag { NSNumber *rootTag = objc_getAssociatedObject(self, _cmd) ?: @1; objc_setAssociatedObject(self, _cmd, @(rootTag.integerValue + 10), OBJC_ASSOCIATION_RETAIN_NONATOMIC); return rootTag; } @end @implementation RCTRootContentView { __weak RCTBridge *_bridge; UIColor *_backgroundColor; } - (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge reactTag:(NSNumber *)reactTag sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility { if ((self = [super initWithFrame:frame])) { _bridge = bridge; self.reactTag = reactTag; _sizeFlexibility = sizeFlexibility; _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; [_touchHandler attachToView:self]; [_bridge.uiManager registerRootView:self]; self.layer.backgroundColor = NULL; } return self; } RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder:(nonnull NSCoder *)aDecoder) - (void)layoutSubviews { [super layoutSubviews]; [self updateAvailableSize]; } - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; [_bridge.performanceLogger markStopForTag:RCTPLTTI]; dispatch_async(dispatch_get_main_queue(), ^{ if (!self->_contentHasAppeared) { self->_contentHasAppeared = YES; [[NSNotificationCenter defaultCenter] postNotificationName:RCTContentDidAppearNotification object:self.superview]; } }); } - (void)setSizeFlexibility:(RCTRootViewSizeFlexibility)sizeFlexibility { if (_sizeFlexibility == sizeFlexibility) { return; } _sizeFlexibility = sizeFlexibility; [self setNeedsLayout]; } - (void)updateAvailableSize { if (!self.reactTag || !_bridge.isValid) { return; } CGSize size = self.bounds.size; CGSize availableSize = CGSizeMake( _sizeFlexibility & RCTRootViewSizeFlexibilityWidth ? INFINITY : size.width, _sizeFlexibility & RCTRootViewSizeFlexibilityHeight ? INFINITY : size.height ); [_bridge.uiManager setAvailableSize:availableSize forRootView:self]; } - (void)setBackgroundColor:(UIColor *)backgroundColor { _backgroundColor = backgroundColor; if (self.reactTag && _bridge.isValid) { [_bridge.uiManager setBackgroundColor:backgroundColor forView:self]; } } - (UIColor *)backgroundColor { return _backgroundColor; } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // The root content view itself should never receive touches UIView *hitView = [super hitTest:point withEvent:event]; if (_passThroughTouches && hitView == self) { return nil; } return hitView; } - (void)invalidate { if (self.userInteractionEnabled) { self.userInteractionEnabled = NO; [(RCTRootView *)self.superview contentViewInvalidated]; [_bridge enqueueJSCall:@"AppRegistry" method:@"unmountApplicationComponentAtRootTag" args:@[self.reactTag] completion:NULL]; } } @end