react-native/React/Modules/RCTDevMenu.mm

790 lines
24 KiB
Plaintext
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"
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h>
#import <jschelpers/JavaScriptCore.h>
#import "JSCSamplingProfiler.h"
#import "RCTAssert.h"
#import "RCTBridge+Private.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 "RCTProfile.h"
#import "RCTRootView.h"
2015-04-19 19:55:46 +00:00
#import "RCTUtils.h"
#import "RCTWebSocketObserverProtocol.h"
2015-04-30 16:50:22 +00:00
#if RCT_DEV
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:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
2015-04-19 19:55:46 +00:00
{
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 () <RCTBridgeModule, RCTInvalidating, RCTWebSocketObserverDelegate>
2015-05-01 13:21:03 +00:00
@property (nonatomic, strong) Class executorClass;
@end
@implementation RCTDevMenu
2015-04-11 22:08:00 +00:00
{
UIAlertController *_actionSheet;
2015-05-01 13:21:03 +00:00
NSUserDefaults *_defaults;
NSMutableDictionary *_settings;
NSURLSessionDataTask *_updateTask;
NSURL *_liveReloadURL;
BOOL _jsLoaded;
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
NSString *_webSocketExecutorName;
NSString *_executorOverride;
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] 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
}]];
2015-04-30 16:50:22 +00:00
_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];
2015-05-07 10:53:35 +00:00
// Delay setup until after Bridge init
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateSettings:self->_settings];
[weakSelf connectPackager];
});
2015-05-07 10:53:35 +00:00
#if TARGET_IPHONE_SIMULATOR
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
action:^(__unused UIKeyCommand *command) {
2015-05-01 13:21:03 +00:00
[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:^(__unused UIKeyCommand *command) {
[weakSelf.bridge.eventDispatcher
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
sendDeviceEventWithName:@"toggleElementInspector"
body:nil];
#pragma clang diagnostic pop
2015-06-03 17:25:53 +00:00
}];
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:^(__unused UIKeyCommand *command) {
2015-05-01 13:21:03 +00:00
weakSelf.executorClass = Nil;
2015-04-30 16:50:22 +00:00
}];
#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;
}
Class webSocketObserverClass = objc_lookUpClass("RCTWebSocketObserver");
if (webSocketObserverClass == Nil) {
return;
}
// If multiple RCTDevMenus are created, the most recently connected one steals the RCTWebSocketObserver.
// (Why this behavior exists is beyond me, as of this writing.)
static NSMutableDictionary<NSString *, id<RCTWebSocketObserver>> *observers = nil;
if (observers == nil) {
observers = [NSMutableDictionary new];
}
NSString *key = [url absoluteString];
id<RCTWebSocketObserver> existingObserver = observers[key];
if (existingObserver) {
existingObserver.delegate = self;
} else {
id<RCTWebSocketObserver> newObserver = [(id<RCTWebSocketObserver>)[webSocketObserverClass alloc] initWithURL:url];
newObserver.delegate = self;
[newObserver start];
observers[key] = newObserver;
}
}
- (BOOL)isSupportedVersion:(NSNumber *)version
{
NSArray<NSNumber *> *const kSupportedVersions = @[ @2 ];
return [kSupportedVersions containsObject:version];
}
- (void)didReceiveWebSocketMessage:(NSDictionary<NSString *, id> *)message
{
if ([self isSupportedVersion:message[@"version"]]) {
[self processMethod:message[@"method"] params:message[@"params"]];
}
}
- (void)processMethod:(NSString *)method params:(NSDictionary<NSString *, id> *)params
{
if ([method isEqualToString:@"reload"]) {
if (![params isEqual:[NSNull null]] && [params[@"debug"] boolValue]) {
_bridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor");
}
[_bridge reload];
}
}
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;
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
2015-05-07 10:53:35 +00:00
{
[_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];
}
}
}
2015-05-07 10:53:35 +00:00
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"]);
2015-05-01 13:21:03 +00:00
}
/**
* 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
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
NSURL *scriptURL = _bridge.bundleURL;
if (![scriptURL isFileURL]) {
2015-05-01 13:21:03 +00:00
// Live reloading is disabled when running from bundled JS file
_liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:scriptURL];
} else {
_liveReloadURL = nil;
2015-05-01 13:21:03 +00:00
}
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
}
});
2015-05-01 13:21:03 +00:00
}
- (void)invalidate
2015-04-19 19:55:46 +00:00
{
_presentedItems = nil;
2015-05-01 13:21:03 +00:00
[_updateTask cancel];
[_actionSheet dismissViewControllerAnimated:YES completion:^(void){}];
2015-04-19 19:55:46 +00:00
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (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) {
[_actionSheet dismissViewControllerAnimated:YES completion:^(void){}];
2015-05-01 13:21:03 +00:00
_actionSheet = nil;
} else {
[self show];
}
}
- (void)addItem:(NSString *)title handler:(void(^)(void))handler
2015-05-01 13:21:03 +00:00
{
[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<RCTDevMenuItem *> *)menuItems
{
NSMutableArray<RCTDevMenuItem *> *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;
}]];
2015-04-19 19:55:46 +00:00
}
2015-05-01 13:21:03 +00:00
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;
}
2015-05-01 13:21:03 +00:00
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<RCTDevMenuItem *> *items = [self menuItems];
for (RCTDevMenuItem *item in items) {
[_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
style:UIAlertActionStyleDefault
handler:[self alertActionHandlerForDevItem:item]]];
2015-05-01 13:21:03 +00:00
}
[_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:[self alertActionHandlerForDevItem:nil]]];
2015-04-19 19:55:46 +00:00
_presentedItems = items;
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
2015-05-01 13:21:03 +00:00
}
- (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;
}
}
}
2015-04-19 19:55:46 +00:00
self->_actionSheet = nil;
};
}
RCT_EXPORT_METHOD(reload)
{
[_bridge reload];
}
RCT_EXPORT_METHOD(debugRemotely:(BOOL)enableDebug)
{
Class jsDebuggingExecutorClass = NSClassFromString(@"RCTWebSocketExecutor");
self.executorClass = enableDebug ? jsDebuggingExecutorClass : nil;
}
2015-05-01 13:21:03 +00:00
- (void)setShakeToShow:(BOOL)shakeToShow
{
_shakeToShow = shakeToShow;
[self updateSetting:@"shakeToShow" value:@(_shakeToShow)];
2015-05-01 13:21:03 +00:00
}
- (void)setStartSamplingProfilerOnLaunch:(BOOL)startSamplingProfilerOnLaunch
{
_startSamplingProfilerOnLaunch = startSamplingProfilerOnLaunch;
[self updateSetting:@"startSamplingProfilerOnLaunch" value:@(_startSamplingProfilerOnLaunch)];
}
RCT_EXPORT_METHOD(setProfilingEnabled:(BOOL)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:^(NSData *logData) {
RCTProfileSendResult(self->_bridge, @"systrace", logData);
}];
2015-05-01 13:21:03 +00:00
}
}
}
RCT_EXPORT_METHOD(setLiveReloadEnabled:(BOOL)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
}
}
- (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];
}
}
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;
_executorOverride = nil;
[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 != objc_lookUpClass("RCTWebSocketExecutor")) {
return;
}
2015-04-19 19:55:46 +00:00
2015-05-01 13:21:03 +00:00
_bridge.executorClass = executorClass;
[_bridge reload];
2015-05-01 13:21:03 +00:00
}
2015-04-19 19:55:46 +00:00
}
- (void)setShowFPS:(BOOL)showFPS
{
_showFPS = showFPS;
[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) {
return;
}
__weak RCTDevMenu *weakSelf = self;
_updateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:
^(__unused NSData *data, NSURLResponse *response, NSError *error) {
2015-05-01 13:21:03 +00:00
dispatch_async(dispatch_get_main_queue(), ^{
RCTDevMenu *strongSelf = weakSelf;
2015-05-01 13:21:03 +00:00
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];
}
2015-05-01 13:21:03 +00:00
}
}
});
}];
[_updateTask resume];
2015-04-19 19:55:46 +00:00
}
- (BOOL)isActionSheetShown
{
return _actionSheet != nil;
}
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 {}
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {}
- (void)addItem:(RCTDevMenu *)item {}
- (BOOL)isActionSheetShown { return NO; }
2015-04-30 16:50:22 +00:00
@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
2015-04-30 16:50:22 +00:00
#endif
2015-04-19 19:55:46 +00:00
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
#if RCT_DEV
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
return [self moduleForClass:[RCTDevMenu class]];
#else
return nil;
#endif
}
@end