From ba501a1bf50bb0ed9e16637e68965ef13cbcc6b3 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 1 May 2015 06:21:03 -0700 Subject: [PATCH] Upgraded dev menu --- Examples/UIExplorer/TabBarIOSExample.js | 2 +- Libraries/Components/TextInput/TextInput.js | 2 +- React/Base/RCTBridge.m | 7 +- React/Base/RCTDevMenu.h | 13 +- React/Base/RCTDevMenu.m | 288 ++++++++++++++------ React/Base/RCTKeyCommands.m | 11 + React/Base/RCTRedBox.m | 2 +- React/React.xcodeproj/project.pbxproj | 1 + 8 files changed, 223 insertions(+), 103 deletions(-) diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index a8f913a07..9b748ee33 100644 --- a/Examples/UIExplorer/TabBarIOSExample.js +++ b/Examples/UIExplorer/TabBarIOSExample.js @@ -78,7 +78,7 @@ var TabBarExample = React.createClass({ this.setState({ selectedTab: 'greenTab', presses: this.state.presses + 1 - }); + }); }}> {this._renderContent('#21551C', 'Green Tab')} diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index c21184b7d..fa87beefc 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -582,7 +582,7 @@ var TextInput = React.createClass({ var counter = event.nativeEvent.eventCounter; if (counter > this.state.mostRecentEventCounter) { this.setState({mostRecentEventCounter: counter}); - } + } }, }); diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 5dd3bac5a..85486ddc1 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -964,18 +964,13 @@ static id _latestJSExecutor; __weak RCTBridge *weakSelf = self; RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; - // Workaround around the first cmd+R not working: http://openradar.appspot.com/19613391 - // You can register just the cmd key and do nothing. This will trigger the bug and cmd+R - // will work like a charm! - [commands registerKeyCommandWithInput:@"" - modifierFlags:UIKeyModifierCommand - action:NULL]; // reload in current mode [commands registerKeyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { [weakSelf reload]; }]; + #endif } diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index 537675576..bb80ac208 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -36,15 +36,16 @@ @property (nonatomic, assign) BOOL liveReloadEnabled; /** - * The time between checks for code changes. Defaults to 1 second. - */ -@property (nonatomic, assign) NSTimeInterval liveReloadPeriod; - -/** - * Manually show the dev menu. + * Manually show the dev menu (can be called from JS). */ - (void)show; +/** + * Manually reload the application. Equivalent to calling [bridge reload] + * directly, but can be called from JS. + */ +- (void)reload; + @end /** diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 529840e0a..82b4fa968 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -28,6 +28,7 @@ @end static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification"; +static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu"; @implementation UIWindow (RCTDevMenu) @@ -40,14 +41,20 @@ static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification @end -@interface RCTDevMenu () +@interface RCTDevMenu () + +@property (nonatomic, strong) Class executorClass; @end @implementation RCTDevMenu { - NSTimer *_updateTimer; UIActionSheet *_actionSheet; + NSUserDefaults *_defaults; + NSMutableDictionary *_settings; + NSURLSessionDataTask *_updateTask; + NSURL *_liveReloadURL; + BOOL _jsLoaded; } @synthesize bridge = _bridge; @@ -66,31 +73,43 @@ RCT_EXPORT_MODULE() { if ((self = [super init])) { - _shakeToShow = YES; - _liveReloadPeriod = 1.0; // 1 second - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(showOnShake) - name:RCTShowDevMenuNotification - object:nil]; + _defaults = [NSUserDefaults standardUserDefaults]; + [self updateSettings]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter addObserver:self + selector:@selector(showOnShake) + name:RCTShowDevMenuNotification + object:nil]; + + [notificationCenter addObserver:self + selector:@selector(updateSettings) + name:NSUserDefaultsDidChangeNotification + object:nil]; + + [notificationCenter addObserver:self + selector:@selector(jsLoaded) + name:RCTJavaScriptDidLoadNotification + object:nil]; #if TARGET_IPHONE_SIMULATOR __weak RCTDevMenu *weakSelf = self; RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; - // Workaround around the first cmd+D not working: http://openradar.appspot.com/19613391 - // You can register just the cmd key and do nothing. This will trigger the bug and cmd+R - // will work like a charm! - [commands registerKeyCommandWithInput:@"" - modifierFlags:UIKeyModifierCommand - action:NULL]; - - // reload in debug mode + // toggle debug menu [commands registerKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { - __strong RCTDevMenu *strongSelf = weakSelf; - [strongSelf show]; + [weakSelf toggle]; + }]; + + // reload in normal mode + [commands registerKeyCommandWithInput:@"n" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + weakSelf.executorClass = Nil; }]; #endif @@ -98,11 +117,58 @@ RCT_EXPORT_MODULE() return self; } +- (void)updateSettings +{ + _settings = [NSMutableDictionary dictionaryWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]]; + + self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; + self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; + self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + self.executorClass = NSClassFromString(_settings[@"executorClass"]); +} + +- (void)jsLoaded +{ + _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]; + } + + // Hit these setters again after bridge has finished loading + self.profilingEnabled = _profilingEnabled; + self.liveReloadEnabled = _liveReloadEnabled; + self.executorClass = _executorClass; +} + - (void)dealloc { + [_updateTask cancel]; + [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (void)updateSetting:(NSString *)name value:(id)value +{ + if (value) { + _settings[name] = value; + } else { + [_settings removeObjectForKey:name]; + } + [_defaults setObject:_settings forKey:RCTDevMenuSettingsKey]; + [_defaults synchronize]; +} + - (void)showOnShake { if (_shakeToShow) { @@ -110,48 +176,73 @@ RCT_EXPORT_MODULE() } } -- (void)show +- (void)toggle { if (_actionSheet) { + [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; + _actionSheet = nil; + } else { + [self show]; + } +} + +RCT_EXPORT_METHOD(show) +{ + if (_actionSheet || !_bridge) { return; } - NSString *debugTitleChrome = _bridge.executorClass && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging"; - NSString *debugTitleSafari = _bridge.executorClass && _bridge.executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Enable Safari Debugging"; - NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; - NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling"; + NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome"; + NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari"; UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development" delegate:self - cancelButtonTitle:@"Cancel" + cancelButtonTitle:nil destructiveButtonTitle:nil - otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil]; + otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, nil]; + + 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; actionSheet.actionSheetStyle = UIBarStyleBlack; [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; _actionSheet = actionSheet; } +RCT_EXPORT_METHOD(reload) +{ + _jsLoaded = NO; + _liveReloadURL = nil; + [_bridge reload]; +} + - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { _actionSheet = nil; switch (buttonIndex) { case 0: { - [_bridge reload]; + [self reload]; break; } case 1: { Class cls = NSClassFromString(@"RCTWebSocketExecutor"); - _bridge.executorClass = (_bridge.executorClass != cls) ? cls : nil; - [_bridge reload]; + self.executorClass = (_executorClass == cls) ? Nil : cls; break; } case 2: { Class cls = NSClassFromString(@"RCTWebViewExecutor"); - _bridge.executorClass = (_bridge.executorClass != cls) ? cls : Nil; - [_bridge reload]; + self.executorClass = (_executorClass == cls) ? Nil : cls; break; } case 3: { @@ -167,89 +258,110 @@ RCT_EXPORT_MODULE() } } +- (void)setShakeToShow:(BOOL)shakeToShow +{ + if (_shakeToShow != shakeToShow) { + _shakeToShow = shakeToShow; + [self updateSetting:@"shakeToShow" value: @(_shakeToShow)]; + } +} + - (void)setProfilingEnabled:(BOOL)enabled { - if (_profilingEnabled == enabled) { - return; + if (_profilingEnabled != enabled) { + _profilingEnabled = enabled; + [self updateSetting:@"profilingEnabled" value: @(_profilingEnabled)]; } - _profilingEnabled = enabled; - if (RCTProfileIsProfiling()) { - [_bridge stopProfiling]; - } else { - [_bridge startProfiling]; + if (_liveReloadURL && enabled != RCTProfileIsProfiling()) { + if (enabled) { + [_bridge startProfiling]; + } else { + [_bridge stopProfiling]; + } } } - (void)setLiveReloadEnabled:(BOOL)enabled { - if (_liveReloadEnabled == enabled) { + if (_liveReloadEnabled != enabled) { + _liveReloadEnabled = enabled; + [self updateSetting:@"liveReloadEnabled" value: @(_liveReloadEnabled)]; + } + + if (_liveReloadEnabled) { + [self checkForUpdates]; + } else { + [_updateTask cancel]; + _updateTask = nil; + } +} + +- (void)setExecutorClass:(Class)executorClass +{ + if (_executorClass != executorClass) { + _executorClass = executorClass; + [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 != NSClassFromString(@"RCTWebSocketExecutor") && + _bridge.executorClass != NSClassFromString(@"RCTWebViewExecutor"))) { + return; + } + + _bridge.executorClass = executorClass; + [self reload]; + } +} + +- (void)checkForUpdates +{ + if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) { return; } - _liveReloadEnabled = enabled; - if (_liveReloadEnabled) { - - _updateTimer = [NSTimer scheduledTimerWithTimeInterval:_liveReloadPeriod - target:self - selector:@selector(pollForUpdates) - userInfo:nil - repeats:YES]; - } else { - - [_updateTimer invalidate]; - _updateTimer = nil; - } -} - -- (void)setLiveReloadPeriod:(NSTimeInterval)liveReloadPeriod -{ - _liveReloadPeriod = liveReloadPeriod; - if (_liveReloadEnabled) { - self.liveReloadEnabled = NO; - self.liveReloadEnabled = YES; - } -} - -- (void)pollForUpdates -{ - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - if (!sourceCodeModule) { - RCTLogError(@"RCTSourceCode module not found"); - self.liveReloadEnabled = NO; + if (_updateTask) { + [_updateTask cancel]; + _updateTask = nil; + return; } - NSURL *longPollURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL]; - [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:longPollURL] - queue:[[NSOperationQueue alloc] init] - completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { + __weak RCTDevMenu *weakSelf = self; + _updateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; - if (_liveReloadEnabled && HTTPResponse.statusCode == 205) { - [_bridge reload]; - } - }]; -} + 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]; + } + } + }); -- (BOOL)isValid -{ - return !_liveReloadEnabled || _updateTimer != nil; -} + }]; -- (void)invalidate -{ - [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; - [_updateTimer invalidate]; - _updateTimer = nil; + [_updateTask resume]; } @end -#else // Unvailable +#else // Unavailable when not in dev mode @implementation RCTDevMenu - (void)show {} +- (void)reload {} @end diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 9141dd31d..823acb241 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -90,6 +90,17 @@ static RCTKeyCommands *RKKeyCommandsSharedInstance = nil; { RCTAssertMainThread(); + if (input.length && flags) { + + // Workaround around the first cmd not working: http://openradar.appspot.com/19613391 + // You can register just the cmd key and do nothing. This ensures that + // command-key modified commands will work first time. + + [self registerKeyCommandWithInput:@"" + modifierFlags:flags + action:nil]; + } + UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input modifierFlags:flags action:@selector(RCT_handleKeyCommand:)]; diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index b54d18aa3..0de61d172 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -91,7 +91,7 @@ [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - [NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:nil]; + [[[NSURLSession sharedSession] dataTaskWithRequest:request] resume]; } - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index c415cb87a..42954d36e 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -582,6 +582,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", "RCT_DEBUG=1", "RCT_DEV=1", "RCT_NSASSERT=1",