Add RCTDevSettings module

Summary:
This decouples non-UI logic from RCTDevMenu into a new module RCTDevSettings.

**Motivation**: This allows developers to change dev settings without depending on the built-in dev menu, e.g. if they want to introduce their own UI, or have other devtools logic that doesn't depend on an action sheet.

It also introduces the RCTDevSettingsDataSource protocol for storing dev tools preferences. This could allow a developer to implement alternative behaviors, e.g. loading the settings from some other config, changing settings based on the user, deciding not to persist some settings, or something else.

The included data source implementation, RCTDevSettingsUserDefaultsDataSource, uses NSUserDefaults and is backwards compatible with the older implementation, so **no workflows or dependent code will break, and old saved settings will persist.**

The RCTDevMenu interface has not changed and is therefore also backwards-compatible, though
some methods are now deprecated.

In order to ensure that RCTDevSettings
Closes https://github.com/facebook/react-native/pull/11613

Reviewed By: mmmulani

Differential Revision: D4571773

Pulled By: javache

fbshipit-source-id: 25555d0a6eaa81f694343e079ed02439e5845fbc
This commit is contained in:
Ben Roth 2017-02-24 06:50:29 -08:00 committed by Facebook Github Bot
parent bb1f85183b
commit 6a14f0b449
10 changed files with 1099 additions and 832 deletions

View File

@ -56,6 +56,7 @@ RCT_EXPORT_MODULE()
if (!_url) {
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
// TODO t16297016: this seems to be unused, remove?
NSInteger port = [standardDefaults integerForKey:@"websocket-executor-port"];
if (!port) {
port = [[[_bridge bundleURL] port] integerValue] ?: 8081;

View File

@ -23,7 +23,7 @@
#import <React/RCTCxxModule.h>
#import <React/RCTCxxUtils.h>
#import <React/RCTDevLoadingView.h>
#import <React/RCTDevMenu.h>
#import <React/RCTDevSettings.h>
#import <React/RCTDisplayLink.h>
#import <React/RCTJavaScriptLoader.h>
#import <React/RCTLog.h>
@ -403,7 +403,7 @@ struct RCTInstanceCallback : public InstanceCallback {
executorFactory.reset(new JSCExecutorFactory("", folly::dynamic::object
("UseCustomJSC", (bool)useCustomJSC)
#if RCT_PROFILE
("StartSamplingProfilerOnInit", (bool)self.devMenu.startSamplingProfilerOnLaunch)
("StartSamplingProfilerOnInit", (bool)self.devSettings.startSamplingProfilerOnLaunch)
#endif
));
} else {

View File

@ -25,6 +25,7 @@
#import "RCTBridge+Private.h"
#import "RCTDefines.h"
#import "RCTDevMenu.h"
#import "RCTDevSettings.h"
#import "RCTJSCErrorHandling.h"
#import "RCTJSCProfiler.h"
#import "RCTJavaScriptLoader.h"
@ -38,8 +39,6 @@ NSString *const RCTJavaScriptContextCreatedNotification = @"RCTJavaScriptContext
RCT_EXTERN NSString *const RCTFBJSContextClassKey = @"_RCTFBJSContextClassKey";
RCT_EXTERN NSString *const RCTFBJSValueClassKey = @"_RCTFBJSValueClassKey";
static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled";
struct __attribute__((packed)) ModuleData {
uint32_t offset;
uint32_t size;
@ -168,15 +167,21 @@ RCT_EXPORT_MODULE()
#if RCT_DEV
static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
{
__weak RCTBridge *weakBridge = bridge;
__weak RCTDevSettings *devSettings = bridge.devSettings;
if (RCTJSCProfilerIsSupported()) {
[bridge.devMenu addItem:[RCTDevMenuItem toggleItemWithKey:RCTJSCProfilerEnabledDefaultsKey title:@"Start Profiling" selectedTitle:@"Stop Profiling" handler:^(BOOL shouldStart) {
[bridge.devMenu addItem:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return devSettings.isJSCProfilingEnabled ? @"Stop Profiling" : @"Start Profiling";
} handler:^{
BOOL shouldStart = !devSettings.isJSCProfilingEnabled;
devSettings.isJSCProfilingEnabled = shouldStart;
if (shouldStart != RCTJSCProfilerIsProfiling(context)) {
if (shouldStart) {
RCTJSCProfilerStart(context);
} else {
NSString *outputFile = RCTJSCProfilerStop(context);
NSData *profileData = [NSData dataWithContentsOfFile:outputFile options:NSDataReadingMappedIfSafe error:NULL];
RCTProfileSendResult(bridge, @"cpu-profile", profileData);
RCTProfileSendResult(weakBridge, @"cpu-profile", profileData);
}
}
}]];
@ -309,7 +314,7 @@ static NSThread *newJavaScriptThread(void)
} else {
if (self->_useCustomJSCLibrary) {
JSC_configureJSCForIOS(true, RCTJSONStringify(@{
@"StartSamplingProfilerOnInit": @(self->_bridge.devMenu.startSamplingProfilerOnLaunch)
@"StartSamplingProfilerOnInit": @(self->_bridge.devSettings.startSamplingProfilerOnLaunch)
}, NULL).UTF8String);
}
contextRef = JSC_JSGlobalContextCreateInGroup(self->_useCustomJSCLibrary, nullptr, nullptr);
@ -401,16 +406,7 @@ static NSThread *newJavaScriptThread(void)
if (!strongSelf.valid || !weakContext) {
return;
}
// JSPokeSamplingProfiler() toggles the profiling process
JSGlobalContextRef ctx = weakContext.JSGlobalContextRef;
JSValueRef jsResult = JSC_JSPokeSamplingProfiler(ctx);
if (JSC_JSValueGetType(ctx, jsResult) != kJSTypeNull) {
NSString *results = [[JSC_JSValue(ctx) valueWithJSValueRef:jsResult inContext:weakContext] toObject];
JSCSamplingProfiler *profilerModule = [strongSelf->_bridge moduleForClass:[JSCSamplingProfiler class]];
[profilerModule operationCompletedWithResults:results];
}
[weakSelf.bridge.devSettings toggleJSCSamplingProfiler];
}]];
// Allow for the profiler to be poked from JS code as well

View File

@ -20,59 +20,44 @@
@interface RCTDevMenu : NSObject
/**
* Is the menu enabled. The menu is enabled by default if RCT_DEV=1, but
* you may wish to disable it so that you can provide your own shake handler.
* Deprecated, use RCTDevSettings instead.
*/
@property (nonatomic, assign) BOOL shakeToShow;
@property (nonatomic, assign) BOOL shakeToShow DEPRECATED_ATTRIBUTE;
/**
* Enables performance profiling.
* Deprecated, use RCTDevSettings instead.
*/
@property (nonatomic, assign) BOOL profilingEnabled;
@property (nonatomic, assign) BOOL profilingEnabled DEPRECATED_ATTRIBUTE;
/**
* Enables starting of profiling sampler on launch
* Deprecated, use RCTDevSettings instead.
*/
@property (nonatomic, assign) BOOL startSamplingProfilerOnLaunch;
@property (nonatomic, assign) BOOL liveReloadEnabled DEPRECATED_ATTRIBUTE;
/**
* Enables automatic polling for JS code changes. Only applicable when
* running the app from a server.
* Deprecated, use RCTDevSettings instead.
*/
@property (nonatomic, assign) BOOL liveReloadEnabled;
/**
* Enables hot loading. Currently not supported in open source.
*/
@property (nonatomic, assign) BOOL hotLoadingEnabled;
/**
* Shows the FPS monitor for the JS and Main threads.
*/
@property (nonatomic, assign) BOOL showFPS;
@property (nonatomic, assign) BOOL hotLoadingEnabled DEPRECATED_ATTRIBUTE;
/**
* Presented items in development menu
*/
@property (nonatomic, copy, readonly) NSArray<RCTDevMenuItem *> *presentedItems;
/**
* Detect if actions sheet (development menu) is shown
*/
- (BOOL)isActionSheetShown;
/**
* Manually show the dev menu (can be called from JS).
*/
- (void)show;
/**
* Manually reload the application. Equivalent to calling [bridge reload]
* directly, but can be called from JS.
* Deprecated, use -[RCTBRidge reload] instead.
*/
- (void)reload;
- (void)reload DEPRECATED_ATTRIBUTE;
/**
* Deprecated. Use the `-addItem:` method instead.
@ -88,6 +73,8 @@
@end
typedef NSString *(^RCTDevMenuItemTitleBlock)(void);
/**
* Developer menu item, used to expose additional functionality via the menu.
*/
@ -98,18 +85,16 @@
* action.
*/
+ (instancetype)buttonItemWithTitle:(NSString *)title
handler:(void(^)(void))handler;
handler:(dispatch_block_t)handler;
/**
* This creates an item with a toggle behavior. The key is used to store the
* state of the toggle. For toggle items, the handler will be called immediately
* after the item is added if the item was already selected when the module was
* last loaded.
* This creates an item with a simple push-button interface, used to trigger an
* action. getTitleForPresentation is called each time the item is about to be
* presented, and should return the item's title.
*/
+ (instancetype)toggleItemWithKey:(NSString *)key
title:(NSString *)title
selectedTitle:(NSString *)selectedTitle
handler:(void(^)(BOOL selected))handler;
+ (instancetype)buttonItemWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock
handler:(dispatch_block_t)handler;
@end
/**

387
React/Modules/RCTDevMenu.m Normal file
View File

@ -0,0 +1,387 @@
/**
* 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 "RCTDevMenu.h"
#import "RCTDevSettings.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
#import "RCTUtils.h"
#if RCT_DEV
static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
@implementation UIWindow (RCTDevMenu)
- (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
}
}
@end
@implementation RCTDevMenuItem
{
RCTDevMenuItemTitleBlock _titleBlock;
dispatch_block_t _handler;
}
- (instancetype)initWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock
handler:(dispatch_block_t)handler
{
if ((self = [super init])) {
_titleBlock = [titleBlock copy];
_handler = [handler copy];
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
+ (instancetype)buttonItemWithTitleBlock:(NSString *(^)(void))titleBlock handler:(dispatch_block_t)handler
{
return [[self alloc] initWithTitleBlock:titleBlock handler:handler];
}
+ (instancetype)buttonItemWithTitle:(NSString *)title
handler:(dispatch_block_t)handler
{
return [[self alloc] initWithTitleBlock:^NSString *{ return title; } handler:handler];
}
- (void)callHandler
{
if (_handler) {
_handler();
}
}
- (NSString *)title
{
if (_titleBlock) {
return _titleBlock();
}
return nil;
}
@end
typedef void(^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating>
@end
@implementation RCTDevMenu
{
UIAlertController *_actionSheet;
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
}
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (void)initialize
{
// We're swizzling here because it's poor form to override methods in a category,
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
// no need to call the original implementation.
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
}
- (instancetype)init
{
if ((self = [super init])) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showOnShake)
name:RCTShowDevMenuNotification
object:nil];
_extraMenuItems = [NSMutableArray new];
#if TARGET_IPHONE_SIMULATOR
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
__weak __typeof(self) weakSelf = self;
// Toggle debug menu
[commands registerKeyCommandWithInput:@"d"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[weakSelf toggle];
}];
// Toggle element inspector
[commands registerKeyCommandWithInput:@"i"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[weakSelf.bridge.devSettings toggleElementInspector];
}];
// Reload in normal mode
[commands registerKeyCommandWithInput:@"n"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[weakSelf.bridge.devSettings setIsDebuggingRemotely:NO];
}];
#endif
}
return self;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
_presentedItems = nil;
[_actionSheet dismissViewControllerAnimated:YES completion:^(void){}];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)showOnShake
{
if ([_bridge.devSettings isShakeToShowDevMenuEnabled]) {
[self show];
}
}
- (void)toggle
{
if (_actionSheet) {
[_actionSheet dismissViewControllerAnimated:YES completion:^(void){}];
_actionSheet = nil;
} else {
[self show];
}
}
- (BOOL)isActionSheetShown
{
return _actionSheet != nil;
}
- (void)addItem:(NSString *)title handler:(void(^)(void))handler
{
[self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
}
- (void)addItem:(RCTDevMenuItem *)item
{
[_extraMenuItems addObject:item];
}
- (NSArray<RCTDevMenuItem *> *)_menuItemsToPresent
{
NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
// Add built-in items
__weak RCTBridge *bridge = _bridge;
__weak RCTDevSettings *devSettings = _bridge.devSettings;
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload" handler:^{
[bridge reload];
}]];
NSString *executorName = devSettings.websocketExecutorName ?: @"Remote JS";
if (!devSettings.isRemoteDebuggingAvailable) {
[items addObject:[RCTDevMenuItem buttonItemWithTitle:[NSString stringWithFormat:@"%@ Debugger Unavailable", executorName] handler:^{
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:[NSString stringWithFormat:@"%@ Debugger Unavailable", executorName]
message:[NSString stringWithFormat:@"You need to include the RCTWebSocket library to enable %@ debugging", executorName]
preferredStyle:UIAlertControllerStyleAlert];
[RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
}]];
} else {
[items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return devSettings.isDebuggingRemotely ?
[NSString stringWithFormat:@"Stop %@ Debugging", executorName] :
[NSString stringWithFormat:@"Debug %@", executorName];
} handler:^{
devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
}]];
}
if (devSettings.isLiveReloadAvailable) {
[items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return devSettings.isLiveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
} handler:^{
devSettings.isLiveReloadEnabled = !devSettings.isLiveReloadEnabled;
}]];
[items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return devSettings.isProfilingEnabled ? @"Stop Systrace" : @"Start Systrace";
} handler:^{
devSettings.isProfilingEnabled = !devSettings.isProfilingEnabled;
}]];
}
if (_bridge.devSettings.isHotLoadingAvailable) {
[items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return devSettings.isHotLoadingEnabled ? @"Disable Hot Reloading" : @"Enable Hot Reloading";
} handler:^{
devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
}]];
}
if (devSettings.isJSCSamplingProfilerAvailable) {
// Note: bridge.jsContext is not implemented in the old bridge, so this code is
// duplicated in RCTJSCExecutor
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Start / Stop JS Sampling Profiler" handler:^{
[devSettings toggleJSCSamplingProfiler];
}]];
}
[items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return (devSettings.isElementInspectorShown) ? @"Hide Inspector" : @"Show Inspector";
} handler:^{
[devSettings toggleElementInspector];
}]];
[items addObjectsFromArray:_extraMenuItems];
return items;
}
RCT_EXPORT_METHOD(show)
{
if (_actionSheet || !_bridge || RCTRunningInAppExtension()) {
return;
}
NSString *title = [NSString stringWithFormat:@"React Native: Development (%@)", [_bridge class]];
// On larger devices we don't have an anchor point for the action sheet
UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone ? UIAlertControllerStyleActionSheet : UIAlertControllerStyleAlert;
_actionSheet = [UIAlertController alertControllerWithTitle:title
message:@""
preferredStyle:style];
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
for (RCTDevMenuItem *item in items) {
[_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
style:UIAlertActionStyleDefault
handler:[self alertActionHandlerForDevItem:item]]];
}
[_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:[self alertActionHandlerForDevItem:nil]]];
_presentedItems = items;
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
}
- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
{
return ^(__unused UIAlertAction *action) {
if (item) {
[item callHandler];
}
self->_actionSheet = nil;
};
}
#pragma mark - deprecated methods and properties
#define WARN_DEPRECATED_DEV_MENU_EXPORT() RCTLogWarn(@"Using deprecated method %s, use RCTDevSettings instead", __func__)
- (void)setShakeToShow:(BOOL)shakeToShow
{
_bridge.devSettings.isShakeToShowDevMenuEnabled = shakeToShow;
}
- (BOOL)shakeToShow
{
return _bridge.devSettings.isShakeToShowDevMenuEnabled;
}
RCT_EXPORT_METHOD(reload)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
[_bridge reload];
}
RCT_EXPORT_METHOD(debugRemotely:(BOOL)enableDebug)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
_bridge.devSettings.isDebuggingRemotely = enableDebug;
}
RCT_EXPORT_METHOD(setProfilingEnabled:(BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
_bridge.devSettings.isProfilingEnabled = enabled;
}
- (BOOL)profilingEnabled
{
return _bridge.devSettings.isProfilingEnabled;
}
RCT_EXPORT_METHOD(setLiveReloadEnabled:(BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
_bridge.devSettings.isLiveReloadEnabled = enabled;
}
- (BOOL)liveReloadEnabled
{
return _bridge.devSettings.isLiveReloadEnabled;
}
RCT_EXPORT_METHOD(setHotLoadingEnabled:(BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
_bridge.devSettings.isHotLoadingEnabled = enabled;
}
- (BOOL)hotLoadingEnabled
{
return _bridge.devSettings.isHotLoadingEnabled;
}
@end
#else // Unavailable when not in dev mode
@implementation RCTDevMenu
- (void)show {}
- (void)reload {}
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {}
- (void)addItem:(RCTDevMenu *)item {}
- (BOOL)isActionSheetShown { return NO; }
@end
@implementation RCTDevMenuItem
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(void(^)(void))handler {return nil;}
+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock
handler:(void(^)(void))handler {return nil;}
@end
#endif
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
#if RCT_DEV
return [self moduleForClass:[RCTDevMenu class]];
#else
return nil;
#endif
}
@end

View File

@ -1,763 +0,0 @@
/**
* 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 "RCTDevMenu.h"
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h>
#import <jschelpers/JavaScriptCore.h>
#import "JSCSamplingProfiler.h"
#import "RCTAssert.h"
#import "RCTBridge+Private.h"
#import "RCTDefines.h"
#import "RCTEventDispatcher.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
#import "RCTPackagerClient.h"
#import "RCTProfile.h"
#import "RCTReloadPackagerMethod.h"
#import "RCTRootView.h"
#import "RCTUtils.h"
#if RCT_DEV
static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu";
@implementation UIWindow (RCTDevMenu)
- (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
}
}
@end
typedef NS_ENUM(NSInteger, RCTDevMenuType) {
RCTDevMenuTypeButton,
RCTDevMenuTypeToggle
};
@interface RCTDevMenuItem ()
@property (nonatomic, assign, readonly) RCTDevMenuType type;
@property (nonatomic, copy, readonly) NSString *key;
@property (nonatomic, copy) id value;
@end
@implementation RCTDevMenuItem
{
id _handler; // block
NSString *_title;
NSString *_selectedTitle;
}
- (instancetype)initWithType:(RCTDevMenuType)type
key:(NSString *)key
title:(NSString *)title
selectedTitle:(NSString *)selectedTitle
handler:(id /* block */)handler
{
if ((self = [super init])) {
_type = type;
_key = [key copy];
_title = [title copy];
_selectedTitle = [selectedTitle copy];
_handler = [handler copy];
_value = nil;
}
return self;
}
- (NSString *)title
{
if (_type == RCTDevMenuTypeToggle && [_value boolValue]) {
return _selectedTitle;
}
return _title;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
+ (instancetype)buttonItemWithTitle:(NSString *)title
handler:(void (^)(void))handler
{
return [[self alloc] initWithType:RCTDevMenuTypeButton
key:nil
title:title
selectedTitle:nil
handler:handler];
}
+ (instancetype)toggleItemWithKey:(NSString *)key
title:(NSString *)title
selectedTitle:(NSString *)selectedTitle
handler:(void (^)(BOOL selected))handler
{
return [[self alloc] initWithType:RCTDevMenuTypeToggle
key:key
title:title
selectedTitle:selectedTitle
handler:handler];
}
- (void)callHandler
{
switch (_type) {
case RCTDevMenuTypeButton: {
if (_handler) {
((void(^)())_handler)();
}
break;
}
case RCTDevMenuTypeToggle: {
if (_handler) {
((void(^)(BOOL selected))_handler)([_value boolValue]);
}
break;
}
}
}
@end
typedef void(^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating>
@property (nonatomic, strong) Class executorClass;
@end
@implementation RCTDevMenu
{
UIAlertController *_actionSheet;
NSUserDefaults *_defaults;
NSMutableDictionary *_settings;
NSURLSessionDataTask *_updateTask;
NSURL *_liveReloadURL;
BOOL _jsLoaded;
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
NSString *_webSocketExecutorName;
NSString *_executorOverride;
}
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (void)initialize
{
// We're swizzling here because it's poor form to override methods in a category,
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
// no need to call the original implementation.
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
}
- (instancetype)init
{
if ((self = [super init])) {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:@selector(showOnShake)
name:RCTShowDevMenuNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(settingsDidChange)
name:NSUserDefaultsDidChangeNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(jsLoaded:)
name:RCTJavaScriptDidLoadNotification
object:nil];
_defaults = [NSUserDefaults standardUserDefaults];
_settings = [[NSMutableDictionary alloc] initWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]];
_extraMenuItems = [NSMutableArray new];
__weak RCTDevMenu *weakSelf = self;
[_extraMenuItems addObject:[RCTDevMenuItem toggleItemWithKey:@"showInspector"
title:@"Show Inspector"
selectedTitle:@"Hide Inspector"
handler:^(__unused BOOL enabled)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
#pragma clang diagnostic pop
}]];
_webSocketExecutorName = [_defaults objectForKey:@"websocket-executor-name"] ?: @"JS Remotely";
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->_executorOverride = [self->_defaults objectForKey:@"executor-override"];
});
// Same values are read during the bridge starup path
_startSamplingProfilerOnLaunch = [_settings[@"startSamplingProfilerOnLaunch"] boolValue];
// Delay setup until after Bridge init
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateSettings:self->_settings];
[weakSelf connectPackager];
});
#if TARGET_IPHONE_SIMULATOR
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
// Toggle debug menu
[commands registerKeyCommandWithInput:@"d"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[weakSelf toggle];
}];
// Toggle element inspector
[commands registerKeyCommandWithInput:@"i"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[weakSelf.bridge.eventDispatcher
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
sendDeviceEventWithName:@"toggleElementInspector"
body:nil];
#pragma clang diagnostic pop
}];
// Reload in normal mode
[commands registerKeyCommandWithInput:@"n"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
weakSelf.executorClass = Nil;
}];
#endif
}
return self;
}
- (NSURL *)packagerURL
{
NSString *host = [_bridge.bundleURL host];
NSString *scheme = [_bridge.bundleURL scheme];
if (!host) {
host = @"localhost";
scheme = @"http";
}
NSNumber *port = [_bridge.bundleURL port];
if (!port) {
port = @8081; // Packager default port
}
return [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%@/message?role=ios-rn-rctdevmenu", scheme, host, port]];
}
// TODO: Move non-UI logic into separate RCTDevSettings module
- (void)connectPackager
{
RCTAssertMainQueue();
NSURL *url = [self packagerURL];
if (!url) {
return;
}
// The jsPackagerClient is a static map that holds different packager clients per the packagerURL
// In case many instances of DevMenu are created, the latest instance that use the same URL as
// previous instances will override given packager client's method handlers
static NSMutableDictionary<NSString *, RCTPackagerClient *> *jsPackagerClients = nil;
if (jsPackagerClients == nil) {
jsPackagerClients = [NSMutableDictionary new];
}
NSString *key = [url absoluteString];
RCTPackagerClient *packagerClient = jsPackagerClients[key];
if (!packagerClient) {
packagerClient = [[RCTPackagerClient alloc] initWithURL:url];
jsPackagerClients[key] = packagerClient;
} else {
[packagerClient stop];
}
[packagerClient addHandler:[[RCTReloadPackagerMethod alloc] initWithBridge:_bridge]
forMethod:@"reload"];
[packagerClient start];
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)settingsDidChange
{
// Needed to prevent a race condition when reloading
__weak RCTDevMenu *weakSelf = self;
NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateSettings:settings];
});
}
/**
* This method loads the settings from NSUserDefaults and overrides any local
* settings with them. It should only be called on app launch, or after the app
* has returned from the background, when the settings might have been edited
* outside of the app.
*/
- (void)updateSettings:(NSDictionary *)settings
{
[_settings setDictionary:settings];
// Fire handlers for items whose values have changed
for (RCTDevMenuItem *item in _extraMenuItems) {
if (item.key) {
id value = settings[item.key];
if (value != item.value && ![value isEqual:item.value]) {
item.value = value;
[item callHandler];
}
}
}
self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue];
self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue];
self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue];
self.hotLoadingEnabled = [_settings[@"hotLoadingEnabled"] ?: @NO boolValue];
self.showFPS = [_settings[@"showFPS"] ?: @NO boolValue];
self.executorClass = NSClassFromString(_executorOverride ?: _settings[@"executorClass"]);
}
/**
* This updates a particular setting, and then saves the settings. Because all
* settings are overwritten by this, it's important that this is not called
* before settings have been loaded initially, otherwise the other settings
* will be reset.
*/
- (void)updateSetting:(NSString *)name value:(id)value
{
// Fire handler for item whose values has changed
for (RCTDevMenuItem *item in _extraMenuItems) {
if ([item.key isEqualToString:name]) {
if (value != item.value && ![value isEqual:item.value]) {
item.value = value;
[item callHandler];
}
break;
}
}
// Save the setting
id currentValue = _settings[name];
if (currentValue == value || [currentValue isEqual:value]) {
return;
}
if (value) {
_settings[name] = value;
} else {
[_settings removeObjectForKey:name];
}
[_defaults setObject:_settings forKey:RCTDevMenuSettingsKey];
[_defaults synchronize];
}
- (void)jsLoaded:(NSNotification *)notification
{
if (notification.userInfo[@"bridge"] != _bridge) {
return;
}
_jsLoaded = YES;
// Check if live reloading is available
NSURL *scriptURL = _bridge.bundleURL;
if (![scriptURL isFileURL]) {
// Live reloading is disabled when running from bundled JS file
_liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:scriptURL];
} else {
_liveReloadURL = nil;
}
dispatch_async(dispatch_get_main_queue(), ^{
// Hit these setters again after bridge has finished loading
self.profilingEnabled = self->_profilingEnabled;
self.liveReloadEnabled = self->_liveReloadEnabled;
self.executorClass = self->_executorClass;
// Inspector can only be shown after JS has loaded
if ([self->_settings[@"showInspector"] boolValue]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
#pragma clang diagnostic pop
}
});
}
- (void)invalidate
{
_presentedItems = nil;
[_updateTask cancel];
[_actionSheet dismissViewControllerAnimated:YES completion:^(void){}];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)showOnShake
{
if (_shakeToShow) {
[self show];
}
}
- (void)toggle
{
if (_actionSheet) {
[_actionSheet dismissViewControllerAnimated:YES completion:^(void){}];
_actionSheet = nil;
} else {
[self show];
}
}
- (void)addItem:(NSString *)title handler:(void(^)(void))handler
{
[self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
}
- (void)addItem:(RCTDevMenuItem *)item
{
[_extraMenuItems addObject:item];
// Fire handler for items whose saved value doesn't match the default
[self settingsDidChange];
}
- (NSArray<RCTDevMenuItem *> *)menuItems
{
NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
// Add built-in items
__weak RCTDevMenu *weakSelf = self;
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload" handler:^{
[weakSelf reload];
}]];
Class jsDebuggingExecutorClass = objc_lookUpClass("RCTWebSocketExecutor");
if (!jsDebuggingExecutorClass) {
[items addObject:[RCTDevMenuItem buttonItemWithTitle:[NSString stringWithFormat:@"%@ Debugger Unavailable", _webSocketExecutorName] handler:^{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@ Debugger Unavailable", self->_webSocketExecutorName]
message:[NSString stringWithFormat:@"You need to include the RCTWebSocket library to enable %@ debugging", self->_webSocketExecutorName]
preferredStyle:UIAlertControllerStyleAlert];
[RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
}]];
} else {
BOOL isDebuggingJS = _executorClass && _executorClass == jsDebuggingExecutorClass;
NSString *debuggingDescription = [_defaults objectForKey:@"websocket-executor-name"] ?: @"Remote JS";
NSString *debugTitleJS = isDebuggingJS ? [NSString stringWithFormat:@"Stop %@ Debugging", debuggingDescription] : [NSString stringWithFormat:@"Debug %@", _webSocketExecutorName];
[items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleJS handler:^{
weakSelf.executorClass = isDebuggingJS ? Nil : jsDebuggingExecutorClass;
}]];
}
if (_liveReloadURL) {
NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
[items addObject:[RCTDevMenuItem buttonItemWithTitle:liveReloadTitle handler:^{
__typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.liveReloadEnabled = !strongSelf->_liveReloadEnabled;
}
}]];
NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Systrace" : @"Start Systrace";
[items addObject:[RCTDevMenuItem buttonItemWithTitle:profilingTitle handler:^{
__typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.profilingEnabled = !strongSelf->_profilingEnabled;
}
}]];
}
if ([self hotLoadingAvailable]) {
NSString *hotLoadingTitle = _hotLoadingEnabled ? @"Disable Hot Reloading" : @"Enable Hot Reloading";
[items addObject:[RCTDevMenuItem buttonItemWithTitle:hotLoadingTitle handler:^{
__typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.hotLoadingEnabled = !strongSelf->_hotLoadingEnabled;
}
}]];
}
// Add toggles for JSC's sampling profiler, if the profiler is enabled
// Note: bridge.jsContext is not implemented in the old bridge, so this code is
// duplicated in RCTJSCExecutor
if (JSC_JSSamplingProfilerEnabled(self->_bridge.jsContext.JSGlobalContextRef)) {
JSContext *context = self->_bridge.jsContext;
// Allow to toggle the sampling profiler through RN's dev menu
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Start / Stop JS Sampling Profiler" handler:^{
JSGlobalContextRef globalContext = context.JSGlobalContextRef;
// JSPokeSamplingProfiler() toggles the profiling process
JSValueRef jsResult = JSC_JSPokeSamplingProfiler(globalContext);
if (JSC_JSValueGetType(globalContext, jsResult) != kJSTypeNull) {
NSString *results = [[JSC_JSValue(globalContext) valueWithJSValueRef:jsResult inContext:context] toObject];
JSCSamplingProfiler *profilerModule = [self->_bridge moduleForClass:[JSCSamplingProfiler class]];
[profilerModule operationCompletedWithResults:results];
}
}]];
}
[items addObjectsFromArray:_extraMenuItems];
return items;
}
RCT_EXPORT_METHOD(show)
{
if (_actionSheet || !_bridge || RCTRunningInAppExtension()) {
return;
}
NSString *title = [NSString stringWithFormat:@"React Native: Development (%@)", [_bridge class]];
// On larger devices we don't have an anchor point for the action sheet
UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone ? UIAlertControllerStyleActionSheet : UIAlertControllerStyleAlert;
_actionSheet = [UIAlertController alertControllerWithTitle:title
message:@""
preferredStyle:style];
NSArray<RCTDevMenuItem *> *items = [self menuItems];
for (RCTDevMenuItem *item in items) {
[_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
style:UIAlertActionStyleDefault
handler:[self alertActionHandlerForDevItem:item]]];
}
[_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:[self alertActionHandlerForDevItem:nil]]];
_presentedItems = items;
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
}
- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
{
return ^(__unused UIAlertAction *action) {
if (item) {
switch (item.type) {
case RCTDevMenuTypeButton: {
[item callHandler];
break;
}
case RCTDevMenuTypeToggle: {
BOOL value = [self->_settings[item.key] boolValue];
[self updateSetting:item.key value:@(!value)]; // will call handler
break;
}
}
}
self->_actionSheet = nil;
};
}
RCT_EXPORT_METHOD(reload)
{
[_bridge reload];
}
RCT_EXPORT_METHOD(debugRemotely:(BOOL)enableDebug)
{
Class jsDebuggingExecutorClass = NSClassFromString(@"RCTWebSocketExecutor");
self.executorClass = enableDebug ? jsDebuggingExecutorClass : nil;
}
- (void)setShakeToShow:(BOOL)shakeToShow
{
_shakeToShow = shakeToShow;
[self updateSetting:@"shakeToShow" value:@(_shakeToShow)];
}
- (void)setStartSamplingProfilerOnLaunch:(BOOL)startSamplingProfilerOnLaunch
{
_startSamplingProfilerOnLaunch = startSamplingProfilerOnLaunch;
[self updateSetting:@"startSamplingProfilerOnLaunch" value:@(_startSamplingProfilerOnLaunch)];
}
RCT_EXPORT_METHOD(setProfilingEnabled:(BOOL)enabled)
{
_profilingEnabled = enabled;
[self updateSetting:@"profilingEnabled" value:@(_profilingEnabled)];
if (_liveReloadURL && enabled != RCTProfileIsProfiling()) {
if (enabled) {
[_bridge startProfiling];
} else {
[_bridge stopProfiling:^(NSData *logData) {
RCTProfileSendResult(self->_bridge, @"systrace", logData);
}];
}
}
}
RCT_EXPORT_METHOD(setLiveReloadEnabled:(BOOL)enabled)
{
_liveReloadEnabled = enabled;
[self updateSetting:@"liveReloadEnabled" value:@(_liveReloadEnabled)];
if (_liveReloadEnabled) {
[self checkForUpdates];
} else {
[_updateTask cancel];
_updateTask = nil;
}
}
- (BOOL)hotLoadingAvailable
{
return _bridge.bundleURL && !_bridge.bundleURL.fileURL; // Only works when running from server
}
RCT_EXPORT_METHOD(setHotLoadingEnabled:(BOOL)enabled)
{
_hotLoadingEnabled = enabled;
[self updateSetting:@"hotLoadingEnabled" value:@(_hotLoadingEnabled)];
BOOL actuallyEnabled = [self hotLoadingAvailable] && _hotLoadingEnabled;
if (RCTGetURLQueryParam(_bridge.bundleURL, @"hot").boolValue != actuallyEnabled) {
_bridge.bundleURL = RCTURLByReplacingQueryParam(_bridge.bundleURL, @"hot",
actuallyEnabled ? @"true" : nil);
[_bridge reload];
}
}
- (void)setExecutorClass:(Class)executorClass
{
if (_executorClass != executorClass) {
_executorClass = executorClass;
_executorOverride = nil;
[self updateSetting:@"executorClass" value:NSStringFromClass(executorClass)];
}
if (_bridge.executorClass != executorClass) {
// TODO (6929129): we can remove this special case test once we have better
// support for custom executors in the dev menu. But right now this is
// needed to prevent overriding a custom executor with the default if a
// custom executor has been set directly on the bridge
if (executorClass == Nil &&
_bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
return;
}
_bridge.executorClass = executorClass;
[_bridge reload];
}
}
- (void)setShowFPS:(BOOL)showFPS
{
_showFPS = showFPS;
[self updateSetting:@"showFPS" value:@(showFPS)];
}
- (void)checkForUpdates
{
if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) {
return;
}
if (_updateTask) {
return;
}
__weak RCTDevMenu *weakSelf = self;
_updateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:
^(__unused NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
RCTDevMenu *strongSelf = weakSelf;
if (strongSelf && strongSelf->_liveReloadEnabled) {
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
if (!error && HTTPResponse.statusCode == 205) {
[strongSelf reload];
} else {
if (error.code != NSURLErrorCancelled) {
strongSelf->_updateTask = nil;
[strongSelf checkForUpdates];
}
}
}
});
}];
[_updateTask resume];
}
- (BOOL)isActionSheetShown
{
return _actionSheet != nil;
}
@end
#else // Unavailable when not in dev mode
@implementation RCTDevMenu
- (void)show {}
- (void)reload {}
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {}
- (void)addItem:(RCTDevMenu *)item {}
- (BOOL)isActionSheetShown { return NO; }
@end
@implementation RCTDevMenuItem
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(void(^)(void))handler {return nil;}
+ (instancetype)toggleItemWithKey:(NSString *)key
title:(NSString *)title
selectedTitle:(NSString *)selectedTitle
handler:(void(^)(BOOL selected))handler {return nil;}
@end
#endif
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
#if RCT_DEV
return [self moduleForClass:[RCTDevMenu class]];
#else
return nil;
#endif
}
@end

View File

@ -0,0 +1,109 @@
/**
* 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 <React/RCTBridge.h>
/**
* An abstraction for a key-value store to manage RCTDevSettings behavior.
* The default implementation persists settings using NSUserDefaults.
*/
@protocol RCTDevSettingsDataSource <NSObject>
/**
* Updates the setting with the given key to the given value.
* How the data source's state changes depends on the implementation.
*/
- (void)updateSettingWithValue:(id)value forKey:(NSString *)key;
/**
* Returns the value for the setting with the given key.
*/
- (id)settingForKey:(NSString *)key;
@end
@interface RCTDevSettings : NSObject
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource;
@property (nonatomic, readonly) BOOL isHotLoadingAvailable;
@property (nonatomic, readonly) BOOL isLiveReloadAvailable;
@property (nonatomic, readonly) BOOL isRemoteDebuggingAvailable;
@property (nonatomic, readonly) BOOL isJSCSamplingProfilerAvailable;
/**
* Whether the bridge is connected to a remote JS executor.
*/
@property (nonatomic, assign) BOOL isDebuggingRemotely;
/**
* Alternate name for the websocket executor, if not the generic term "remote".
* TODO t16297016: this seems to be unused, remove?
*/
@property (nonatomic, copy) NSString *websocketExecutorName;
/*
* Whether shaking will show RCTDevMenu. The menu is enabled by default if RCT_DEV=1, but
* you may wish to disable it so that you can provide your own shake handler.
*/
@property (nonatomic, assign) BOOL isShakeToShowDevMenuEnabled;
/**
* Whether performance profiling is enabled.
*/
@property (nonatomic, assign, setter=setProfilingEnabled:) BOOL isProfilingEnabled;
/**
* Whether automatic polling for JS code changes is enabled. Only applicable when
* running the app from a server.
*/
@property (nonatomic, assign, setter=setLiveReloadEnabled:) BOOL isLiveReloadEnabled;
/**
* Whether hot loading is enabled.
*/
@property (nonatomic, assign, setter=setHotLoadingEnabled:) BOOL isHotLoadingEnabled;
/**
* Toggle the element inspector.
*/
- (void)toggleElementInspector;
/**
* Toggle JSC's sampling profiler.
*/
- (void)toggleJSCSamplingProfiler;
/**
* Enables starting of profiling sampler on launch
*/
@property (nonatomic, assign) BOOL startSamplingProfilerOnLaunch;
/**
* Whether the element inspector is visible.
*/
@property (nonatomic, readonly) BOOL isElementInspectorShown;
/**
* Whether the performance monitor is visible.
*/
@property (nonatomic, assign) BOOL isPerfMonitorShown;
/**
* Whether JSC profiling is enabled.
*/
@property (nonatomic, assign) BOOL isJSCProfilingEnabled;
@end
@interface RCTBridge (RCTDevSettings)
@property (nonatomic, readonly) RCTDevSettings *devSettings;
@end

View File

@ -0,0 +1,537 @@
/**
* 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 "RCTDevSettings.h"
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h>
#import <jschelpers/JavaScriptCore.h>
#import "JSCSamplingProfiler.h"
#import "RCTBridge+Private.h"
#import "RCTBridgeModule.h"
#import "RCTDevMenu.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTPackagerClient.h"
#import "RCTProfile.h"
#import "RCTReloadPackagerMethod.h"
#import "RCTUtils.h"
NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled";
NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled";
NSString *const kRCTDevSettingLiveReloadEnabled = @"liveReloadEnabled";
NSString *const kRCTDevSettingIsInspectorShown = @"showInspector";
NSString *const kRCTDevSettingIsDebuggingRemotely = @"isDebuggingRemotely";
NSString *const kRCTDevSettingWebsocketExecutorName = @"websocket-executor-name";
NSString *const kRCTDevSettingExecutorOverrideClass = @"executor-override";
NSString *const kRCTDevSettingShakeToShowDevMenu = @"shakeToShow";
NSString *const kRCTDevSettingIsPerfMonitorShown = @"RCTPerfMonitorKey";
NSString *const kRCTDevSettingIsJSCProfilingEnabled = @"RCTJSCProfilerEnabled";
NSString *const kRCTDevSettingStartSamplingProfilerOnLaunch = @"startSamplingProfilerOnLaunch";
NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu";
#if RCT_DEV
@interface RCTDevSettingsUserDefaultsDataSource : NSObject <RCTDevSettingsDataSource>
@end
@implementation RCTDevSettingsUserDefaultsDataSource {
NSMutableDictionary *_settings;
NSUserDefaults *_userDefaults;
}
- (instancetype)init
{
return [self initWithDefaultValues:nil];
}
- (instancetype)initWithDefaultValues:(NSDictionary *)defaultValues
{
if (self = [super init]) {
_userDefaults = [NSUserDefaults standardUserDefaults];
if (defaultValues) {
[self _reloadWithDefaults:defaultValues];
}
}
return self;
}
- (void)updateSettingWithValue:(id)value forKey:(NSString *)key
{
RCTAssert((key != nil), @"%@", [NSString stringWithFormat:@"%@: Tried to update nil key", [self class]]);
id currentValue = [self settingForKey:key];
if (currentValue == value || [currentValue isEqual:value]) {
return;
}
if (value) {
_settings[key] = value;
} else {
[_settings removeObjectForKey:key];
}
[_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey];
}
- (id)settingForKey:(NSString *)key
{
return _settings[key];
}
- (void)_reloadWithDefaults:(NSDictionary *)defaultValues
{
NSDictionary *existingSettings = [_userDefaults objectForKey:kRCTDevSettingsUserDefaultsKey];
_settings = existingSettings ? [existingSettings mutableCopy] : [NSMutableDictionary dictionary];
for (NSString *key in [defaultValues keyEnumerator]) {
if (!_settings[key]) {
_settings[key] = defaultValues[key];
}
}
[_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey];
}
@end
@interface RCTDevSettings () <RCTBridgeModule, RCTInvalidating>
{
NSURLSessionDataTask *_liveReloadUpdateTask;
NSURL *_liveReloadURL;
BOOL _isJSLoaded;
}
@property (nonatomic, strong) Class executorClass;
@property (nonatomic, readwrite, strong) id<RCTDevSettingsDataSource> dataSource;
@end
@implementation RCTDevSettings
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
- (instancetype)init
{
// default behavior is to use NSUserDefaults
NSDictionary *defaultValues = @{
kRCTDevSettingShakeToShowDevMenu: @YES,
};
RCTDevSettingsUserDefaultsDataSource *dataSource = [[RCTDevSettingsUserDefaultsDataSource alloc] initWithDefaultValues:defaultValues];
return [self initWithDataSource:dataSource];
}
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource
{
if (self = [super init]) {
_dataSource = dataSource;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(jsLoaded:)
name:RCTJavaScriptDidLoadNotification
object:nil];
// Delay setup until after Bridge init
dispatch_async(dispatch_get_main_queue(), ^{
[self _synchronizeAllSettings];
[self connectPackager];
});
}
return self;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
[_liveReloadUpdateTask cancel];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)_updateSettingWithValue:(id)value forKey:(NSString *)key
{
[_dataSource updateSettingWithValue:value forKey:key];
}
- (id)settingForKey:(NSString *)key
{
return [_dataSource settingForKey:key];
}
- (BOOL)isRemoteDebuggingAvailable
{
Class jsDebuggingExecutorClass = objc_lookUpClass("RCTWebSocketExecutor");
return (jsDebuggingExecutorClass != nil);
}
- (BOOL)isHotLoadingAvailable
{
return _bridge.bundleURL && !_bridge.bundleURL.fileURL; // Only works when running from server
}
- (BOOL)isLiveReloadAvailable
{
return (_liveReloadURL != nil);
}
- (BOOL)isJSCSamplingProfilerAvailable
{
return JSC_JSSamplingProfilerEnabled(_bridge.jsContext.JSGlobalContextRef);
}
RCT_EXPORT_METHOD(reload)
{
[_bridge reload];
}
- (NSString *)websocketExecutorName
{
// This value is passed as a command-line argument, so fall back to reading from NSUserDefaults directly
return [[NSUserDefaults standardUserDefaults] stringForKey:kRCTDevSettingWebsocketExecutorName];
}
- (void)setIsShakeToShowDevMenuEnabled:(BOOL)isShakeToShowDevMenuEnabled
{
[self _updateSettingWithValue:@(isShakeToShowDevMenuEnabled) forKey:kRCTDevSettingShakeToShowDevMenu];
}
- (BOOL)isShakeToShowDevMenuEnabled
{
return [[self settingForKey:kRCTDevSettingShakeToShowDevMenu] boolValue];
}
RCT_EXPORT_METHOD(setIsDebuggingRemotely:(BOOL)enabled)
{
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingIsDebuggingRemotely];
[self _remoteDebugSettingDidChange];
}
- (BOOL)isDebuggingRemotely
{
return [[self settingForKey:kRCTDevSettingIsDebuggingRemotely] boolValue];
}
- (void)_remoteDebugSettingDidChange
{
// This value is passed as a command-line argument, so fall back to reading from NSUserDefaults directly
NSString *executorOverride = [[NSUserDefaults standardUserDefaults] stringForKey:kRCTDevSettingExecutorOverrideClass];
if (executorOverride) {
self.executorClass = NSClassFromString(executorOverride);
} else {
BOOL enabled = self.isRemoteDebuggingAvailable && self.isDebuggingRemotely;
self.executorClass = enabled ? objc_getClass("RCTWebSocketExecutor") : nil;
}
}
RCT_EXPORT_METHOD(setProfilingEnabled:(BOOL)enabled)
{
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingProfilingEnabled];
[self _profilingSettingDidChange];
}
- (BOOL)isProfilingEnabled
{
return [[self settingForKey:kRCTDevSettingProfilingEnabled] boolValue];
}
- (void)_profilingSettingDidChange
{
BOOL enabled = self.isProfilingEnabled;
if (_liveReloadURL && enabled != RCTProfileIsProfiling()) {
if (enabled) {
[_bridge startProfiling];
} else {
[_bridge stopProfiling:^(NSData *logData) {
RCTProfileSendResult(self->_bridge, @"systrace", logData);
}];
}
}
}
RCT_EXPORT_METHOD(setLiveReloadEnabled:(BOOL)enabled)
{
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingLiveReloadEnabled];
[self _liveReloadSettingDidChange];
}
- (BOOL)isLiveReloadEnabled
{
return [[self settingForKey:kRCTDevSettingLiveReloadEnabled] boolValue];
}
- (void)_liveReloadSettingDidChange
{
BOOL liveReloadEnabled = (self.isLiveReloadAvailable && self.isLiveReloadEnabled);
if (liveReloadEnabled) {
[self _pollForLiveReload];
} else {
[_liveReloadUpdateTask cancel];
_liveReloadUpdateTask = nil;
}
}
RCT_EXPORT_METHOD(setHotLoadingEnabled:(BOOL)enabled)
{
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingHotLoadingEnabled];
[self _hotLoadingSettingDidChange];
}
- (BOOL)isHotLoadingEnabled
{
return [[self settingForKey:kRCTDevSettingHotLoadingEnabled] boolValue];
}
- (void)_hotLoadingSettingDidChange
{
BOOL hotLoadingEnabled = self.isHotLoadingAvailable && self.isHotLoadingEnabled;
if (RCTGetURLQueryParam(_bridge.bundleURL, @"hot").boolValue != hotLoadingEnabled) {
_bridge.bundleURL = RCTURLByReplacingQueryParam(_bridge.bundleURL, @"hot",
hotLoadingEnabled ? @"true" : nil);
[_bridge reload];
}
}
RCT_EXPORT_METHOD(toggleElementInspector)
{
BOOL value = [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
[self _updateSettingWithValue:@(!value) forKey:kRCTDevSettingIsInspectorShown];
if (_isJSLoaded) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
#pragma clang diagnostic pop
}
}
- (void)toggleJSCSamplingProfiler
{
JSContext *context = _bridge.jsContext;
JSGlobalContextRef globalContext = context.JSGlobalContextRef;
// JSPokeSamplingProfiler() toggles the profiling process
JSValueRef jsResult = JSC_JSPokeSamplingProfiler(globalContext);
if (JSC_JSValueGetType(globalContext, jsResult) != kJSTypeNull) {
NSString *results = [[JSC_JSValue(globalContext) valueWithJSValueRef:jsResult inContext:context] toObject];
JSCSamplingProfiler *profilerModule = [_bridge moduleForClass:[JSCSamplingProfiler class]];
[profilerModule operationCompletedWithResults:results];
}
}
- (BOOL)isElementInspectorShown
{
return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
}
- (void)setIsPerfMonitorShown:(BOOL)isPerfMonitorShown
{
[self _updateSettingWithValue:@(isPerfMonitorShown) forKey:kRCTDevSettingIsPerfMonitorShown];
}
- (BOOL)isPerfMonitorShown
{
return [[self settingForKey:kRCTDevSettingIsPerfMonitorShown] boolValue];
}
- (void)setIsJSCProfilingEnabled:(BOOL)isJSCProfilingEnabled
{
[self _updateSettingWithValue:@(isJSCProfilingEnabled) forKey:kRCTDevSettingIsJSCProfilingEnabled];
}
- (BOOL)isJSCProfilingEnabled
{
return [[self settingForKey:kRCTDevSettingIsJSCProfilingEnabled] boolValue];
}
- (void)setStartSamplingProfilerOnLaunch:(BOOL)startSamplingProfilerOnLaunch
{
[self _updateSettingWithValue:@(startSamplingProfilerOnLaunch) forKey:kRCTDevSettingStartSamplingProfilerOnLaunch];
}
- (BOOL)startSamplingProfilerOnLaunch
{
return [[self settingForKey:kRCTDevSettingStartSamplingProfilerOnLaunch] boolValue];
}
- (void)setExecutorClass:(Class)executorClass
{
_executorClass = executorClass;
if (_bridge.executorClass != executorClass) {
// TODO (6929129): we can remove this special case test once we have better
// support for custom executors in the dev menu. But right now this is
// needed to prevent overriding a custom executor with the default if a
// custom executor has been set directly on the bridge
if (executorClass == Nil &&
_bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
return;
}
_bridge.executorClass = executorClass;
[_bridge reload];
}
}
#pragma mark - internal
/**
* Query the data source for all possible settings and make sure we're doing the right
* thing for the state of each setting.
*/
- (void)_synchronizeAllSettings
{
[self _hotLoadingSettingDidChange];
[self _liveReloadSettingDidChange];
[self _remoteDebugSettingDidChange];
[self _profilingSettingDidChange];
}
- (void)_pollForLiveReload
{
if (!_isJSLoaded || ![[self settingForKey:kRCTDevSettingLiveReloadEnabled] boolValue] || !_liveReloadURL) {
return;
}
if (_liveReloadUpdateTask) {
return;
}
__weak RCTDevSettings *weakSelf = self;
_liveReloadUpdateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:
^(__unused NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong RCTDevSettings *strongSelf = weakSelf;
if (strongSelf && [[strongSelf settingForKey:kRCTDevSettingLiveReloadEnabled] boolValue]) {
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
if (!error && HTTPResponse.statusCode == 205) {
[strongSelf reload];
} else {
if (error.code != NSURLErrorCancelled) {
strongSelf->_liveReloadUpdateTask = nil;
[strongSelf _pollForLiveReload];
}
}
}
});
}];
[_liveReloadUpdateTask resume];
}
- (void)jsLoaded:(NSNotification *)notification
{
if (notification.userInfo[@"bridge"] != _bridge) {
return;
}
_isJSLoaded = YES;
// Check if live reloading is available
NSURL *scriptURL = _bridge.bundleURL;
if (![scriptURL isFileURL]) {
// Live reloading is disabled when running from bundled JS file
_liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:scriptURL];
} else {
_liveReloadURL = nil;
}
dispatch_async(dispatch_get_main_queue(), ^{
// update state again after the bridge has finished loading
[self _synchronizeAllSettings];
});
}
#pragma mark - RCTWebSocketObserver
- (NSURL *)packagerURL
{
NSString *host = [_bridge.bundleURL host];
NSString *scheme = [_bridge.bundleURL scheme];
if (!host) {
host = @"localhost";
scheme = @"http";
}
NSNumber *port = [_bridge.bundleURL port];
if (!port) {
port = @8081; // Packager default port
}
return [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%@/message?role=ios-rn-rctdevmenu", scheme, host, port]];
}
// TODO: Move non-UI logic into separate RCTDevSettings module
- (void)connectPackager
{
RCTAssertMainQueue();
NSURL *url = [self packagerURL];
if (!url) {
return;
}
// The jsPackagerClient is a static map that holds different packager clients per the packagerURL
// In case many instances of DevMenu are created, the latest instance that use the same URL as
// previous instances will override given packager client's method handlers
static NSMutableDictionary<NSString *, RCTPackagerClient *> *jsPackagerClients = nil;
if (jsPackagerClients == nil) {
jsPackagerClients = [NSMutableDictionary new];
}
NSString *key = [url absoluteString];
RCTPackagerClient *packagerClient = jsPackagerClients[key];
if (!packagerClient) {
packagerClient = [[RCTPackagerClient alloc] initWithURL:url];
jsPackagerClients[key] = packagerClient;
} else {
[packagerClient stop];
}
[packagerClient addHandler:[[RCTReloadPackagerMethod alloc] initWithBridge:_bridge]
forMethod:@"reload"];
[packagerClient start];
}
@end
#else // #if RCT_DEV
@implementation RCTDevSettings
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource { return [super init]; }
- (BOOL)isHotLoadingAvailable { return NO; }
- (BOOL)isLiveReloadAvailable { return NO; }
- (BOOL)isRemoteDebuggingAvailable { return NO; }
- (id)settingForKey:(NSString *)key { return nil; }
- (void)reload {}
- (void)toggleElementInspector {}
- (void)toggleJSCSamplingProfiler {}
@end
#endif
@implementation RCTBridge (RCTDevSettings)
- (RCTDevSettings *)devSettings
{
#if RCT_DEV
return [self moduleForClass:[RCTDevSettings class]];
#else
return nil;
#endif
}
@end

View File

@ -17,6 +17,7 @@
#import "RCTBridge.h"
#import "RCTDevMenu.h"
#import "RCTDevSettings.h"
#import "RCTFPSGraph.h"
#import "RCTInvalidating.h"
#import "RCTJavaScriptExecutor.h"
@ -26,7 +27,6 @@
#import "RCTUIManager.h"
#import "RCTBridge+Private.h"
static NSString *const RCTPerfMonitorKey = @"RCTPerfMonitorKey";
static NSString *const RCTPerfMonitorCellIdentifier = @"RCTPerfMonitorCellIdentifier";
static CGFloat const RCTPerfMonitorBarHeight = 50;
@ -154,16 +154,19 @@ RCT_EXPORT_MODULE()
{
if (!_devMenuItem) {
__weak __typeof__(self) weakSelf = self;
__weak RCTDevSettings *devSettings = self.bridge.devSettings;
_devMenuItem =
[RCTDevMenuItem toggleItemWithKey:RCTPerfMonitorKey
title:@"Show Perf Monitor"
selectedTitle:@"Hide Perf Monitor"
handler:
^(BOOL selected) {
if (selected) {
[weakSelf show];
} else {
[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return (devSettings.isPerfMonitorShown) ?
@"Hide Perf Monitor" :
@"Show Perf Monitor";
} handler:^{
if (devSettings.isPerfMonitorShown) {
[weakSelf hide];
devSettings.isPerfMonitorShown = NO;
} else {
[weakSelf show];
devSettings.isPerfMonitorShown = YES;
}
}];
}

View File

@ -29,7 +29,6 @@
137327E91AA5CF210034F82E /* RCTTabBarItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E41AA5CF210034F82E /* RCTTabBarItemManager.m */; };
137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E61AA5CF210034F82E /* RCTTabBarManager.m */; };
13A0C2891B74F71200B29F6F /* RCTDevLoadingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13A0C2861B74F71200B29F6F /* RCTDevLoadingView.m */; };
13A0C28A1B74F71200B29F6F /* RCTDevMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13A0C2881B74F71200B29F6F /* RCTDevMenu.mm */; };
13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */; };
13A6E20E1C19AA0C00845B82 /* RCTParserUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 13A6E20D1C19AA0C00845B82 /* RCTParserUtils.m */; };
13AB5E011DF777F2001A8C30 /* YGNodeList.c in Sources */ = {isa = PBXBuildFile; fileRef = 130A77051DF767AF001F9587 /* YGNodeList.c */; };
@ -111,7 +110,6 @@
2D3B5EB11D9B090100451313 /* RCTAppState.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7091AB030C200659ED6 /* RCTAppState.m */; };
2D3B5EB21D9B090300451313 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; };
2D3B5EB41D9B090A00451313 /* RCTDevLoadingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13A0C2861B74F71200B29F6F /* RCTDevLoadingView.m */; };
2D3B5EB51D9B091100451313 /* RCTDevMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13A0C2881B74F71200B29F6F /* RCTDevMenu.mm */; };
2D3B5EB61D9B091400451313 /* RCTExceptionsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FEA1A69327A00A75B9A /* RCTExceptionsManager.m */; };
2D3B5EB71D9B091800451313 /* RCTRedBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 13F17A841B8493E5007D4C75 /* RCTRedBox.m */; };
2D3B5EB81D9B091B00451313 /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; };
@ -754,6 +752,12 @@
A2440AA41DF8D865006E7BFC /* RCTReloadCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = A2440AA01DF8D854006E7BFC /* RCTReloadCommand.h */; };
AC70D2E91DE489E4002E6351 /* RCTJavaScriptLoader.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC70D2E81DE489E4002E6351 /* RCTJavaScriptLoader.mm */; };
B233E6EA1D2D845D00BC68BA /* RCTI18nManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B233E6E91D2D845D00BC68BA /* RCTI18nManager.m */; };
B505583E1E43DFB900F71A00 /* RCTDevMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = B505583B1E43DFB900F71A00 /* RCTDevMenu.m */; };
B505583F1E43DFB900F71A00 /* RCTDevSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = B505583C1E43DFB900F71A00 /* RCTDevSettings.h */; };
B50558401E43DFB900F71A00 /* RCTDevSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */; };
B50558411E43E13D00F71A00 /* RCTDevMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = B505583B1E43DFB900F71A00 /* RCTDevMenu.m */; };
B50558421E43E14000F71A00 /* RCTDevSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */; };
B50558431E43E64600F71A00 /* RCTDevSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = B505583C1E43DFB900F71A00 /* RCTDevSettings.h */; };
B95154321D1B34B200FE7B80 /* RCTActivityIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = B95154311D1B34B200FE7B80 /* RCTActivityIndicatorView.m */; };
E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */; };
/* End PBXBuildFile section */
@ -1199,7 +1203,6 @@
13A0C2851B74F71200B29F6F /* RCTDevLoadingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTDevLoadingView.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
13A0C2861B74F71200B29F6F /* RCTDevLoadingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevLoadingView.m; sourceTree = "<group>"; };
13A0C2871B74F71200B29F6F /* RCTDevMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTDevMenu.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
13A0C2881B74F71200B29F6F /* RCTDevMenu.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTDevMenu.mm; sourceTree = "<group>"; };
13A1F71C1A75392D00D3D453 /* RCTKeyCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTKeyCommands.h; sourceTree = "<group>"; };
13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTKeyCommands.m; sourceTree = "<group>"; };
13A6E20C1C19AA0C00845B82 /* RCTParserUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTParserUtils.h; sourceTree = "<group>"; };
@ -1411,6 +1414,9 @@
ACDD3FDA1BC7430D00E7DE33 /* RCTBorderStyle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBorderStyle.h; sourceTree = "<group>"; };
B233E6E81D2D843200BC68BA /* RCTI18nManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTI18nManager.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
B233E6E91D2D845D00BC68BA /* RCTI18nManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTI18nManager.m; sourceTree = "<group>"; };
B505583B1E43DFB900F71A00 /* RCTDevMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenu.m; sourceTree = "<group>"; };
B505583C1E43DFB900F71A00 /* RCTDevSettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDevSettings.h; sourceTree = "<group>"; };
B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTDevSettings.mm; sourceTree = "<group>"; };
B95154301D1B34B200FE7B80 /* RCTActivityIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorView.h; sourceTree = "<group>"; };
B95154311D1B34B200FE7B80 /* RCTActivityIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorView.m; sourceTree = "<group>"; };
E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = "<group>"; };
@ -1485,7 +1491,9 @@
13A0C2851B74F71200B29F6F /* RCTDevLoadingView.h */,
13A0C2861B74F71200B29F6F /* RCTDevLoadingView.m */,
13A0C2871B74F71200B29F6F /* RCTDevMenu.h */,
13A0C2881B74F71200B29F6F /* RCTDevMenu.mm */,
B505583B1E43DFB900F71A00 /* RCTDevMenu.m */,
B505583C1E43DFB900F71A00 /* RCTDevSettings.h */,
B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */,
13D9FEE91CDCCECF00158BD7 /* RCTEventEmitter.h */,
13D9FEEA1CDCCECF00158BD7 /* RCTEventEmitter.m */,
13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */,
@ -1896,6 +1904,7 @@
3D302F381DF828F800D6DDAE /* RCTFrameUpdate.h in Headers */,
3D5AC7221E005763000F9153 /* RCTTVRemoteHandler.h in Headers */,
3D302F391DF828F800D6DDAE /* RCTImageSource.h in Headers */,
B50558431E43E64600F71A00 /* RCTDevSettings.h in Headers */,
3D302F3A1DF828F800D6DDAE /* RCTInvalidating.h in Headers */,
3D302F3B1DF828F800D6DDAE /* RCTJavaScriptExecutor.h in Headers */,
3D302F3C1DF828F800D6DDAE /* RCTJavaScriptLoader.h in Headers */,
@ -2152,6 +2161,7 @@
3D80DA6C1DF820620028D040 /* RCTMap.h in Headers */,
3D80DA6D1DF820620028D040 /* RCTMapAnnotation.h in Headers */,
3D80DA6E1DF820620028D040 /* RCTMapManager.h in Headers */,
B505583F1E43DFB900F71A00 /* RCTDevSettings.h in Headers */,
3D80DA6F1DF820620028D040 /* RCTMapOverlay.h in Headers */,
3D80DA701DF820620028D040 /* RCTModalHostView.h in Headers */,
3D80DA711DF820620028D040 /* RCTModalHostViewController.h in Headers */,
@ -2462,6 +2472,7 @@
3D80D91B1DF6F8200028D040 /* RCTPlatform.m in Sources */,
2DD0EFE11DA84F2800B0C975 /* RCTStatusBarManager.m in Sources */,
2D3B5EC91D9B095C00451313 /* RCTBorderDrawing.m in Sources */,
B50558411E43E13D00F71A00 /* RCTDevMenu.m in Sources */,
2D3B5ED31D9B097B00451313 /* RCTMapOverlay.m in Sources */,
2D3B5E991D9B089A00451313 /* RCTDisplayLink.m in Sources */,
2D9F8B9B1DE398DB00A16144 /* RCTPlatform.m in Sources */,
@ -2517,7 +2528,6 @@
2D3B5E9E1D9B08AD00451313 /* RCTJSStackFrame.m in Sources */,
2D3B5E941D9B087900451313 /* RCTBundleURLProvider.m in Sources */,
2D3B5EB81D9B091B00451313 /* RCTSourceCode.m in Sources */,
2D3B5EB51D9B091100451313 /* RCTDevMenu.mm in Sources */,
945929C51DD62ADD00653A7D /* RCTConvert+Transform.m in Sources */,
2D3B5EBD1D9B092A00451313 /* RCTTiming.m in Sources */,
2D3B5EA81D9B08D300451313 /* RCTUtils.m in Sources */,
@ -2538,6 +2548,7 @@
2D3B5E9C1D9B08A300451313 /* RCTImageSource.m in Sources */,
3DDEC1521DDCE0CA0020BBDF /* JSCSamplingProfiler.m in Sources */,
3D5AC7231E005766000F9153 /* RCTTVRemoteHandler.m in Sources */,
B50558421E43E14000F71A00 /* RCTDevSettings.mm in Sources */,
2D3B5EC31D9B094800451313 /* RCTProfileTrampoline-arm.S in Sources */,
2D3B5ED91D9B098E00451313 /* RCTNavItem.m in Sources */,
2D74EAFA1DAE9590003B751B /* RCTMultipartDataTask.m in Sources */,
@ -2629,7 +2640,6 @@
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */,
13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */,
13B202041BFB948C00C07393 /* RCTMapAnnotation.m in Sources */,
13A0C28A1B74F71200B29F6F /* RCTDevMenu.mm in Sources */,
13BCE8091C99CB9D00DD7AAD /* RCTRootShadowView.m in Sources */,
14C2CA711B3AC63800E6CBB2 /* RCTModuleMethod.m in Sources */,
006FC4141D9B20820057AAAD /* RCTMultipartDataTask.m in Sources */,
@ -2669,7 +2679,9 @@
1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */,
134FCB3D1A6E7F0800051CC8 /* RCTJSCExecutor.mm in Sources */,
14C2CA781B3ACB0400E6CBB2 /* RCTBatchedBridge.m in Sources */,
B50558401E43DFB900F71A00 /* RCTDevSettings.mm in Sources */,
13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */,
B505583E1E43DFB900F71A00 /* RCTDevMenu.m in Sources */,
14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */,
13D033631C1837FE0021DC29 /* RCTClipboard.m in Sources */,
14C2CA741B3AC64300E6CBB2 /* RCTModuleData.mm in Sources */,