From 6a14f0b44942874cb72d1535a2df86fb75c3e2be Mon Sep 17 00:00:00 2001 From: Ben Roth Date: Fri, 24 Feb 2017 06:50:29 -0800 Subject: [PATCH] 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 --- Libraries/WebSocket/RCTWebSocketExecutor.m | 1 + React/CxxBridge/RCTCxxBridge.mm | 4 +- React/Executors/RCTJSCExecutor.mm | 26 +- React/Modules/RCTDevMenu.h | 53 +- React/Modules/RCTDevMenu.m | 387 +++++++++++ React/Modules/RCTDevMenu.mm | 763 --------------------- React/Modules/RCTDevSettings.h | 109 +++ React/Modules/RCTDevSettings.mm | 537 +++++++++++++++ React/Profiler/RCTPerfMonitor.m | 27 +- React/React.xcodeproj/project.pbxproj | 24 +- 10 files changed, 1099 insertions(+), 832 deletions(-) create mode 100644 React/Modules/RCTDevMenu.m delete mode 100644 React/Modules/RCTDevMenu.mm create mode 100644 React/Modules/RCTDevSettings.h create mode 100644 React/Modules/RCTDevSettings.mm diff --git a/Libraries/WebSocket/RCTWebSocketExecutor.m b/Libraries/WebSocket/RCTWebSocketExecutor.m index cea7ab7fb43147..7534181bd7ceeb 100644 --- a/Libraries/WebSocket/RCTWebSocketExecutor.m +++ b/Libraries/WebSocket/RCTWebSocketExecutor.m @@ -56,6 +56,7 @@ - (void)setUp 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; diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 5f566c95730177..bcbb492b7e99f9 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -23,7 +23,7 @@ #import #import #import -#import +#import #import #import #import @@ -403,7 +403,7 @@ - (void)start 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 { diff --git a/React/Executors/RCTJSCExecutor.mm b/React/Executors/RCTJSCExecutor.mm index 3b5a1816fef373..2cd5e8e98c11a6 100644 --- a/React/Executors/RCTJSCExecutor.mm +++ b/React/Executors/RCTJSCExecutor.mm @@ -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 @@ 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 @@ @implementation RCTJSCExecutor #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 @@ - (void)setUp } 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 @@ - (void)setUp 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 diff --git a/React/Modules/RCTDevMenu.h b/React/Modules/RCTDevMenu.h index 8f1ce29ff4b51d..f5dc12afbec88a 100644 --- a/React/Modules/RCTDevMenu.h +++ b/React/Modules/RCTDevMenu.h @@ -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 *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 /** diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m new file mode 100644 index 00000000000000..2c0ce388345540 --- /dev/null +++ b/React/Modules/RCTDevMenu.m @@ -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 () + +@end + +@implementation RCTDevMenu +{ + UIAlertController *_actionSheet; + NSMutableArray *_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 *)_menuItemsToPresent +{ + NSMutableArray *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 *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 diff --git a/React/Modules/RCTDevMenu.mm b/React/Modules/RCTDevMenu.mm deleted file mode 100644 index cf0cb559ff016d..00000000000000 --- a/React/Modules/RCTDevMenu.mm +++ /dev/null @@ -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 - -#import - -#import - -#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 () - -@property (nonatomic, strong) Class executorClass; - -@end - -@implementation RCTDevMenu -{ - UIAlertController *_actionSheet; - NSUserDefaults *_defaults; - NSMutableDictionary *_settings; - NSURLSessionDataTask *_updateTask; - NSURL *_liveReloadURL; - BOOL _jsLoaded; - NSMutableArray *_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 *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 *)menuItems -{ - NSMutableArray *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 *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 diff --git a/React/Modules/RCTDevSettings.h b/React/Modules/RCTDevSettings.h new file mode 100644 index 00000000000000..6fb6fb4652f936 --- /dev/null +++ b/React/Modules/RCTDevSettings.h @@ -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 + +/** + * An abstraction for a key-value store to manage RCTDevSettings behavior. + * The default implementation persists settings using NSUserDefaults. + */ +@protocol RCTDevSettingsDataSource + +/** + * 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)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 diff --git a/React/Modules/RCTDevSettings.mm b/React/Modules/RCTDevSettings.mm new file mode 100644 index 00000000000000..9ef8a86c088814 --- /dev/null +++ b/React/Modules/RCTDevSettings.mm @@ -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 + +#import + +#import + +#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 + +@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 () +{ + NSURLSessionDataTask *_liveReloadUpdateTask; + NSURL *_liveReloadURL; + BOOL _isJSLoaded; +} + +@property (nonatomic, strong) Class executorClass; +@property (nonatomic, readwrite, strong) id 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)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 *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)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 diff --git a/React/Profiler/RCTPerfMonitor.m b/React/Profiler/RCTPerfMonitor.m index f2aaf5c3e834c9..d0a01a399f10ba 100644 --- a/React/Profiler/RCTPerfMonitor.m +++ b/React/Profiler/RCTPerfMonitor.m @@ -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,18 +154,21 @@ - (RCTDevMenuItem *)devMenuItem { 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 { - [weakSelf hide]; - } - }]; + [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; + } + }]; } return _devMenuItem; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 90384aac8ca244..2977459d7a8c56 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -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 = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 13A0C2861B74F71200B29F6F /* RCTDevLoadingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevLoadingView.m; sourceTree = ""; }; 13A0C2871B74F71200B29F6F /* RCTDevMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTDevMenu.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - 13A0C2881B74F71200B29F6F /* RCTDevMenu.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTDevMenu.mm; sourceTree = ""; }; 13A1F71C1A75392D00D3D453 /* RCTKeyCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTKeyCommands.h; sourceTree = ""; }; 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTKeyCommands.m; sourceTree = ""; }; 13A6E20C1C19AA0C00845B82 /* RCTParserUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTParserUtils.h; sourceTree = ""; }; @@ -1411,6 +1414,9 @@ ACDD3FDA1BC7430D00E7DE33 /* RCTBorderStyle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBorderStyle.h; sourceTree = ""; }; B233E6E81D2D843200BC68BA /* RCTI18nManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTI18nManager.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; B233E6E91D2D845D00BC68BA /* RCTI18nManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTI18nManager.m; sourceTree = ""; }; + B505583B1E43DFB900F71A00 /* RCTDevMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenu.m; sourceTree = ""; }; + B505583C1E43DFB900F71A00 /* RCTDevSettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDevSettings.h; sourceTree = ""; }; + B505583D1E43DFB900F71A00 /* RCTDevSettings.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTDevSettings.mm; sourceTree = ""; }; B95154301D1B34B200FE7B80 /* RCTActivityIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorView.h; sourceTree = ""; }; B95154311D1B34B200FE7B80 /* RCTActivityIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorView.m; sourceTree = ""; }; E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = ""; }; @@ -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 */,