Files
react-native/React/Base/RCTRootView.m
T
Héctor Ramos 63fa3f21c5 Add Appearance native module
Summary:
Implements the Appearance native module as discussed in https://github.com/react-native-community/discussions-and-proposals/issues/126.

The purpose of the Appearance native module is to expose the user's appearance preferences. It provides a basic get() API that returns the user's preferred color scheme on iOS 13 devices, also known as Dark Mode. It also provides the ability to subscribe to events whenever an appearance preference changes.

The name, "Appearance", was chosen purposefully to allow for future expansion to cover other appearance preferences such as reduced motion, reduced transparency, or high contrast modes.

Changelog:

[iOS] [Added] - The Appearance native module can be used to prepare your app for Dark Mode on iOS 13.

Reviewed By: yungsters

Differential Revision: D16699954

fbshipit-source-id: 03b4cc5d2a1a69f31f3a6d9bece23f6867b774ea
2019-08-30 17:07:57 -07:00

409 lines
12 KiB
Objective-C

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTRootView.h"
#import "RCTRootViewDelegate.h"
#import "RCTRootViewInternal.h"
#import <objc/runtime.h>
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTBridge+Private.h"
#import "RCTEventDispatcher.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
#import "RCTPerformanceLogger.h"
#import "RCTProfile.h"
#import "RCTRootContentView.h"
#import "RCTTouchHandler.h"
#import "RCTUIManager.h"
#import "RCTUIManagerUtils.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "UIView+React.h"
#if TARGET_OS_TV
#import "RCTTVRemoteHandler.h"
#import "RCTTVNavigationEventEmitter.h"
#endif
NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification";
NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification";
@interface RCTUIManager (RCTRootView)
- (NSNumber *)allocateRootTag;
@end
@implementation RCTRootView
{
RCTBridge *_bridge;
NSString *_moduleName;
RCTRootContentView *_contentView;
BOOL _passThroughTouches;
CGSize _intrinsicContentSize;
}
- (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 (!bridge.isLoading) {
[bridge.performanceLogger markStartForTag:RCTPLTTI];
}
if (self = [super initWithFrame:CGRectZero]) {
self.backgroundColor = [UIColor whiteColor];
_bridge = bridge;
_moduleName = moduleName;
_appProperties = [initialProperties copy];
_loadingViewFadeDelay = 0.25;
_loadingViewFadeDuration = 0.25;
_sizeFlexibility = RCTRootViewSizeFlexibilityNone;
[[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 (NSString *key in [self.tvRemoteHandler.tvRemoteGestureRecognizers allKeys]) {
[self addGestureRecognizer:self.tvRemoteHandler.tvRemoteGestureRecognizers[key]];
}
#endif
[self showLoadingView];
// Immediately schedule the application to be started.
// (Sometimes actual `_bridge` is already batched bridge here.)
[self bundleFinishedLoading:([_bridge batchedBridge] ?: _bridge)];
}
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
#pragma mark - passThroughTouches
- (BOOL)passThroughTouches
{
return _contentView.passThroughTouches;
}
- (void)setPassThroughTouches:(BOOL)passThroughTouches
{
_passThroughTouches = passThroughTouches;
_contentView.passThroughTouches = passThroughTouches;
}
#pragma mark - Layout
- (CGSize)sizeThatFits:(CGSize)size
{
CGSize fitSize = _intrinsicContentSize;
CGSize currentSize = self.bounds.size;
// Following the current `size` and current `sizeFlexibility` policy.
fitSize = CGSizeMake(
_sizeFlexibility & RCTRootViewSizeFlexibilityWidth ? fitSize.width : currentSize.width,
_sizeFlexibility & RCTRootViewSizeFlexibilityHeight ? fitSize.height : currentSize.height
);
// Following the given size constraints.
fitSize = CGSizeMake(
MIN(size.width, fitSize.width),
MIN(size.height, fitSize.height)
);
return fitSize;
}
- (void)layoutSubviews
{
[super layoutSubviews];
_contentView.frame = self.bounds;
_loadingView.center = (CGPoint){
CGRectGetMidX(self.bounds),
CGRectGetMidY(self.bounds)
};
}
- (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 = RCTAllocateRootViewTag();
}
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"];
if (bridge != _contentView.bridge) {
[self bundleFinishedLoading:bridge];
}
}
- (void)bundleFinishedLoading:(RCTBridge *)bridge
{
RCTAssert(bridge != nil, @"Bridge cannot be nil");
if (!bridge.valid) {
return;
}
[_contentView removeFromSuperview];
_contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds
bridge:bridge
reactTag:self.reactTag
sizeFlexiblity:_sizeFlexibility];
[self runApplication:bridge];
_contentView.passThroughTouches = _passThroughTouches;
[self insertSubview:_contentView atIndex:0];
if (_sizeFlexibility == RCTRootViewSizeFlexibilityNone) {
self.intrinsicContentSize = 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;
}
- (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)setIntrinsicContentSize:(CGSize)intrinsicContentSize
{
BOOL oldSizeHasAZeroDimension = _intrinsicContentSize.height == 0 || _intrinsicContentSize.width == 0;
BOOL newSizeHasAZeroDimension = intrinsicContentSize.height == 0 || intrinsicContentSize.width == 0;
BOOL bothSizesHaveAZeroDimension = oldSizeHasAZeroDimension && newSizeHasAZeroDimension;
BOOL sizesAreEqual = CGSizeEqualToSize(_intrinsicContentSize, intrinsicContentSize);
_intrinsicContentSize = intrinsicContentSize;
// Don't notify the delegate if the content remains invisible or its size has not changed
if (bothSizesHaveAZeroDimension || sizesAreEqual) {
return;
}
[self invalidateIntrinsicContentSize];
[self.superview setNeedsLayout];
[_delegate rootViewDidChangeIntrinsicSize:self];
}
- (CGSize)intrinsicContentSize
{
return _intrinsicContentSize;
}
- (void)contentViewInvalidated
{
[_contentView removeFromSuperview];
_contentView = nil;
[self showLoadingView];
}
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
if (@available(iOS 13.0, *)) {
if ([previousTraitCollection hasDifferentColorAppearanceComparedToTraitCollection:self.traitCollection]) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTUserInterfaceStyleDidChangeNotification
object:self
userInfo:@{@"traitCollection": self.traitCollection}];
}
}
}
#endif
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_contentView invalidate];
}
@end
@implementation RCTRootView (Deprecated)
- (CGSize)intrinsicSize
{
RCTLogWarn(@"Calling deprecated `[-RCTRootView intrinsicSize]`.");
return self.intrinsicContentSize;
}
- (void)cancelTouches
{
RCTLogWarn(@"`-[RCTRootView cancelTouches]` is deprecated and will be deleted soon.");
[[_contentView touchHandler] cancel];
}
@end