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 */,