Files
react-native/React/CoreModules/RCTDevMenu.mm
T
Rick Hanlon a9bac177e5 Add "Open Debugger" and "Open React DevTools" to iOS dev menu
Summary:
This diff introduces a new "Open Debugger" menu item for VMs that support on device debugging and for opening the React DevTools in Flipper. Provided so that we don't drift too far from the Android code.

Changelog: [Internal]

Reviewed By: RSNara

Differential Revision: D20784270

fbshipit-source-id: 6bb16431d25a6c093a583e2e041b8cffa6765ddd
2020-04-15 19:34:52 -07:00

599 lines
24 KiB
Plaintext

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTDevMenu.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTDefines.h>
#import <React/RCTDevSettings.h>
#import <React/RCTKeyCommands.h>
#import <React/RCTLog.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
#if RCT_DEV_MENU
#if RCT_ENABLE_INSPECTOR
#import <React/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 () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
@end
@implementation RCTDevMenu {
UIAlertController *_actionSheet;
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
}
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (void)initialize
{
// We're swizzling here because it's poor form to override methods in a category,
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
// no need to call the original implementation.
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
}
+ (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 || TARGET_OS_MACCATALYST
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){
}];
}
- (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];
}
- (void)setDefaultJSBundle
{
[[RCTBundleURLProvider sharedSettings] resetToDefaults];
self->_bridge.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForFallbackResource:nil
fallbackExtension:nil];
RCTTriggerReloadCommandListeners(@"Dev menu - reset to default");
}
- (NSArray<RCTDevMenuItem *> *)_menuItemsToPresent
{
NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
// Add built-in items
__weak RCTBridge *bridge = _bridge;
__weak RCTDevSettings *devSettings = _bridge.devSettings;
__weak RCTDevMenu *weakSelf = self;
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload"
handler:^{
RCTTriggerReloadCommandListeners(@"Dev menu - reload");
}]];
if (!devSettings.isProfilingEnabled) {
#if RCT_ENABLE_INSPECTOR
if (devSettings.isDeviceDebuggingAvailable) {
// For on-device debugging we link out to Flipper.
// Since we're assuming Flipper is available, also include the DevTools.
// Note: For parity with the Android code.
// Reset the old debugger setting so no one gets stuck.
// TODO: Remove in a few weeks.
if (devSettings.isDebuggingRemotely) {
devSettings.isDebuggingRemotely = false;
}
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Open Debugger";
}
handler:^{
[RCTInspectorDevServerHelper
openURL:@"flipper://null/Hermesdebuggerrn?device=React%20Native"
withBundleURL:bridge.bundleURL
withErrorMessage:@"Failed to open Flipper. Please check that Metro is runnning."];
}]];
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Open React DevTools";
}
handler:^{
[RCTInspectorDevServerHelper
openURL:@"flipper://null/React?device=React%20Native"
withBundleURL:bridge.bundleURL
withErrorMessage:@"Failed to open Flipper. Please check that Metro is runnning."];
}]];
} else if (devSettings.isRemoteDebuggingAvailable) {
#else
if (devSettings.isRemoteDebuggingAvailable) {
#endif
// For remote debugging, we open up Chrome running the app in a web worker.
// Note that this requires async communication, which will not work for Turbo Modules.
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return devSettings.isDebuggingRemotely ? @"Stop Debugging" : @"Debug with Chrome";
}
handler:^{
devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
}]];
} else {
// If neither are available, we're defaulting to a message that tells you about remote debugging.
[items
addObject:[RCTDevMenuItem
buttonItemWithTitle:@"Debugger Unavailable"
handler:^{
NSString *message = RCTTurboModuleEnabled()
? @"Debugging with Chrome is not supported when TurboModules are enabled."
: @"Include the RCTWebSocket library to enable JavaScript debugging.";
UIAlertController *alertController =
[UIAlertController alertControllerWithTitle:@"Debugger Unavailable"
message:message
preferredStyle:UIAlertControllerStyleAlert];
__weak __typeof__(alertController) weakAlertController = alertController;
[alertController
addAction:[UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
[weakAlertController
dismissViewControllerAnimated:YES
completion:nil];
}]];
[RCTPresentedViewController() presentViewController:alertController
animated:YES
completion:NULL];
}]];
}
}
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return devSettings.isElementInspectorShown ? @"Hide Inspector" : @"Show Inspector";
}
handler:^{
[devSettings toggleElementInspector];
}]];
if (devSettings.isHotLoadingAvailable) {
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
// Previously known as "Hot Reloading". We won't use this term anymore.
return devSettings.isHotLoadingEnabled ? @"Disable Fast Refresh" : @"Enable Fast Refresh";
}
handler:^{
devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
}]];
}
if (devSettings.isLiveReloadAvailable) {
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return devSettings.isDebuggingRemotely
? @"Systrace Unavailable"
: devSettings.isProfilingEnabled ? @"Stop Systrace" : @"Start Systrace";
}
handler:^{
if (devSettings.isDebuggingRemotely) {
UIAlertController *alertController =
[UIAlertController alertControllerWithTitle:@"Systrace Unavailable"
message:@"Stop debugging to enable Systrace."
preferredStyle:UIAlertControllerStyleAlert];
__weak __typeof__(alertController) weakAlertController = alertController;
[alertController
addAction:[UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
[weakAlertController
dismissViewControllerAnimated:YES
completion:nil];
}]];
[RCTPresentedViewController() presentViewController:alertController
animated:YES
completion:NULL];
} else {
devSettings.isProfilingEnabled = !devSettings.isProfilingEnabled;
}
}]];
// "Live reload" which refreshes on every edit was removed in favor of "Fast Refresh".
// While native code for "Live reload" is still there, please don't add the option back.
// See D15958697 for more context.
}
[items
addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Configure Bundler";
}
handler:^{
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:@"Configure Bundler"
message:@"Provide a custom bundler address, port, and entrypoint."
preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"0.0.0.0";
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"8081";
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"index";
}];
[alertController
addAction:[UIAlertAction
actionWithTitle:@"Apply Changes"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
NSArray *textfields = alertController.textFields;
UITextField *ipTextField = textfields[0];
UITextField *portTextField = textfields[1];
UITextField *bundleRootTextField = textfields[2];
NSString *bundleRoot = bundleRootTextField.text;
if (ipTextField.text.length == 0 && portTextField.text.length == 0) {
[weakSelf setDefaultJSBundle];
return;
}
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
NSNumber *portNumber =
[formatter numberFromString:portTextField.text];
if (portNumber == nil) {
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
}
[RCTBundleURLProvider sharedSettings].jsLocation = [NSString
stringWithFormat:@"%@:%d", ipTextField.text, portNumber.intValue];
__strong RCTBridge *strongBridge = bridge;
if (strongBridge) {
NSURL *bundleURL = bundleRoot.length
? [[RCTBundleURLProvider sharedSettings]
jsBundleURLForBundleRoot:bundleRoot
fallbackResource:nil]
: [strongBridge.delegate sourceURLForBridge:strongBridge];
strongBridge.bundleURL = bundleURL;
RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
}
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Reset to Default"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
[weakSelf setDefaultJSBundle];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:^(__unused UIAlertAction *action) {
return;
}]];
[RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
}]];
[items addObjectsFromArray:_extraMenuItems];
return items;
}
RCT_EXPORT_METHOD(show)
{
if (_actionSheet || !_bridge || RCTRunningInAppExtension()) {
return;
}
NSString *bridgeDescription = _bridge.bridgeDescription;
NSString *description =
bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
// 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:@"React Native Debug Menu"
message:description
preferredStyle:style];
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
for (RCTDevMenuItem *item in items) {
[_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
style:UIAlertActionStyleDefault
handler:[self alertActionHandlerForDevItem:item]]];
}
[_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:[self alertActionHandlerForDevItem:nil]]];
_presentedItems = items;
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
[_bridge enqueueJSCall:@"RCTNativeAppEventEmitter" method:@"emit" args:@[ @"RCTDevMenuShown" ] completion:NULL];
}
- (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();
RCTTriggerReloadCommandListeners(@"Unknown from JS");
}
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(setHotLoadingEnabled : (BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
_bridge.devSettings.isHotLoadingEnabled = enabled;
}
- (BOOL)hotLoadingEnabled
{
return _bridge.devSettings.isHotLoadingEnabled;
}
- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
nativeInvoker:(std::shared_ptr<facebook::react::CallInvoker>)nativeInvoker
perfLogger:(id<RCTTurboModulePerformanceLogger>)perfLogger
{
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(self, jsInvoker, nativeInvoker, perfLogger);
}
@end
#else // Unavailable when not in dev mode
@interface RCTDevMenu () <NativeDevMenuSpec>
@end
@implementation RCTDevMenu
- (void)show
{
}
- (void)reload
{
}
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler
{
}
- (void)addItem:(RCTDevMenu *)item
{
}
- (void)debugRemotely:(BOOL)enableDebug
{
}
- (BOOL)isActionSheetShown
{
return NO;
}
+ (NSString *)moduleName
{
return @"DevMenu";
}
- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
nativeInvoker:(std::shared_ptr<facebook::react::CallInvoker>)nativeInvoker
perfLogger:(id<RCTTurboModulePerformanceLogger>)perfLogger
{
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(self, jsInvoker, nativeInvoker, perfLogger);
}
@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_MENU
return [self moduleForClass:[RCTDevMenu class]];
#else
return nil;
#endif
}
@end
Class RCTDevMenuCls(void)
{
return RCTDevMenu.class;
}