/** * 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 "RCTBridge+Private.h" #import "RCTDevSettings.h" #import "RCTKeyCommands.h" #import "RCTLog.h" #import "RCTUtils.h" #if RCT_DEV #if RCT_ENABLE_INSPECTOR #import "RCTInspectorDevServerHelper.h" #endif 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:)); } + (BOOL)requiresMainQueueSetup { return YES; } - (instancetype)init { if ((self = [super init])) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showOnShake) name:RCTShowDevMenuNotification object:nil]; _extraMenuItems = [NSMutableArray new]; #if TARGET_OS_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]; }]]; if (devSettings.isNuclideDebuggingAvailable) { [items addObject:[RCTDevMenuItem buttonItemWithTitle:[NSString stringWithFormat:@"Debug JS in Nuclide %@", @"\U0001F4AF"] handler:^{ #if RCT_ENABLE_INSPECTOR [RCTInspectorDevServerHelper attachDebugger:@"ReactNative" withBundleURL:bridge.bundleURL withView: RCTPresentedViewController()]; #endif }]]; } if (!devSettings.isRemoteDebuggingAvailable) { [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Remote JS Debugger Unavailable" handler:^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Remote JS Debugger Unavailable" message:@"You need to include the RCTWebSocket library to enable remote JS debugging" preferredStyle:UIAlertControllerStyleAlert]; __weak typeof(alertController) weakAlertController = alertController; [alertController addAction: [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ [weakAlertController dismissViewControllerAnimated:YES completion:nil]; }]]; [RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL]; }]]; } else { [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{ NSString *title = devSettings.isDebuggingRemotely ? @"Stop Remote JS Debugging" : @"Debug JS Remotely"; if (devSettings.isNuclideDebuggingAvailable) { return [NSString stringWithFormat:@"%@ %@", title, @"\U0001F645"]; } else { return title; } } 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:^{ if (devSettings.isDebuggingRemotely) { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Systrace Unavailable" message:@"You need to stop remote JS debugging to enable Systrace" preferredStyle:UIAlertControllerStyleAlert]; __weak typeof(alertController) weakAlertController = alertController; [alertController addAction: [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ [weakAlertController dismissViewControllerAnimated:YES completion:nil]; }]]; [RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL]; } else { 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 @"Toggle Inspector"; } handler:^{ [devSettings toggleElementInspector]; }]]; [items addObjectsFromArray:_extraMenuItems]; return items; } RCT_EXPORT_METHOD(show) { if (_actionSheet || !_bridge || RCTRunningInAppExtension()) { return; } NSString *desc = _bridge.bridgeDescription; if (desc.length == 0) { desc = NSStringFromClass([_bridge class]); } NSString *title = [NSString stringWithFormat:@"React Native: Development (%@)", desc]; // 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