mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
63fa3f21c5
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
409 lines
12 KiB
Objective-C
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
|