792 lines
24 KiB
Plaintext
792 lines
24 KiB
Plaintext
/**
|
|
* 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"
|
|
#import "RCTDefines.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTKeyCommands.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTProfile.h"
|
|
#import "RCTRootView.h"
|
|
#import "RCTUtils.h"
|
|
#import "RCTWebSocketObserverProtocol.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 () <RCTBridgeModule, RCTInvalidating, RCTWebSocketObserverDelegate>
|
|
|
|
@property (nonatomic, strong) Class executorClass;
|
|
|
|
@end
|
|
|
|
@implementation RCTDevMenu
|
|
{
|
|
UIAlertController *_actionSheet;
|
|
NSUserDefaults *_defaults;
|
|
NSMutableDictionary *_settings;
|
|
NSURLSessionDataTask *_updateTask;
|
|
NSURL *_liveReloadURL;
|
|
BOOL _jsLoaded;
|
|
NSMutableArray<RCTDevMenuItem *> *_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=shell", 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 = @[ @1 ];
|
|
return [kSupportedVersions containsObject:version];
|
|
}
|
|
|
|
- (void)didReceiveWebSocketMessage:(NSDictionary<NSString *, id> *)message
|
|
{
|
|
if ([self isSupportedVersion:message[@"version"]]) {
|
|
[self processTarget:message[@"target"] action:message[@"action"] options:message[@"options"]];
|
|
}
|
|
}
|
|
|
|
- (void)processTarget:(NSString *)target action:(NSString *)action options:(NSDictionary<NSString *, id> *)options
|
|
{
|
|
if ([target isEqualToString:@"bridge"]) {
|
|
if ([action isEqualToString:@"reload"]) {
|
|
if ([options[@"debug"] boolValue]) {
|
|
_bridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor");
|
|
}
|
|
[_bridge reload];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (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<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;
|
|
}]];
|
|
}
|
|
|
|
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<RCTDevMenuItem *> *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
|