Added toggle items to dev menu

Reviewed By: @tadeuzagallo

Differential Revision: D2424595
This commit is contained in:
Nick Lockwood 2015-09-14 09:34:33 -07:00 committed by facebook-github-bot-3
parent eb48759675
commit 515d5a5f4b
3 changed files with 252 additions and 86 deletions

View File

@ -225,7 +225,7 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
} }
__block BOOL isProfiling = NO; __block BOOL isProfiling = NO;
[bridge.devMenu addItem:@"Profile" handler:^{ [bridge.devMenu addItem:[RCTDevMenuItem buttonItemWithTitle:@"Profile" handler:^{
if (isProfiling) { if (isProfiling) {
NSString *outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cpu_profile.json"]; NSString *outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cpu_profile.json"];
nativeProfilerEnd(context, "profile", outputFile.UTF8String); nativeProfilerEnd(context, "profile", outputFile.UTF8String);
@ -238,7 +238,7 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
nativeProfilerStart(context, "profile"); nativeProfilerStart(context, "profile");
} }
isProfiling = !isProfiling; isProfiling = !isProfiling;
}]; }]];
} }
} }
#endif #endif

View File

@ -12,6 +12,8 @@
#import "RCTBridge.h" #import "RCTBridge.h"
#import "RCTBridgeModule.h" #import "RCTBridgeModule.h"
@class RCTDevMenuItem;
/** /**
* Developer menu, useful for exposing extra functionality when debugging. * Developer menu, useful for exposing extra functionality when debugging.
*/ */
@ -35,7 +37,7 @@
@property (nonatomic, assign) BOOL liveReloadEnabled; @property (nonatomic, assign) BOOL liveReloadEnabled;
/** /**
* Shows the FPS monitor for the JS and Main threads * Shows the FPS monitor for the JS and Main threads.
*/ */
@property (nonatomic, assign) BOOL showFPS; @property (nonatomic, assign) BOOL showFPS;
@ -50,14 +52,44 @@
*/ */
- (void)reload; - (void)reload;
/**
* Deprecated. Use the `-addItem:` method instead.
*/
- (void)addItem:(NSString *)title
handler:(void(^)(void))handler DEPRECATED_ATTRIBUTE;
/** /**
* Add custom item to the development menu. The handler will be called * Add custom item to the development menu. The handler will be called
* when user selects the item. * when user selects the item.
*/ */
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler; - (void)addItem:(RCTDevMenuItem *)item;
@end @end
/**
* Developer menu item, used to expose additional functionality via the menu.
*/
@interface RCTDevMenuItem : NSObject
/**
* This creates an item with a simple push-button interface, used to trigger an
* action.
*/
+ (instancetype)buttonItemWithTitle:(NSString *)title
handler:(void(^)(void))handler;
/**
* This creates an item with a toggle behavior. The key is used to store the
* state of the toggle. For toggle items, the handler will be called immediately
* after the item is added if the item was already selected when the module was
* last loaded.
*/
+ (instancetype)toggleItemWithKey:(NSString *)key
title:(NSString *)title
selectedTitle:(NSString *)selectedTitle
handler:(void(^)(BOOL selected))handler;
@end
/** /**
* This category makes the developer menu instance available via the * This category makes the developer menu instance available via the
* RCTBridge, which is useful for any class that needs to access the menu. * RCTBridge, which is useful for any class that needs to access the menu.

View File

@ -44,31 +44,88 @@ static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu";
@end @end
@interface RCTDevMenuItem : NSObject typedef NS_ENUM(NSInteger, RCTDevMenuType) {
RCTDevMenuTypeButton,
RCTDevMenuTypeToggle
};
@property (nonatomic, copy) NSString *title; @interface RCTDevMenuItem ()
@property (nonatomic, copy) dispatch_block_t handler;
- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler NS_DESIGNATED_INITIALIZER; @property (nonatomic, assign, readonly) RCTDevMenuType type;
@property (nonatomic, copy, readonly) NSString *key;
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, copy, readonly) NSString *selectedTitle;
@property (nonatomic, copy) id value;
@end @end
@implementation RCTDevMenuItem @implementation RCTDevMenuItem
{
id _handler; // block
}
- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler - (instancetype)initWithType:(RCTDevMenuType)type
key:(NSString *)key
title:(NSString *)title
selectedTitle:(NSString *)selectedTitle
handler:(id /* block */)handler
{ {
if ((self = [super init])) { if ((self = [super init])) {
_type = type;
_key = [key copy];
_title = [title copy]; _title = [title copy];
_selectedTitle = [selectedTitle copy];
_handler = [handler copy]; _handler = [handler copy];
_value = nil;
} }
return self; return self;
} }
RCT_NOT_IMPLEMENTED(- (instancetype)init) 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 @end
@interface RCTDevMenu () <RCTBridgeModule, UIActionSheetDelegate> @interface RCTDevMenu () <RCTBridgeModule, UIActionSheetDelegate, RCTInvalidating>
@property (nonatomic, strong) Class executorClass; @property (nonatomic, strong) Class executorClass;
@ -125,15 +182,42 @@ RCT_EXPORT_MODULE()
object:nil]; object:nil];
_defaults = [NSUserDefaults standardUserDefaults]; _defaults = [NSUserDefaults standardUserDefaults];
_settings = [NSMutableDictionary new]; _settings = [[NSMutableDictionary alloc] initWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]];
_extraMenuItems = [NSMutableArray array]; _extraMenuItems = [NSMutableArray new];
__weak RCTDevMenu *weakSelf = self;
[_extraMenuItems addObject:[RCTDevMenuItem toggleItemWithKey:@"showFPS"
title:@"Show FPS Monitor"
selectedTitle:@"Hide FPS Monitor"
handler:^(BOOL showFPS)
{
RCTDevMenu *strongSelf = weakSelf;
if (strongSelf) {
strongSelf->_showFPS = showFPS;
if (showFPS) {
[strongSelf.bridge.perfStats show];
} else {
[strongSelf.bridge.perfStats hide];
}
}
}]];
[_extraMenuItems addObject:[RCTDevMenuItem toggleItemWithKey:@"showInspector"
title:@"Show Inspector"
selectedTitle:@"Hide Inspector"
handler:^(__unused BOOL enabled)
{
[weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
}]];
// Delay setup until after Bridge init // Delay setup until after Bridge init
[self settingsDidChange]; dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateSettings:_settings];
});
#if TARGET_IPHONE_SIMULATOR #if TARGET_IPHONE_SIMULATOR
__weak RCTDevMenu *weakSelf = self;
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
// Toggle debug menu // Toggle debug menu
@ -173,14 +257,31 @@ RCT_EXPORT_MODULE()
{ {
// Needed to prevent a race condition when reloading // Needed to prevent a race condition when reloading
__weak RCTDevMenu *weakSelf = self; __weak RCTDevMenu *weakSelf = self;
NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateSettings]; [weakSelf updateSettings:settings];
}); });
} }
- (void)updateSettings /**
* 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
{ {
NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey]; // 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];
}
}
}
if ([settings isEqualToDictionary:_settings]) { if ([settings isEqualToDictionary:_settings]) {
return; return;
} }
@ -193,6 +294,39 @@ RCT_EXPORT_MODULE()
self.executorClass = NSClassFromString(_settings[@"executorClass"]); self.executorClass = NSClassFromString(_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 - (void)jsLoaded:(NSNotification *)notification
{ {
if (notification.userInfo[@"bridge"] != _bridge) { if (notification.userInfo[@"bridge"] != _bridge) {
@ -220,31 +354,22 @@ RCT_EXPORT_MODULE()
self.profilingEnabled = _profilingEnabled; self.profilingEnabled = _profilingEnabled;
self.liveReloadEnabled = _liveReloadEnabled; self.liveReloadEnabled = _liveReloadEnabled;
self.executorClass = _executorClass; self.executorClass = _executorClass;
// Inspector can only be shown after JS has loaded
if ([_settings[@"showInspector"] boolValue]) {
[self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
}
}); });
} }
- (void)dealloc - (void)invalidate
{ {
_presentedItems = nil;
[_updateTask cancel]; [_updateTask cancel];
[_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES];
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self];
} }
- (void)updateSetting:(NSString *)name value:(id)value
{
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)showOnShake - (void)showOnShake
{ {
if (_shakeToShow) { if (_shakeToShow) {
@ -262,22 +387,34 @@ RCT_EXPORT_MODULE()
} }
} }
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler - (void)addItem:(NSString *)title handler:(void(^)(void))handler
{ {
[_extraMenuItems addObject:[[RCTDevMenuItem alloc] initWithTitle:title handler: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 *)menuItems - (NSArray *)menuItems
{ {
NSMutableArray *items = [NSMutableArray array]; NSMutableArray *items = [NSMutableArray new];
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Reload" handler:^{ // Add built-in items
[self reload];
__weak RCTDevMenu *weakSelf = self;
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload" handler:^{
[weakSelf reload];
}]]; }]];
Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor");
if (!chromeExecutorClass) { if (!chromeExecutorClass) {
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Chrome Debugger Unavailable" handler:^{ [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Chrome Debugger Unavailable" handler:^{
[[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable"
message:@"You need to include the RCTWebSocket library to enable Chrome debugging" message:@"You need to include the RCTWebSocket library to enable Chrome debugging"
delegate:nil delegate:nil
@ -286,37 +423,28 @@ RCT_EXPORT_MODULE()
}]]; }]];
} else { } else {
BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass;
NSString *debugTitleChrome = isDebuggingInChrome ? @"Stop Chrome Debugging" : @"Debug in Chrome"; NSString *debugTitleChrome = isDebuggingInChrome ? @"Disable Chrome Debugging" : @"Debug in Chrome";
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleChrome handler:^{ [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleChrome handler:^{
self.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; weakSelf.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass;
}]]; }]];
} }
Class safariExecutorClass = NSClassFromString(@"RCTWebViewExecutor"); Class safariExecutorClass = NSClassFromString(@"RCTWebViewExecutor");
BOOL isDebuggingInSafari = _executorClass && _executorClass == safariExecutorClass; BOOL isDebuggingInSafari = _executorClass && _executorClass == safariExecutorClass;
NSString *debugTitleSafari = isDebuggingInSafari ? @"Stop Safari Debugging" : @"Debug in Safari"; NSString *debugTitleSafari = isDebuggingInSafari ? @"Disable Safari Debugging" : @"Debug in Safari";
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleSafari handler:^{ [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleSafari handler:^{
self.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass; weakSelf.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass;
}]];
NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor";
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:fpsMonitor handler:^{
self.showFPS = !_showFPS;
}]];
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Inspect Element" handler:^{
[_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
}]]; }]];
if (_liveReloadURL) { if (_liveReloadURL) {
NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:liveReloadTitle handler:^{ [items addObject:[RCTDevMenuItem buttonItemWithTitle:liveReloadTitle handler:^{
self.liveReloadEnabled = !_liveReloadEnabled; weakSelf.liveReloadEnabled = !_liveReloadEnabled;
}]]; }]];
NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Systrace" : @"Start Systrace"; NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Systrace" : @"Start Systrace";
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:profilingTitle handler:^{ [items addObject:[RCTDevMenuItem buttonItemWithTitle:profilingTitle handler:^{
self.profilingEnabled = !_profilingEnabled; weakSelf.profilingEnabled = !_profilingEnabled;
}]]; }]];
} }
@ -337,7 +465,17 @@ RCT_EXPORT_METHOD(show)
NSArray *items = [self menuItems]; NSArray *items = [self menuItems];
for (RCTDevMenuItem *item in items) { for (RCTDevMenuItem *item in items) {
switch (item.type) {
case RCTDevMenuTypeButton: {
[actionSheet addButtonWithTitle:item.title]; [actionSheet addButtonWithTitle:item.title];
break;
}
case RCTDevMenuTypeToggle: {
BOOL selected = [item.value boolValue];
[actionSheet addButtonWithTitle:selected? item.selectedTitle : item.title];
break;
}
}
} }
[actionSheet addButtonWithTitle:@"Cancel"]; [actionSheet addButtonWithTitle:@"Cancel"];
@ -357,7 +495,17 @@ RCT_EXPORT_METHOD(show)
} }
RCTDevMenuItem *item = _presentedItems[buttonIndex]; RCTDevMenuItem *item = _presentedItems[buttonIndex];
item.handler(); switch (item.type) {
case RCTDevMenuTypeButton: {
[item callHandler];
break;
}
case RCTDevMenuTypeToggle: {
BOOL value = [_settings[item.key] boolValue];
[self updateSetting:item.key value:@(!value)]; // will call handler
break;
}
}
return; return;
} }
@ -370,18 +518,14 @@ RCT_EXPORT_METHOD(reload)
- (void)setShakeToShow:(BOOL)shakeToShow - (void)setShakeToShow:(BOOL)shakeToShow
{ {
if (_shakeToShow != shakeToShow) {
_shakeToShow = shakeToShow; _shakeToShow = shakeToShow;
[self updateSetting:@"shakeToShow" value: @(_shakeToShow)]; [self updateSetting:@"shakeToShow" value:@(_shakeToShow)];
}
} }
- (void)setProfilingEnabled:(BOOL)enabled - (void)setProfilingEnabled:(BOOL)enabled
{ {
if (_profilingEnabled != enabled) {
_profilingEnabled = enabled; _profilingEnabled = enabled;
[self updateSetting:@"profilingEnabled" value: @(_profilingEnabled)]; [self updateSetting:@"profilingEnabled" value:@(_profilingEnabled)];
}
if (_liveReloadURL && enabled != RCTProfileIsProfiling()) { if (_liveReloadURL && enabled != RCTProfileIsProfiling()) {
if (enabled) { if (enabled) {
@ -394,10 +538,8 @@ RCT_EXPORT_METHOD(reload)
- (void)setLiveReloadEnabled:(BOOL)enabled - (void)setLiveReloadEnabled:(BOOL)enabled
{ {
if (_liveReloadEnabled != enabled) {
_liveReloadEnabled = enabled; _liveReloadEnabled = enabled;
[self updateSetting:@"liveReloadEnabled" value: @(_liveReloadEnabled)]; [self updateSetting:@"liveReloadEnabled" value:@(_liveReloadEnabled)];
}
if (_liveReloadEnabled) { if (_liveReloadEnabled) {
[self checkForUpdates]; [self checkForUpdates];
@ -411,7 +553,7 @@ RCT_EXPORT_METHOD(reload)
{ {
if (_executorClass != executorClass) { if (_executorClass != executorClass) {
_executorClass = executorClass; _executorClass = executorClass;
[self updateSetting:@"executorClass" value: NSStringFromClass(executorClass)]; [self updateSetting:@"executorClass" value:NSStringFromClass(executorClass)];
} }
if (_bridge.executorClass != executorClass) { if (_bridge.executorClass != executorClass) {
@ -433,17 +575,8 @@ RCT_EXPORT_METHOD(reload)
- (void)setShowFPS:(BOOL)showFPS - (void)setShowFPS:(BOOL)showFPS
{ {
if (_showFPS != showFPS) {
_showFPS = showFPS; _showFPS = showFPS;
if (showFPS) {
[_bridge.perfStats show];
} else {
[_bridge.perfStats hide];
}
[self updateSetting:@"showFPS" value:@(showFPS)]; [self updateSetting:@"showFPS" value:@(showFPS)];
}
} }
- (void)checkForUpdates - (void)checkForUpdates
@ -489,6 +622,7 @@ RCT_EXPORT_METHOD(reload)
- (void)show {} - (void)show {}
- (void)reload {} - (void)reload {}
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {} - (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {}
- (void)addItem:(RCTDevMenu *)item {}
@end @end