react-native/React/Base/RCTDevMenu.m

461 lines
12 KiB
Mathematica
Raw Normal View History

/**
* 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"
2015-04-19 19:55:46 +00:00
#import "RCTBridge.h"
2015-04-30 16:50:22 +00:00
#import "RCTDefines.h"
#import "RCTEventDispatcher.h"
2015-04-30 16:50:22 +00:00
#import "RCTKeyCommands.h"
2015-04-19 19:55:46 +00:00
#import "RCTLog.h"
#import "RCTPerfStats.h"
#import "RCTProfile.h"
#import "RCTRootView.h"
2015-04-02 14:33:21 +00:00
#import "RCTSourceCode.h"
2015-04-19 19:55:46 +00:00
#import "RCTUtils.h"
2015-04-30 16:50:22 +00:00
#if RCT_DEV
2015-04-19 19:55:46 +00:00
@interface RCTBridge (Profiling)
- (void)startProfiling;
- (void)stopProfiling;
@end
2015-04-19 19:55:46 +00:00
static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
2015-05-01 13:21:03 +00:00
static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu";
2015-04-19 19:55:46 +00:00
@implementation UIWindow (RCTDevMenu)
- (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
}
}
@end
2015-05-01 13:21:03 +00:00
@interface RCTDevMenu () <RCTBridgeModule, UIActionSheetDelegate>
@property (nonatomic, strong) Class executorClass;
@end
@implementation RCTDevMenu
2015-04-11 22:08:00 +00:00
{
2015-04-19 19:55:46 +00:00
UIActionSheet *_actionSheet;
2015-05-01 13:21:03 +00:00
NSUserDefaults *_defaults;
NSMutableDictionary *_settings;
NSURLSessionDataTask *_updateTask;
NSURL *_liveReloadURL;
BOOL _jsLoaded;
2015-04-11 22:08:00 +00:00
}
2015-04-19 19:55:46 +00:00
@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
{
2015-04-19 19:55:46 +00:00
if ((self = [super init])) {
2015-05-01 13:21:03 +00:00
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:@selector(showOnShake)
name:RCTShowDevMenuNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(settingsDidChange)
2015-05-01 13:21:03 +00:00
name:NSUserDefaultsDidChangeNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(jsLoaded:)
2015-05-01 13:21:03 +00:00
name:RCTJavaScriptDidLoadNotification
object:nil];
2015-04-30 16:50:22 +00:00
2015-05-07 10:53:35 +00:00
_defaults = [NSUserDefaults standardUserDefaults];
_settings = [[NSMutableDictionary alloc] init];
2015-04-30 16:50:22 +00:00
2015-05-07 10:53:35 +00:00
// Delay setup until after Bridge init
[self settingsDidChange];
2015-05-07 10:53:35 +00:00
#if TARGET_IPHONE_SIMULATOR
__weak RCTDevMenu *weakSelf = self;
2015-04-30 16:50:22 +00:00
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
2015-05-07 10:53:35 +00:00
// Toggle debug menu
2015-05-01 13:21:03 +00:00
[commands registerKeyCommandWithInput:@"d"
2015-04-30 16:50:22 +00:00
modifierFlags:UIKeyModifierCommand
2015-05-01 13:21:03 +00:00
action:^(UIKeyCommand *command) {
[weakSelf toggle];
}];
2015-04-30 16:50:22 +00:00
2015-06-03 17:25:53 +00:00
// Toggle element inspector
[commands registerKeyCommandWithInput:@"i"
modifierFlags:UIKeyModifierCommand
action:^(UIKeyCommand *command) {
[_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
}];
2015-05-07 10:53:35 +00:00
// Reload in normal mode
2015-05-01 13:21:03 +00:00
[commands registerKeyCommandWithInput:@"n"
2015-04-30 16:50:22 +00:00
modifierFlags:UIKeyModifierCommand
action:^(UIKeyCommand *command) {
2015-05-01 13:21:03 +00:00
weakSelf.executorClass = Nil;
2015-04-30 16:50:22 +00:00
}];
#endif
}
return self;
}
2015-05-07 10:53:35 +00:00
- (dispatch_queue_t)methodQueue
2015-05-01 13:21:03 +00:00
{
2015-05-07 10:53:35 +00:00
return dispatch_get_main_queue();
}
- (void)settingsDidChange
{
// Needed to prevent a race condition when reloading
__weak RCTDevMenu *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateSettings];
});
}
2015-05-07 10:53:35 +00:00
- (void)updateSettings
{
NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey];
if ([settings isEqualToDictionary:_settings]) {
return;
}
2015-05-06 17:46:45 +00:00
2015-05-07 10:53:35 +00:00
[_settings setDictionary:settings];
self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue];
self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue];
self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue];
self.showFPS = [_settings[@"showFPS"] ?: @NO boolValue];
2015-05-07 10:53:35 +00:00
self.executorClass = NSClassFromString(_settings[@"executorClass"]);
2015-05-01 13:21:03 +00:00
}
- (void)jsLoaded:(NSNotification *)notification
2015-05-01 13:21:03 +00:00
{
if (notification.userInfo[@"bridge"] != _bridge) {
return;
}
2015-05-01 13:21:03 +00:00
_jsLoaded = YES;
// Check if live reloading is available
_liveReloadURL = nil;
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
if (!sourceCodeModule.scriptURL) {
if (!sourceCodeModule) {
RCTLogWarn(@"RCTSourceCode module not found");
} else {
RCTLogWarn(@"RCTSourceCode module scriptURL has not been set");
}
} else if (![sourceCodeModule.scriptURL isFileURL]) {
// Live reloading is disabled when running from bundled JS file
_liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL];
}
dispatch_async(dispatch_get_main_queue(), ^{
// Hit these setters again after bridge has finished loading
self.profilingEnabled = _profilingEnabled;
self.liveReloadEnabled = _liveReloadEnabled;
self.executorClass = _executorClass;
});
2015-05-01 13:21:03 +00:00
}
2015-05-07 10:53:35 +00:00
- (BOOL)isValid
{
return NO;
}
2015-04-19 19:55:46 +00:00
- (void)dealloc
{
2015-05-01 13:21:03 +00:00
[_updateTask cancel];
[_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES];
2015-04-19 19:55:46 +00:00
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
2015-05-01 13:21:03 +00:00
- (void)updateSetting:(NSString *)name value:(id)value
{
2015-05-07 10:53:35 +00:00
id currentValue = _settings[name];
if (currentValue == value || [currentValue isEqual:value]) {
return;
}
2015-05-01 13:21:03 +00:00
if (value) {
_settings[name] = value;
} else {
[_settings removeObjectForKey:name];
}
[_defaults setObject:_settings forKey:RCTDevMenuSettingsKey];
[_defaults synchronize];
}
2015-04-19 19:55:46 +00:00
- (void)showOnShake
{
if (_shakeToShow) {
[self show];
}
}
2015-05-01 13:21:03 +00:00
- (void)toggle
{
2015-04-19 19:55:46 +00:00
if (_actionSheet) {
2015-05-01 13:21:03 +00:00
[_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES];
_actionSheet = nil;
} else {
[self show];
}
}
RCT_EXPORT_METHOD(show)
{
if (_actionSheet || !_bridge) {
2015-04-19 19:55:46 +00:00
return;
}
2015-05-01 13:21:03 +00:00
NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome";
NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari";
NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor";
2015-04-19 19:55:46 +00:00
UIActionSheet *actionSheet =
[[UIActionSheet alloc] initWithTitle:@"React Native: Development"
delegate:self
2015-05-01 13:21:03 +00:00
cancelButtonTitle:nil
2015-04-19 19:55:46 +00:00
destructiveButtonTitle:nil
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil];
2015-05-01 13:21:03 +00:00
[actionSheet addButtonWithTitle:@"Inspect Element"];
2015-05-01 13:21:03 +00:00
if (_liveReloadURL) {
NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling";
[actionSheet addButtonWithTitle:liveReloadTitle];
[actionSheet addButtonWithTitle:profilingTitle];
}
[actionSheet addButtonWithTitle:@"Cancel"];
actionSheet.cancelButtonIndex = [actionSheet numberOfButtons] - 1;
2015-04-19 19:55:46 +00:00
actionSheet.actionSheetStyle = UIBarStyleBlack;
2015-04-19 19:55:46 +00:00
[actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view];
2015-04-30 16:50:22 +00:00
_actionSheet = actionSheet;
}
2015-05-01 13:21:03 +00:00
RCT_EXPORT_METHOD(reload)
{
_jsLoaded = NO;
_liveReloadURL = nil;
[_bridge reload];
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
2015-04-19 19:55:46 +00:00
_actionSheet = nil;
2015-05-07 10:53:35 +00:00
if (buttonIndex == actionSheet.cancelButtonIndex) {
return;
}
2015-04-19 19:55:46 +00:00
switch (buttonIndex) {
case 0: {
2015-05-01 13:21:03 +00:00
[self reload];
2015-04-19 19:55:46 +00:00
break;
}
case 1: {
Class cls = NSClassFromString(@"RCTWebSocketExecutor");
if (!cls) {
[[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable"
message:@"You need to include the RCTWebSocket library to enable Chrome debugging"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
return;
}
2015-05-01 13:21:03 +00:00
self.executorClass = (_executorClass == cls) ? Nil : cls;
2015-04-19 19:55:46 +00:00
break;
}
case 2: {
Class cls = NSClassFromString(@"RCTWebViewExecutor");
2015-05-01 13:21:03 +00:00
self.executorClass = (_executorClass == cls) ? Nil : cls;
2015-04-19 19:55:46 +00:00
break;
}
case 3: {
self.showFPS = !_showFPS;
2015-04-19 19:55:46 +00:00
break;
}
case 4: {
[_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
break;
}
case 5: {
self.liveReloadEnabled = !_liveReloadEnabled;
break;
}
case 6: {
2015-04-19 19:55:46 +00:00
self.profilingEnabled = !_profilingEnabled;
break;
}
2015-04-19 19:55:46 +00:00
default:
break;
}
}
2015-05-01 13:21:03 +00:00
- (void)setShakeToShow:(BOOL)shakeToShow
{
if (_shakeToShow != shakeToShow) {
_shakeToShow = shakeToShow;
[self updateSetting:@"shakeToShow" value: @(_shakeToShow)];
}
}
2015-04-19 19:55:46 +00:00
- (void)setProfilingEnabled:(BOOL)enabled
{
2015-05-01 13:21:03 +00:00
if (_profilingEnabled != enabled) {
_profilingEnabled = enabled;
[self updateSetting:@"profilingEnabled" value: @(_profilingEnabled)];
2015-04-19 19:55:46 +00:00
}
2015-05-01 13:21:03 +00:00
if (_liveReloadURL && enabled != RCTProfileIsProfiling()) {
if (enabled) {
[_bridge startProfiling];
} else {
[_bridge stopProfiling];
}
}
}
2015-04-19 19:55:46 +00:00
- (void)setLiveReloadEnabled:(BOOL)enabled
{
2015-05-01 13:21:03 +00:00
if (_liveReloadEnabled != enabled) {
_liveReloadEnabled = enabled;
[self updateSetting:@"liveReloadEnabled" value: @(_liveReloadEnabled)];
2015-04-19 19:55:46 +00:00
}
if (_liveReloadEnabled) {
2015-05-01 13:21:03 +00:00
[self checkForUpdates];
2015-04-19 19:55:46 +00:00
} else {
2015-05-01 13:21:03 +00:00
[_updateTask cancel];
_updateTask = nil;
2015-04-19 19:55:46 +00:00
}
}
2015-05-01 13:21:03 +00:00
- (void)setExecutorClass:(Class)executorClass
2015-04-19 19:55:46 +00:00
{
2015-05-01 13:21:03 +00:00
if (_executorClass != executorClass) {
_executorClass = executorClass;
[self updateSetting:@"executorClass" value: NSStringFromClass(executorClass)];
2015-04-19 19:55:46 +00:00
}
2015-05-01 13:21:03 +00:00
if (_bridge.executorClass != executorClass) {
2015-04-19 19:55:46 +00:00
2015-05-01 13:21:03 +00:00
// 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 != NSClassFromString(@"RCTWebSocketExecutor") &&
_bridge.executorClass != NSClassFromString(@"RCTWebViewExecutor"))) {
return;
}
2015-04-19 19:55:46 +00:00
2015-05-01 13:21:03 +00:00
_bridge.executorClass = executorClass;
[self reload];
}
2015-04-19 19:55:46 +00:00
}
- (void)setShowFPS:(BOOL)showFPS
{
if (_showFPS != showFPS) {
_showFPS = showFPS;
if (showFPS) {
[_bridge.perfStats show];
} else {
[_bridge.perfStats hide];
}
[self updateSetting:@"showFPS" value:@(showFPS)];
}
}
2015-05-01 13:21:03 +00:00
- (void)checkForUpdates
2015-04-19 19:55:46 +00:00
{
2015-05-01 13:21:03 +00:00
if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) {
return;
}
2015-04-19 19:55:46 +00:00
2015-05-01 13:21:03 +00:00
if (_updateTask) {
[_updateTask cancel];
_updateTask = nil;
return;
}
__weak RCTDevMenu *weakSelf = self;
_updateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
__strong RCTDevMenu *strongSelf = weakSelf;
if (strongSelf && strongSelf->_liveReloadEnabled) {
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
if (!error && HTTPResponse.statusCode == 205) {
[strongSelf reload];
} else {
strongSelf->_updateTask = nil;
[strongSelf checkForUpdates];
}
}
});
}];
[_updateTask resume];
2015-04-19 19:55:46 +00:00
}
@end
2015-05-01 13:21:03 +00:00
#else // Unavailable when not in dev mode
2015-04-30 16:50:22 +00:00
@implementation RCTDevMenu
- (void)show {}
2015-05-01 13:21:03 +00:00
- (void)reload {}
2015-04-30 16:50:22 +00:00
@end
#endif
2015-04-19 19:55:46 +00:00
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
return self.modules[RCTBridgeModuleNameForClass([RCTDevMenu class])];
}
@end