From 08ec846176c57d8c3d7e941d1f4104401745b894 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Wed, 29 Apr 2015 20:20:15 -0700 Subject: [PATCH 01/51] [ReactNative] Fix logic in popToRoute --- Libraries/CustomComponents/Navigator/Navigator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 6d3d55c31..538c35538 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -1062,7 +1062,7 @@ var Navigator = React.createClass({ indexOfRoute !== -1, 'Calling pop to route for a route that doesn\'t exist!' ); - return this.state.routeStack.length - indexOfRoute - 1; + return this.state.presentedIndex - indexOfRoute; }, popToRoute: function(route) { From 648cdfeb1829496e73b2669f5ac199a3ac1742f0 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 29 Apr 2015 23:33:19 -0700 Subject: [PATCH 02/51] [react-native] Fix example pages requiring React directly --- Examples/UIExplorer/createExamplePage.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Examples/UIExplorer/createExamplePage.js b/Examples/UIExplorer/createExamplePage.js index ab725e844..4bc933eba 100644 --- a/Examples/UIExplorer/createExamplePage.js +++ b/Examples/UIExplorer/createExamplePage.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var ReactIOS = require('ReactIOS'); var UIExplorerBlock = require('./UIExplorerBlock'); var UIExplorerPage = require('./UIExplorerPage'); @@ -46,20 +47,28 @@ var createExamplePage = function(title: ?string, exampleModule: ExampleModule) getBlock: function(example, i) { // Hack warning: This is a hack because the www UI explorer requires // renderComponent to be called. - var originalRenderComponent = React.renderComponent; var originalRender = React.render; + var originalRenderComponent = React.renderComponent; + var originalIOSRender = ReactIOS.render; + var originalIOSRenderComponent = ReactIOS.renderComponent; var renderedComponent; // TODO remove typecasts when Flow bug #6560135 is fixed // and workaround is removed from react-native.js - (React: Object).render = (React: Object).renderComponent = function(element, container) { - renderedComponent = element; - }; + (React: Object).render = + (React: Object).renderComponent = + (ReactIOS: Object).render = + (ReactIOS: Object).renderComponent = + function(element, container) { + renderedComponent = element; + }; var result = example.render(null); if (result) { renderedComponent = result; } - (React: Object).renderComponent = originalRenderComponent; (React: Object).render = originalRender; + (React: Object).renderComponent = originalRenderComponent; + (ReactIOS: Object).render = originalIOSRender; + (ReactIOS: Object).renderComponent = originalIOSRenderComponent; return ( Date: Thu, 30 Apr 2015 02:45:00 -0700 Subject: [PATCH 03/51] [react-native] Fix Chrome debugging Summary: Requiring ExceptionsManager in renderApplication (added in D2023119) led to a transitive require of ExecutionEnvironment, which has to run after InitializeJavaScriptAppEngine. InitializeJavaScriptAppEngine is the right place for this sort of logic because we control the order that things are loaded, so move the console.error hook initialization there. @public Test Plan: Loaded shell app in simulator with Chrome debugging with no errors. --- .../Initialization/InitializeJavaScriptAppEngine.js | 7 +++++++ Libraries/ReactIOS/renderApplication.ios.js | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 51f6809cc..73493aaf6 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -79,6 +79,12 @@ function setupRedBoxErrorHandler() { ErrorUtils.setGlobalHandler(handleErrorWithRedBox); } +function setupRedBoxConsoleErrorHandler() { + // ExceptionsManager transitively requires Promise so we install it after + var ExceptionsManager = require('ExceptionsManager'); + ExceptionsManager.installConsoleErrorReporter(); +} + /** * Sets up a set of window environment wrappers that ensure that the * BatchedBridge is flushed after each tick. In both the case of the @@ -139,4 +145,5 @@ setupTimers(); setupAlert(); setupPromise(); setupXHR(); +setupRedBoxConsoleErrorHandler(); setupGeolocation(); diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js index 39e5720f5..16052c6fa 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.ios.js @@ -11,8 +11,6 @@ */ 'use strict'; -require('ExceptionsManager').installConsoleErrorReporter(); - var React = require('React'); var StyleSheet = require('StyleSheet'); var View = require('View'); From d82aaa7fb3a69c1ee736359ac53f3b8fd583b78b Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Thu, 30 Apr 2015 08:03:04 -0700 Subject: [PATCH 04/51] [React Native] Remove RKCustomTabBarController Summary: @public Closes GitHub issue #1064 Test Plan: @nicklockwood approves. --- React/Views/RCTTabBar.m | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 11ad47e32..8a7fb4a43 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -18,24 +18,6 @@ #import "RCTWrapperViewController.h" #import "UIView+React.h" -@interface RKCustomTabBarController : UITabBarController - -@end - -@implementation RKCustomTabBarController - -@synthesize currentTopLayoutGuide = _currentTopLayoutGuide; -@synthesize currentBottomLayoutGuide = _currentBottomLayoutGuide; - -- (void)viewWillLayoutSubviews -{ - [super viewWillLayoutSubviews]; - _currentTopLayoutGuide = self.topLayoutGuide; - _currentBottomLayoutGuide = self.bottomLayoutGuide; -} - -@end - @interface RCTTabBar() @end @@ -53,7 +35,7 @@ if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = eventDispatcher; _tabViews = [[NSMutableArray alloc] init]; - _tabController = [[RKCustomTabBarController alloc] init]; + _tabController = [[UITabBarController alloc] init]; _tabController.delegate = self; [self addSubview:_tabController.view]; } From eb0476074f0b0a2c8d47f858946792046e5f94e9 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 30 Apr 2015 09:50:22 -0700 Subject: [PATCH 05/51] Improved debug and fixed macros --- .../UIExplorer.xcodeproj/project.pbxproj | 1 + .../xcschemes/UIExplorer.xcscheme | 2 +- .../project.pbxproj | 7 ++-- Libraries/RCTTest/RCTTestRunner.m | 10 ----- React/Base/RCTBridge.m | 33 +--------------- React/Base/RCTDevMenu.h | 6 +-- React/Base/RCTDevMenu.m | 39 ++++++++++++++++++- React/Base/RCTRedBox.h | 6 --- React/Base/RCTRedBox.m | 22 +++++++++-- React/Modules/RCTExceptionsManager.m | 10 ++--- React/React.xcodeproj/project.pbxproj | 12 +++--- 11 files changed, 74 insertions(+), 74 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 348d04f0d..cf9440c05 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -632,6 +632,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index b231b77ee..488c0077d 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -1,6 +1,6 @@ 0 && ![testModule isDone] && error == nil) { @@ -98,13 +95,6 @@ } else { RCTAssert([testModule isDone], @"Test didn't finish within %d seconds", TIMEOUT_SECONDS); } - -#else - - expectErrorBlock(@"RCTRedBox unavailable. Set RCT_DEBUG=1 for testing."); - -#endif - } @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 5b6a92682..5dd3bac5a 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -934,8 +934,6 @@ static id _latestJSExecutor; _loading = NO; if (error != nil) { -#if RCT_DEBUG // Red box is only available in debug mode - NSArray *stack = [[error userInfo] objectForKey:@"stack"]; if (stack) { [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] @@ -945,8 +943,6 @@ static id _latestJSExecutor; withDetails:[error localizedFailureReason]]; } -#endif - } else { [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification @@ -980,33 +976,6 @@ static id _latestJSExecutor; action:^(UIKeyCommand *command) { [weakSelf reload]; }]; - // reset to normal mode - [commands registerKeyCommandWithInput:@"n" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - __strong RCTBridge *strongSelf = weakSelf; - strongSelf.executorClass = Nil; - [strongSelf reload]; - }]; - -#if RCT_DEV // Debug executors are only available in dev mode - - // reload in debug mode - [commands registerKeyCommandWithInput:@"d" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - __strong RCTBridge *strongSelf = weakSelf; - strongSelf.executorClass = NSClassFromString(@"RCTWebSocketExecutor"); - if (!strongSelf.executorClass) { - strongSelf.executorClass = NSClassFromString(@"RCTWebViewExecutor"); - } - if (!strongSelf.executorClass) { - RCTLogError(@"WebSocket debugger is not available. " - "Did you forget to include RCTWebSocketExecutor?"); - } - [strongSelf reload]; - }]; -#endif #endif } @@ -1091,7 +1060,7 @@ static id _latestJSExecutor; [self _invokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, args ?: @[]] + arguments:@[moduleID ?: @0, methodID ?: @0, args ?: @[]] context:RCTGetExecutorID(_javaScriptExecutor)]; } diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index 8057e5708..537675576 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -16,10 +16,10 @@ /** * Developer menu, useful for exposing extra functionality when debugging. */ -@interface RCTDevMenu : NSObject +@interface RCTDevMenu : NSObject /** - * Is the menu enabled. The menu is enabled by default in debug mode, but + * Is the menu enabled. The menu is enabled by default if RCT_DEV=1, but * you may wish to disable it so that you can provide your own shake handler. */ @property (nonatomic, assign) BOOL shakeToShow; @@ -41,7 +41,7 @@ @property (nonatomic, assign) NSTimeInterval liveReloadPeriod; /** - * Manually show the menu. This will. + * Manually show the dev menu. */ - (void)show; diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 4af9d4e62..529840e0a 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -10,12 +10,16 @@ #import "RCTDevMenu.h" #import "RCTBridge.h" +#import "RCTDefines.h" +#import "RCTKeyCommands.h" #import "RCTLog.h" #import "RCTProfile.h" #import "RCTRootView.h" #import "RCTSourceCode.h" #import "RCTUtils.h" +#if RCT_DEV + @interface RCTBridge (Profiling) - (void)startProfiling; @@ -36,7 +40,7 @@ static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification @end -@interface RCTDevMenu () +@interface RCTDevMenu () @end @@ -68,6 +72,28 @@ RCT_EXPORT_MODULE() selector:@selector(showOnShake) name:RCTShowDevMenuNotification 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 + [commands registerKeyCommandWithInput:@"d" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + __strong RCTDevMenu *strongSelf = weakSelf; + [strongSelf show]; + }]; +#endif + } return self; } @@ -104,6 +130,7 @@ RCT_EXPORT_MODULE() actionSheet.actionSheetStyle = UIBarStyleBlack; [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; + _actionSheet = actionSheet; } - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex @@ -218,6 +245,16 @@ RCT_EXPORT_MODULE() @end +#else // Unvailable + +@implementation RCTDevMenu + +- (void)show {} + +@end + +#endif + @implementation RCTBridge (RCTDevMenu) - (RCTDevMenu *)devMenu diff --git a/React/Base/RCTRedBox.h b/React/Base/RCTRedBox.h index 058759da7..9a3a9b49a 100644 --- a/React/Base/RCTRedBox.h +++ b/React/Base/RCTRedBox.h @@ -7,10 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTDefines.h" - -#if RCT_DEBUG // Red box is only available in debug mode - #import @interface RCTRedBox : NSObject @@ -27,5 +23,3 @@ - (void)dismiss; @end - -#endif diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index 626840252..b54d18aa3 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -7,15 +7,14 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTDefines.h" - -#if RCT_DEBUG // Red box is only available in debug mode - #import "RCTRedBox.h" #import "RCTBridge.h" +#import "RCTDefines.h" #import "RCTUtils.h" +#if RCT_DEBUG + @interface RCTRedBoxWindow : UIWindow @property (nonatomic, copy) NSString *lastErrorMessage; @@ -310,4 +309,19 @@ @end +#else // Disabled + +@implementation RCTRedBox + ++ (instancetype)sharedInstance { return nil; } +- (void)showErrorMessage:(NSString *)message {} +- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {} +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack {} +- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack {} +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow {} +- (NSString *)currentErrorMessage { return nil; } +- (void)dismiss {} + +@end + #endif diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index 878138282..f391e6af0 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -61,12 +61,8 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message } else { - // Filter out numbers so the same base errors are mapped to the same categories independent of incorrect values. - NSString *pattern = @"[+-]?\\d+[,.]?\\d*"; - NSString *sanitizedMessage = [message stringByReplacingOccurrencesOfString:pattern withString:@"" options:NSRegularExpressionSearch range:(NSRange){0, message.length}]; - - if (sanitizedMessage.length > maxMessageLength) { - sanitizedMessage = [[sanitizedMessage substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; + if (message.length > maxMessageLength) { + message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; } NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"]; @@ -74,7 +70,7 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; } - NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:sanitizedMessage]; + NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message]; [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; } diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 10867e9cb..c415cb87a 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -582,8 +582,9 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", + "RCT_DEBUG=1", + "RCT_DEV=1", + "RCT_NSASSERT=1", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -621,6 +622,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREPROCESSOR_DEFINITIONS = ""; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -638,10 +640,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -658,6 +657,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", From b6646d1c4cba256713a2ec38a112a9a50e9ad633 Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Thu, 30 Apr 2015 12:21:06 -0700 Subject: [PATCH 06/51] [ReactNative] Honor fontWeight once again --- React/Base/RCTConvert.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 465d1f0bb..5802a80f6 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -733,11 +733,6 @@ static BOOL RCTFontIsCondensed(UIFont *font) isItalic = [self RCTFontStyle:style]; } - // Get font weight - if (weight) { - fontWeight = [self RCTFontWeight:weight]; - } - // Gracefully handle being given a font name rather than font family, for // example: "Helvetica Light Oblique" rather than just "Helvetica". if ([UIFont fontNamesForFamilyName:familyName].count == 0) { @@ -756,6 +751,11 @@ static BOOL RCTFontIsCondensed(UIFont *font) } } + // Get font weight + if (weight) { + fontWeight = [self RCTFontWeight:weight]; + } + // Get the closest font that matches the given weight for the fontFamily UIFont *bestMatch = [UIFont fontWithName:font.fontName size: fontSize]; CGFloat closestWeight; From 8fe6626d5f809369f219401ff2f5ee210fc17c71 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Thu, 30 Apr 2015 11:33:40 -0700 Subject: [PATCH 07/51] [react_native] JS files from D2028144: [react_native] Expose android version to JS --- .../JavaScriptAppEngine/Initialization/ExceptionsManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index b5868139c..2677a0029 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -11,7 +11,6 @@ */ 'use strict'; -var Platform = require('Platform'); var RCTExceptionsManager = require('NativeModules').ExceptionsManager; var loadSourceMap = require('loadSourceMap'); From 4f70e58b37d51529712e479fd8ad6f400497ce3b Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 30 Apr 2015 10:45:10 -0700 Subject: [PATCH 08/51] [react-native] Only intercept console.error on iOS --- .../Initialization/InitializeJavaScriptAppEngine.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 73493aaf6..1b67c4940 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -82,7 +82,11 @@ function setupRedBoxErrorHandler() { function setupRedBoxConsoleErrorHandler() { // ExceptionsManager transitively requires Promise so we install it after var ExceptionsManager = require('ExceptionsManager'); - ExceptionsManager.installConsoleErrorReporter(); + var Platform = require('Platform'); + // TODO (#6925182): Enable console.error redbox on Android + if (Platform.OS === 'ios') { + ExceptionsManager.installConsoleErrorReporter(); + } } /** From 63ab6e82811c7bbc67b61da23b9e1310b4182840 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 30 Apr 2015 13:18:52 -0700 Subject: [PATCH 09/51] [react-native] Remove iOS-specific attributes from ART text --- Libraries/ART/RCTConvert+ART.h | 1 - Libraries/ART/RCTConvert+ART.m | 15 ++------------- Libraries/ART/ReactIOSART.js | 28 +--------------------------- 3 files changed, 3 insertions(+), 41 deletions(-) diff --git a/Libraries/ART/RCTConvert+ART.h b/Libraries/ART/RCTConvert+ART.h index 24944fb12..3cbd0e787 100644 --- a/Libraries/ART/RCTConvert+ART.h +++ b/Libraries/ART/RCTConvert+ART.h @@ -17,7 +17,6 @@ @interface RCTConvert (ART) + (CGPathRef)CGPath:(id)json; -+ (CTFontRef)CTFont:(id)json; + (CTTextAlignment)CTTextAlignment:(id)json; + (ARTTextFrame)ARTTextFrame:(id)json; + (ARTCGFloatArray)ARTCGFloatArray:(id)json; diff --git a/Libraries/ART/RCTConvert+ART.m b/Libraries/ART/RCTConvert+ART.m index 4cd11bd3f..7a607a12c 100644 --- a/Libraries/ART/RCTConvert+ART.m +++ b/Libraries/ART/RCTConvert+ART.m @@ -64,18 +64,6 @@ return (CGPathRef)CFAutorelease(path); } -+ (CTFontRef)CTFont:(id)json -{ - NSDictionary *dict = [self NSDictionary:json]; - if (!dict) { - return nil; - } - CTFontDescriptorRef fontDescriptor = CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)dict); - CTFontRef font = CTFontCreateWithFontDescriptor(fontDescriptor, 0.0, NULL); - CFRelease(fontDescriptor); - return (CTFontRef)CFAutorelease(font); -} - RCT_ENUM_CONVERTER(CTTextAlignment, (@{ @"auto": @(kCTTextAlignmentNatural), @"left": @(kCTTextAlignmentLeft), @@ -98,7 +86,8 @@ RCT_ENUM_CONVERTER(CTTextAlignment, (@{ return frame; } - CTFontRef font = [self CTFont:dict[@"font"]]; + NSDictionary *fontDict = dict[@"font"]; + CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"]]; if (!font) { return frame; } diff --git a/Libraries/ART/ReactIOSART.js b/Libraries/ART/ReactIOSART.js index 9ef2f8843..7eb018471 100644 --- a/Libraries/ART/ReactIOSART.js +++ b/Libraries/ART/ReactIOSART.js @@ -50,18 +50,11 @@ function fontAndLinesDiffer(a, b) { return true; } - var aTraits = a.font.NSCTFontTraitsAttribute; - var bTraits = b.font.NSCTFontTraitsAttribute; - if ( a.font.fontFamily !== b.font.fontFamily || a.font.fontSize !== b.font.fontSize || a.font.fontWeight !== b.font.fontWeight || - a.font.fontStyle !== b.font.fontStyle || - // TODO(6364240): remove iOS-specific attrs - a.font.NSFontFamilyAttribute !== b.font.NSFontFamilyAttribute || - a.font.NSFontSizeAttribute !== b.font.NSFontSizeAttribute || - aTraits.NSCTFontSymbolicTrait !== bTraits.NSCTFontSymbolicTrait + a.font.fontStyle !== b.font.fontStyle ) { return true; } @@ -418,14 +411,6 @@ var Shape = React.createClass({ var cachedFontObjectsFromString = {}; -function extractFontTraits(isBold, isItalic) { - var italic = isItalic ? 1 : 0; - var bold = isBold ? 2 : 0; - return { - NSCTFontSymbolicTrait: italic | bold - }; -} - var fontFamilyPrefix = /^[\s"']*/; var fontFamilySuffix = /[\s"']*$/; @@ -456,10 +441,6 @@ function parseFontString(font) { fontSize: fontSize, fontWeight: isBold ? 'bold' : 'normal', fontStyle: isItalic ? 'italic' : 'normal', - // TODO(6364240): remove iOS-specific attrs - NSFontFamilyAttribute: fontFamily, - NSFontSizeAttribute: fontSize, - NSCTFontTraitsAttribute: extractFontTraits(isBold, isItalic) }; return cachedFontObjectsFromString[font]; } @@ -479,13 +460,6 @@ function extractFont(font) { fontSize: fontSize, fontWeight: font.fontWeight, fontStyle: font.fontStyle, - // TODO(6364240): remove iOS-specific attrs - NSFontFamilyAttribute: fontFamily, - NSFontSizeAttribute: fontSize, - NSCTFontTraitsAttribute: extractFontTraits( - font.fontWeight === 'bold', - font.fontStyle === 'italic' - ) }; } From 824da1badaceccfb3009af79343422670e4f3ac0 Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Thu, 30 Apr 2015 14:09:55 -0700 Subject: [PATCH 10/51] [ReactNative] pass in launchOptions to relevant bridged modules --- .../PushNotificationIOS/RCTPushNotificationManager.h | 2 -- .../PushNotificationIOS/RCTPushNotificationManager.m | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index b60a646e4..ef1ba1496 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -13,8 +13,6 @@ @interface RCTPushNotificationManager : NSObject -- (instancetype)initWithInitialNotification:(NSDictionary *)initialNotification NS_DESIGNATED_INITIALIZER; - + (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; + (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index c0bedee5e..4846c885e 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -24,14 +24,8 @@ RCT_EXPORT_MODULE() @synthesize bridge = _bridge; - (instancetype)init -{ - return [self initWithInitialNotification:nil]; -} - -- (instancetype)initWithInitialNotification:(NSDictionary *)initialNotification { if ((self = [super init])) { - _initialNotification = [initialNotification copy]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRemoteNotificationReceived:) name:RCTRemoteNotificationReceived @@ -45,6 +39,12 @@ RCT_EXPORT_MODULE() [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + _initialNotification = [bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]; +} + + (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { if ([application respondsToSelector:@selector(registerForRemoteNotifications)]) { From e5f47731c6b094fa1776eb313fd39bbc2ccd6463 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Thu, 30 Apr 2015 14:18:00 -0700 Subject: [PATCH 11/51] [ReactNative] Only report console.error()s as exceptions in dev mode --- .../Initialization/InitializeJavaScriptAppEngine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 1b67c4940..bb0aa263c 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -84,7 +84,7 @@ function setupRedBoxConsoleErrorHandler() { var ExceptionsManager = require('ExceptionsManager'); var Platform = require('Platform'); // TODO (#6925182): Enable console.error redbox on Android - if (Platform.OS === 'ios') { + if (__DEV__ && Platform.OS === 'ios') { ExceptionsManager.installConsoleErrorReporter(); } } From 76dc14684d9a3cab0e8efcac6f61e9b6b96278a1 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Thu, 30 Apr 2015 15:53:27 -0700 Subject: [PATCH 12/51] [ReactNative] Navigator block touches to non-active scenes Summary: When tapping a link quickly, it will cause two scenes to be pushed on. This prevents against that case by swallowing touches for all non-active scenes. @public Test Plan: Can no longer double-push scenes --- .../CustomComponents/Navigator/Navigator.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 538c35538..86d068f9f 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -1176,12 +1176,18 @@ var Navigator = React.createClass({ var scene = shouldRenderScene ? this._renderScene(route, i, sceneNavigatorContext) : null; return ( - - {scene} - + ( + i !== this.state.presentedIndex + )}> + + {scene} + + ); }, From 6cb717809879854fc9c31c627af6444fedf48889 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 30 Apr 2015 17:23:46 -0700 Subject: [PATCH 13/51] [ReactNative] Suggest un-pausing debugger when there are issues Summary: @public The most common problem I've noticed is that the debugger is paused, so we shouldn't ask the user if Chrome is open, because it usually is. Test Plan: Try to reload with debugger paused, see new error message. --- Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m index 753c8d76d..16a378ccf 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -66,7 +66,8 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); retries--; } if (!runtimeIsReady) { - RCTLogError(@"Runtime is not ready. Do you have Chrome open?"); + RCTLogError(@"Runtime is not ready. Make sure Chrome is running and not " + "paused on a breakpoint or exception and try reloading again."); [self invalidate]; return nil; } From 67196b36bb3a4d880c4397ec48c84f1eb8d500c0 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 1 May 2015 06:11:24 -0700 Subject: [PATCH 14/51] [ReactNative] Fix WebView executor --- React/Executors/RCTWebViewExecutor.m | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/React/Executors/RCTWebViewExecutor.m b/React/Executors/RCTWebViewExecutor.m index 09628850f..a180bae2c 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -42,13 +42,17 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) { UIWebView *_webView; NSMutableDictionary *_objectsToInject; + NSRegularExpression *_commentsRegex; } +@synthesize valid = _valid; + - (instancetype)initWithWebView:(UIWebView *)webView { if ((self = [super init])) { _objectsToInject = [[NSMutableDictionary alloc] init]; _webView = webView ?: [[UIWebView alloc] init]; + _commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]+?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL]; _webView.delegate = self; } return self; @@ -59,13 +63,9 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) return [self initWithWebView:nil]; } -- (BOOL)isValid -{ - return _webView != nil; -} - - (void)invalidate { + _valid = NO; _webView.delegate = nil; _webView = nil; } @@ -129,10 +129,21 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) } RCTAssert(onComplete != nil, @""); - _onApplicationScriptLoaded = onComplete; + __weak RCTWebViewExecutor *weakSelf = self; + _onApplicationScriptLoaded = ^(NSError *error){ + RCTWebViewExecutor *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + strongSelf->_valid = error == nil; + onComplete(error); + }; + + script = [_commentsRegex stringByReplacingMatchesInString:script + options:0 + range:NSMakeRange(0, script.length) + withTemplate:@""]; - script = [script stringByReplacingOccurrencesOfString:@"" withString:@""]; if (_objectsToInject.count > 0) { NSMutableString *scriptWithInjections = [[NSMutableString alloc] initWithString:@"/* BEGIN NATIVELY INJECTED OBJECTS */\n"]; [_objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *blockScript, BOOL *stop) { From ba501a1bf50bb0ed9e16637e68965ef13cbcc6b3 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 1 May 2015 06:21:03 -0700 Subject: [PATCH 15/51] 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", From 4bd56ca6d1c0fdfe7c0ac89ad184aa4f13de8ed4 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Fri, 1 May 2015 06:54:40 -0700 Subject: [PATCH 16/51] [react_native] JS files from D2038898: Move view specific constants out of UIManager to the cooresponding view manager class. --- Libraries/Components/ScrollView/ScrollView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index e66612b22..76419bce0 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -364,7 +364,7 @@ var validAttributes = { if (Platform.OS === 'android') { var AndroidScrollView = createReactIOSNativeComponentClass({ validAttributes: validAttributes, - uiViewClassName: 'AndroidScrollView', + uiViewClassName: 'RCTScrollView', }); var AndroidHorizontalScrollView = createReactIOSNativeComponentClass({ validAttributes: validAttributes, From 6abf37b47ee35aedfa7838b3eba98342eabc5cf3 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Fri, 1 May 2015 14:17:02 -0700 Subject: [PATCH 17/51] [ReactNative] Navigator.Interceptor handler arg fix --- Libraries/CustomComponents/Navigator/NavigatorInterceptor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js index dcc5d43ef..e70820435 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js +++ b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js @@ -78,9 +78,9 @@ var NavigatorInterceptor = React.createClass({ } switch (action) { case 'pop': - return this.props.onPopRequest && this.props.onPopRequest(action, arg1, arg2); + return this.props.onPopRequest && this.props.onPopRequest(arg1, arg2); case 'push': - return this.props.onPushRequest && this.props.onPushRequest(action, arg1, arg2); + return this.props.onPushRequest && this.props.onPushRequest(arg1, arg2); default: return false; } From 5453be2ab239f6e5ccb4aa61c9bf0223a107221e Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Fri, 1 May 2015 16:19:27 -0700 Subject: [PATCH 18/51] [ReactNative] Fix touch issue caused by D2036644 after swiping back --- .../CustomComponents/Navigator/Navigator.js | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 86d068f9f..280d8c4dd 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -1176,18 +1176,12 @@ var Navigator = React.createClass({ var scene = shouldRenderScene ? this._renderScene(route, i, sceneNavigatorContext) : null; return ( - ( - i !== this.state.presentedIndex - )}> - - {scene} - - + + {scene} + ); }, @@ -1202,6 +1196,9 @@ var Navigator = React.createClass({ { + return i !== this.state.presentedIndex; + }} style={[initialSceneStyle, this.props.sceneStyle]}> {React.cloneElement(child, { ref: this._handleItemRef.bind(null, this.state.idStack[i]), From 48b281de76170a63d046e21f9180fbae3a9c9eea Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Fri, 1 May 2015 16:30:47 -0700 Subject: [PATCH 19/51] [ReactNative] Remove padding restriction on images Summary: @public Padding actually works fine on images. Test Plan: Nested text inside an image is properly positioned with padding on the image. --- Libraries/Image/ImageStylePropTypes.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Libraries/Image/ImageStylePropTypes.js b/Libraries/Image/ImageStylePropTypes.js index 4e361d9de..d46807ce7 100644 --- a/Libraries/Image/ImageStylePropTypes.js +++ b/Libraries/Image/ImageStylePropTypes.js @@ -29,19 +29,4 @@ var ImageStylePropTypes = { opacity: ReactPropTypes.number, }; -// Image doesn't support padding correctly (#4841912) -var unsupportedProps = Object.keys({ - padding: null, - paddingTop: null, - paddingLeft: null, - paddingRight: null, - paddingBottom: null, - paddingVertical: null, - paddingHorizontal: null, -}); - -for (var i = 0; i < unsupportedProps.length; i++) { - delete ImageStylePropTypes[unsupportedProps[i]]; -} - module.exports = ImageStylePropTypes; From 43e038887d720a11382f8818a52277553af24f21 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Fri, 1 May 2015 17:05:24 -0700 Subject: [PATCH 20/51] [react-packager] Combine source maps coming from transformer Summary: @public Fixes [#393](https://github.com/facebook/react-native/issues/393). Currently the transformer assumes line-preserving compilers and defaults to a super-fast source map generation process. However, we need to support compilers that aren't preserving lines. I had feared this wuold slow down the server but I came about a little known peace of the spec that defines an "indexed source map" just for the purpose of concating files: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit Test Plan: 1. runJestTests.sh 2. run server and click around example apps 3. add a custom transporter like babel 4. add a custom file and a debugger statement 5. debug in chrome and make sure it works redbox still works --- .../__tests__/Transformer-test.js | 4 +- .../react-packager/src/JSTransformer/index.js | 8 +- .../react-packager/src/Packager/Package.js | 112 ++++++++++++--- .../src/Packager/__tests__/Package-test.js | 132 ++++++++++++++++-- .../src/Packager/__tests__/Packager-test.js | 56 ++++---- packager/react-packager/src/Packager/index.js | 37 ++--- .../react-packager/src/lib/ModuleTransport.js | 38 +++++ 7 files changed, 308 insertions(+), 79 deletions(-) create mode 100644 packager/react-packager/src/lib/ModuleTransport.js diff --git a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js index 72845a2e1..eb307a02e 100644 --- a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js +++ b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js @@ -11,6 +11,7 @@ jest .dontMock('worker-farm') .dontMock('os') + .dontMock('../../lib/ModuleTransport') .dontMock('../index'); var OPTIONS = { @@ -37,7 +38,7 @@ describe('Transformer', function() { pit('should loadFileAndTransform', function() { workers.mockImpl(function(data, callback) { - callback(null, { code: 'transformed' }); + callback(null, { code: 'transformed', map: 'sourceMap' }); }); require('fs').readFile.mockImpl(function(file, callback) { callback(null, 'content'); @@ -47,6 +48,7 @@ describe('Transformer', function() { .then(function(data) { expect(data).toEqual({ code: 'transformed', + map: 'sourceMap', sourcePath: 'file', sourceCode: 'content' }); diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index 962eb7fe9..33e017037 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -14,6 +14,7 @@ var Cache = require('./Cache'); var workerFarm = require('worker-farm'); var declareOpts = require('../lib/declareOpts'); var util = require('util'); +var ModuleTransport = require('../lib/ModuleTransport'); var readFile = Promise.promisify(fs.readFile); @@ -100,11 +101,12 @@ Transformer.prototype.loadFileAndTransform = function(filePath) { throw formatError(res.error, filePath, sourceCode); } - return { + return new ModuleTransport({ code: res.code, + map: res.map, sourcePath: filePath, - sourceCode: sourceCode - }; + sourceCode: sourceCode, + }); } ); }); diff --git a/packager/react-packager/src/Packager/Package.js b/packager/react-packager/src/Packager/Package.js index 67e31e47e..c621f7470 100644 --- a/packager/react-packager/src/Packager/Package.js +++ b/packager/react-packager/src/Packager/Package.js @@ -11,6 +11,7 @@ var _ = require('underscore'); var base64VLQ = require('./base64-vlq'); var UglifyJS = require('uglify-js'); +var ModuleTransport = require('../lib/ModuleTransport'); module.exports = Package; @@ -19,22 +20,25 @@ function Package(sourceMapUrl) { this._modules = []; this._assets = []; this._sourceMapUrl = sourceMapUrl; + this._shouldCombineSourceMaps = false; } Package.prototype.setMainModuleId = function(moduleId) { this._mainModuleId = moduleId; }; -Package.prototype.addModule = function( - transformedCode, - sourceCode, - sourcePath -) { - this._modules.push({ - transformedCode: transformedCode, - sourceCode: sourceCode, - sourcePath: sourcePath - }); +Package.prototype.addModule = function(module) { + if (!(module instanceof ModuleTransport)) { + throw new Error('Expeceted a ModuleTransport object'); + } + + // If we get a map from the transformer we'll switch to a mode + // were we're combining the source maps as opposed to + if (!this._shouldCombineSourceMaps && module.map != null) { + this._shouldCombineSourceMaps = true; + } + + this._modules.push(module); }; Package.prototype.addAsset = function(asset) { @@ -45,11 +49,12 @@ Package.prototype.finalize = function(options) { options = options || {}; if (options.runMainModule) { var runCode = ';require("' + this._mainModuleId + '");'; - this.addModule( - runCode, - runCode, - 'RunMainModule.js' - ); + this.addModule(new ModuleTransport({ + code: runCode, + virtual: true, + sourceCode: runCode, + sourcePath: 'RunMainModule.js' + })); } Object.freeze(this._modules); @@ -67,7 +72,7 @@ Package.prototype._assertFinalized = function() { Package.prototype._getSource = function() { if (this._source == null) { - this._source = _.pluck(this._modules, 'transformedCode').join('\n'); + this._source = _.pluck(this._modules, 'code').join('\n'); } return this._source; }; @@ -136,10 +141,50 @@ Package.prototype.getMinifiedSourceAndMap = function() { } }; +/** + * I found a neat trick in the sourcemap spec that makes it easy + * to concat sourcemaps. The `sections` field allows us to combine + * the sourcemap easily by adding an offset. Tested on chrome. + * Seems like it's not yet in Firefox but that should be fine for + * now. + */ +Package.prototype._getCombinedSourceMaps = function(options) { + var result = { + version: 3, + file: 'bundle.js', + sections: [], + }; + + var line = 0; + this._modules.forEach(function(module) { + var map = module.map; + if (module.virtual) { + map = generateSourceMapForVirtualModule(module); + } + + if (options.excludeSource) { + map = _.extend({}, map, {sourcesContent: []}); + } + + result.sections.push({ + offset: { line: line, column: 0 }, + map: map, + }); + line += module.code.split('\n').length; + }); + + return result; +}; + Package.prototype.getSourceMap = function(options) { this._assertFinalized(); options = options || {}; + + if (this._shouldCombineSourceMaps) { + return this._getCombinedSourceMaps(options); + } + var mappings = this._getMappings(); var map = { file: 'bundle.js', @@ -168,13 +213,14 @@ Package.prototype._getMappings = function() { // except for the lineno mappinp: curLineno - prevLineno = 1; Which is C. var line = 'AACA'; + var moduleLines = Object.create(null); var mappings = ''; for (var i = 0; i < modules.length; i++) { var module = modules[i]; - var transformedCode = module.transformedCode; + var code = module.code; var lastCharNewLine = false; - module.lines = 0; - for (var t = 0; t < transformedCode.length; t++) { + moduleLines[module.sourcePath] = 0; + for (var t = 0; t < code.length; t++) { if (t === 0 && i === 0) { mappings += firstLine; } else if (t === 0) { @@ -183,13 +229,15 @@ Package.prototype._getMappings = function() { // This is the only place were we actually don't know the mapping ahead // of time. When it's a new module (and not the first) the lineno // mapping is 0 (current) - number of lines in prev module. - mappings += base64VLQ.encode(0 - modules[i - 1].lines); + mappings += base64VLQ.encode( + 0 - moduleLines[modules[i - 1].sourcePath] + ); mappings += 'A'; } else if (lastCharNewLine) { - module.lines++; + moduleLines[module.sourcePath]++; mappings += line; } - lastCharNewLine = transformedCode[t] === '\n'; + lastCharNewLine = code[t] === '\n'; if (lastCharNewLine) { mappings += ';'; } @@ -218,7 +266,25 @@ Package.prototype.getDebugInfo = function() { this._modules.map(function(m) { return '

Path:

' + m.sourcePath + '

Source:

' + '
'; + _.escape(m.code) + ''; }).join('\n'), ].join('\n'); }; + +function generateSourceMapForVirtualModule(module) { + // All lines map 1-to-1 + var mappings = 'AAAA;'; + + for (var i = 1; i < module.code.split('\n').length; i++) { + mappings += 'AACA;'; + } + + return { + version: 3, + sources: [ module.sourcePath ], + names: [], + mappings: mappings, + file: module.sourcePath, + sourcesContent: [ module.code ], + }; +} diff --git a/packager/react-packager/src/Packager/__tests__/Package-test.js b/packager/react-packager/src/Packager/__tests__/Package-test.js index db596a7bc..0aaa3971c 100644 --- a/packager/react-packager/src/Packager/__tests__/Package-test.js +++ b/packager/react-packager/src/Packager/__tests__/Package-test.js @@ -13,11 +13,13 @@ jest.autoMockOff(); var SourceMapGenerator = require('source-map').SourceMapGenerator; describe('Package', function() { + var ModuleTransport; var Package; var ppackage; beforeEach(function() { Package = require('../Package'); + ModuleTransport = require('../../lib/ModuleTransport'); ppackage = new Package('test_url'); ppackage.getSourceMap = jest.genMockFn().mockImpl(function() { return 'test-source-map'; @@ -26,8 +28,17 @@ describe('Package', function() { describe('source package', function() { it('should create a package and get the source', function() { - ppackage.addModule('transformed foo;', 'source foo', 'foo path'); - ppackage.addModule('transformed bar;', 'source bar', 'bar path'); + ppackage.addModule(new ModuleTransport({ + code: 'transformed foo;', + sourceCode: 'source foo', + sourcePath: 'foo path', + })); + ppackage.addModule(new ModuleTransport({ + code: 'transformed bar;', + sourceCode: 'source bar', + sourcePath: 'bar path', + })); + ppackage.finalize({}); expect(ppackage.getSource()).toBe([ 'transformed foo;', @@ -37,8 +48,18 @@ describe('Package', function() { }); it('should create a package and add run module code', function() { - ppackage.addModule('transformed foo;', 'source foo', 'foo path'); - ppackage.addModule('transformed bar;', 'source bar', 'bar path'); + ppackage.addModule(new ModuleTransport({ + code: 'transformed foo;', + sourceCode: 'source foo', + sourcePath: 'foo path' + })); + + ppackage.addModule(new ModuleTransport({ + code: 'transformed bar;', + sourceCode: 'source bar', + sourcePath: 'bar path' + })); + ppackage.setMainModuleId('foo'); ppackage.finalize({runMainModule: true}); expect(ppackage.getSource()).toBe([ @@ -59,7 +80,11 @@ describe('Package', function() { return minified; }; - ppackage.addModule('transformed foo;', 'source foo', 'foo path'); + ppackage.addModule(new ModuleTransport({ + code: 'transformed foo;', + sourceCode: 'source foo', + sourcePath: 'foo path' + })); ppackage.finalize(); expect(ppackage.getMinifiedSourceAndMap()).toBe(minified); }); @@ -68,13 +93,104 @@ describe('Package', function() { describe('sourcemap package', function() { it('should create sourcemap', function() { var p = new Package('test_url'); - p.addModule('transformed foo;\n', 'source foo', 'foo path'); - p.addModule('transformed bar;\n', 'source bar', 'bar path'); + p.addModule(new ModuleTransport({ + code: [ + 'transformed foo', + 'transformed foo', + 'transformed foo', + ].join('\n'), + sourceCode: [ + 'source foo', + 'source foo', + 'source foo', + ].join('\n'), + sourcePath: 'foo path', + })); + p.addModule(new ModuleTransport({ + code: [ + 'transformed bar', + 'transformed bar', + 'transformed bar', + ].join('\n'), + sourceCode: [ + 'source bar', + 'source bar', + 'source bar', + ].join('\n'), + sourcePath: 'bar path', + })); + p.setMainModuleId('foo'); p.finalize({runMainModule: true}); var s = p.getSourceMap(); expect(s).toEqual(genSourceMap(p._modules)); }); + + it('should combine sourcemaps', function() { + var p = new Package('test_url'); + + p.addModule(new ModuleTransport({ + code: 'transformed foo;\n', + map: {name: 'sourcemap foo'}, + sourceCode: 'source foo', + sourcePath: 'foo path' + })); + + p.addModule(new ModuleTransport({ + code: 'transformed foo;\n', + map: {name: 'sourcemap bar'}, + sourceCode: 'source foo', + sourcePath: 'foo path' + })); + + p.addModule(new ModuleTransport({ + code: 'image module;\nimage module;', + virtual: true, + sourceCode: 'image module;\nimage module;', + sourcePath: 'image.png', + })); + + p.setMainModuleId('foo'); + p.finalize({runMainModule: true}); + + var s = p.getSourceMap(); + expect(s).toEqual({ + file: 'bundle.js', + version: 3, + sections: [ + { offset: { line: 0, column: 0 }, map: { name: 'sourcemap foo' } }, + { offset: { line: 2, column: 0 }, map: { name: 'sourcemap bar' } }, + { + offset: { + column: 0, + line: 4 + }, + map: { + file: 'image.png', + mappings: 'AAAA;AACA;', + names: {}, + sources: [ 'image.png' ], + sourcesContent: ['image module;\nimage module;'], + version: 3, + } + }, + { + offset: { + column: 0, + line: 6 + }, + map: { + file: 'RunMainModule.js', + mappings: 'AAAA;', + names: {}, + sources: [ 'RunMainModule.js' ], + sourcesContent: [';require("foo");'], + version: 3, + } + } + ], + }); + }); }); describe('getAssets()', function() { @@ -95,7 +211,7 @@ describe('Package', function() { var packageLineNo = 0; for (var i = 0; i < modules.length; i++) { var module = modules[i]; - var transformedCode = module.transformedCode; + var transformedCode = module.code; var sourcePath = module.sourcePath; var sourceCode = module.sourceCode; var transformedLineCount = 0; diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Packager/__tests__/Packager-test.js index 8e1420a3a..3f0934462 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Packager/__tests__/Packager-test.js @@ -13,6 +13,7 @@ jest .dontMock('path') .dontMock('os') .dontMock('underscore') + .dontMock('../../lib/ModuleTransport') .setMock('uglify-js') .dontMock('../'); @@ -93,6 +94,7 @@ describe('Packager', function() { .mockImpl(function(path) { return Promise.resolve({ code: 'transformed ' + path, + map: 'sourcemap ' + path, sourceCode: 'source ' + path, sourcePath: path }); @@ -117,16 +119,19 @@ describe('Packager', function() { return packager.package('/root/foo.js', true, 'source_map_url') .then(function(p) { - expect(p.addModule.mock.calls[0]).toEqual([ - 'lol transformed /root/foo.js lol', - 'source /root/foo.js', - '/root/foo.js' - ]); - expect(p.addModule.mock.calls[1]).toEqual([ - 'lol transformed /root/bar.js lol', - 'source /root/bar.js', - '/root/bar.js' - ]); + expect(p.addModule.mock.calls[0][0]).toEqual({ + code: 'lol transformed /root/foo.js lol', + map: 'sourcemap /root/foo.js', + sourceCode: 'source /root/foo.js', + sourcePath: '/root/foo.js', + }); + + expect(p.addModule.mock.calls[1][0]).toEqual({ + code: 'lol transformed /root/bar.js lol', + map: 'sourcemap /root/bar.js', + sourceCode: 'source /root/bar.js', + sourcePath: '/root/bar.js' + }); var imgModule_DEPRECATED = { __packager_asset: true, @@ -138,15 +143,15 @@ describe('Packager', function() { deprecated: true, }; - expect(p.addModule.mock.calls[2]).toEqual([ - 'lol module.exports = ' + + expect(p.addModule.mock.calls[2][0]).toEqual({ + code: 'lol module.exports = ' + JSON.stringify(imgModule_DEPRECATED) + '; lol', - 'module.exports = ' + + sourceCode: 'module.exports = ' + JSON.stringify(imgModule_DEPRECATED) + ';', - '/root/img/img.png' - ]); + sourcePath: '/root/img/img.png' + }); var imgModule = { __packager_asset: true, @@ -160,21 +165,21 @@ describe('Packager', function() { type: 'png', }; - expect(p.addModule.mock.calls[3]).toEqual([ - 'lol module.exports = ' + + expect(p.addModule.mock.calls[3][0]).toEqual({ + code: 'lol module.exports = ' + JSON.stringify(imgModule) + '; lol', - 'module.exports = ' + + sourceCode: 'module.exports = ' + JSON.stringify(imgModule) + ';', - '/root/img/new_image.png' - ]); + sourcePath: '/root/img/new_image.png' + }); - expect(p.addModule.mock.calls[4]).toEqual([ - 'lol module.exports = {"json":true}; lol', - 'module.exports = {"json":true};', - '/root/file.json' - ]); + expect(p.addModule.mock.calls[4][0]).toEqual({ + code: 'lol module.exports = {"json":true}; lol', + sourceCode: 'module.exports = {"json":true};', + sourcePath: '/root/file.json' + }); expect(p.finalize.mock.calls[0]).toEqual([ {runMainModule: true} @@ -189,5 +194,4 @@ describe('Packager', function() { ]); }); }); - }); diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index 8563e2728..c03a0432c 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -14,9 +14,9 @@ var path = require('path'); var Promise = require('bluebird'); var Transformer = require('../JSTransformer'); var DependencyResolver = require('../DependencyResolver'); -var _ = require('underscore'); var Package = require('./Package'); var Activity = require('../Activity'); +var ModuleTransport = require('../lib/ModuleTransport'); var declareOpts = require('../lib/declareOpts'); var imageSize = require('image-size'); @@ -125,12 +125,8 @@ Packager.prototype.package = function(main, runModule, sourceMapUrl, isDev) { .then(function(transformedModules) { Activity.endEvent(transformEventId); - transformedModules.forEach(function(transformed) { - ppackage.addModule( - transformed.code, - transformed.sourceCode, - transformed.sourcePath - ); + transformedModules.forEach(function(moduleTransport) { + ppackage.addModule(moduleTransport); }); ppackage.finalize({ runMainModule: runModule }); @@ -163,11 +159,13 @@ Packager.prototype._transformModule = function(ppackage, module) { var resolver = this._resolver; return transform.then(function(transformed) { - return _.extend( - {}, - transformed, - {code: resolver.wrapModule(module, transformed.code)} - ); + var code = resolver.wrapModule(module, transformed.code); + return new ModuleTransport({ + code: code, + map: transformed.map, + sourceCode: transformed.sourceCode, + sourcePath: transformed.sourcePath, + }); }); }; @@ -191,11 +189,12 @@ Packager.prototype.generateAssetModule_DEPRECATED = function(ppackage, module) { var code = 'module.exports = ' + JSON.stringify(img) + ';'; - return { + return new ModuleTransport({ code: code, sourceCode: code, sourcePath: module.path, - }; + virtual: true, + }); }); }; @@ -222,11 +221,12 @@ Packager.prototype.generateAssetModule = function(ppackage, module) { var code = 'module.exports = ' + JSON.stringify(img) + ';'; - return { + return new ModuleTransport({ code: code, sourceCode: code, sourcePath: module.path, - }; + virtual: true, + }); }); }; @@ -234,11 +234,12 @@ function generateJSONModule(module) { return readFile(module.path).then(function(data) { var code = 'module.exports = ' + data.toString('utf8') + ';'; - return { + return new ModuleTransport({ code: code, sourceCode: code, sourcePath: module.path, - }; + virtual: true, + }); }); } diff --git a/packager/react-packager/src/lib/ModuleTransport.js b/packager/react-packager/src/lib/ModuleTransport.js new file mode 100644 index 000000000..a5f1d5689 --- /dev/null +++ b/packager/react-packager/src/lib/ModuleTransport.js @@ -0,0 +1,38 @@ +/** + * 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. + */ +'use strict'; + +function ModuleTransport(data) { + assertExists(data, 'code'); + this.code = data.code; + + assertExists(data, 'sourceCode'); + this.sourceCode = data.sourceCode; + + assertExists(data, 'sourcePath'); + this.sourcePath = data.sourcePath; + + this.virtual = data.virtual; + + if (this.virtual && data.map) { + throw new Error('Virtual modules cannot have source maps'); + } + + this.map = data.map; + + Object.freeze(this); +} + +module.exports = ModuleTransport; + +function assertExists(obj, field) { + if (obj[field] == null) { + throw new Error('Modules must have `' + field + '`'); + } +} From 28e6e993c6c536a198eb2d841aae21cda4f1df08 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Fri, 1 May 2015 17:50:20 -0700 Subject: [PATCH 21/51] [ReactNative] Navigator focus handler context fix --- Libraries/CustomComponents/Navigator/Navigator.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 280d8c4dd..f6fd60888 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -231,16 +231,13 @@ var Navigator = React.createClass({ initialRouteStack: PropTypes.arrayOf(PropTypes.object), /** - * Will emit the target route upon mounting and before each nav transition, - * overriding the handler in this.props.navigator. This overrides the onDidFocus - * handler that would be found in this.props.navigator + * Will emit the target route upon mounting and before each nav transition */ onWillFocus: PropTypes.func, /** * Will be called with the new route of each scene after the transition is - * complete or after the initial mounting. This overrides the onDidFocus - * handler that would be found in this.props.navigator + * complete or after the initial mounting */ onDidFocus: PropTypes.func, @@ -601,7 +598,8 @@ var Navigator = React.createClass({ this._lastDidFocus = route; if (this.props.onDidFocus) { this.props.onDidFocus(route); - } else if (this.parentNavigator && this.parentNavigator.onDidFocus) { + } + if (this.parentNavigator && this.parentNavigator.onDidFocus) { this.parentNavigator.onDidFocus(route); } }, @@ -617,7 +615,8 @@ var Navigator = React.createClass({ } if (this.props.onWillFocus) { this.props.onWillFocus(route); - } else if (this.parentNavigator && this.parentNavigator.onWillFocus) { + } + if (this.parentNavigator && this.parentNavigator.onWillFocus) { this.parentNavigator.onWillFocus(route); } }, From c0d77dbe9c43e9ad27e4c39e2f6fa48fb9a6ca3b Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Fri, 1 May 2015 18:59:59 -0700 Subject: [PATCH 22/51] [ReactNative] Navigator pop specify route to pop Summary: Now you can pass a route to the pop function in the navigator context, and the Navigator will pop that scene off the stack and every scene that follows. This changes the request API that bubbles up to the top-level navigator. The `pop` request had previously taken a route for `popToRoute`, but that is a more obscure use case. This makes the request API more closely match the context method naming. @public Test Plan: Verified this works by popping several routes in AdsManager. Experimented with UIExplorer Navigator example to make sure popToRoute still works as expected --- .../CustomComponents/Navigator/Navigator.js | 36 +++++++++++++++---- .../Navigator/NavigatorInterceptor.js | 2 ++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index f6fd60888..b740cfaea 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -387,12 +387,12 @@ var Navigator = React.createClass({ return this._handleRequest.apply(null, arguments); }, - requestPop: function() { - return this.request('pop'); + requestPop: function(popToBeforeRoute) { + return this.request('pop', popToBeforeRoute); }, requestPopTo: function(route) { - return this.request('pop', route); + return this.request('popTo', route); }, _handleRequest: function(action, arg1, arg2) { @@ -403,6 +403,8 @@ var Navigator = React.createClass({ switch (action) { case 'pop': return this._handlePop(arg1); + case 'popTo': + return this._handlePopTo(arg1); case 'push': return this._handlePush(arg1); default: @@ -411,11 +413,31 @@ var Navigator = React.createClass({ } }, - _handlePop: function(route) { - if (route) { - var hasRoute = this.state.routeStack.indexOf(route) !== -1; + _handlePop: function(popToBeforeRoute) { + if (popToBeforeRoute) { + var popToBeforeRouteIndex = this.state.routeStack.indexOf(popToBeforeRoute); + if (popToBeforeRouteIndex === -1) { + return false; + } + invariant( + popToBeforeRouteIndex <= this.state.presentedIndex, + 'Cannot pop past a route that is forward in the navigator' + ); + this._popN(this.state.presentedIndex - popToBeforeRouteIndex + 1); + return true; + } + if (this.state.presentedIndex === 0) { + return false; + } + this.pop(); + return true; + }, + + _handlePopTo: function(destRoute) { + if (destRoute) { + var hasRoute = this.state.routeStack.indexOf(destRoute) !== -1; if (hasRoute) { - this.popToRoute(route); + this.popToRoute(destRoute); return true; } else { return false; diff --git a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js index e70820435..efff4c9dc 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js +++ b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js @@ -79,6 +79,8 @@ var NavigatorInterceptor = React.createClass({ switch (action) { case 'pop': return this.props.onPopRequest && this.props.onPopRequest(arg1, arg2); + case 'popTo': + return this.props.onPopToRequest && this.props.onPopToRequest(arg1, arg2); case 'push': return this.props.onPushRequest && this.props.onPushRequest(arg1, arg2); default: From 30a479db8b9d9fa8b09030201cd533f9bd175400 Mon Sep 17 00:00:00 2001 From: Emily Eisenberg Date: Fri, 1 May 2015 18:29:43 -0700 Subject: [PATCH 23/51] Make AsyncStorage types match the implementations Summary: The `getAllKeys` callback type was completely missing the return value, and all of the callbacks are optional. This just fixes the types to match what the implementations are doing. Closes https://github.com/facebook/react-native/pull/1079 Github Author: Emily Eisenberg Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Libraries/Storage/AsyncStorage.ios.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Libraries/Storage/AsyncStorage.ios.js b/Libraries/Storage/AsyncStorage.ios.js index 5ee2dc5e3..fe92f5c58 100644 --- a/Libraries/Storage/AsyncStorage.ios.js +++ b/Libraries/Storage/AsyncStorage.ios.js @@ -37,7 +37,7 @@ var AsyncStorage = { */ getItem: function( key: string, - callback: (error: ?Error, result: ?string) => void + callback?: ?(error: ?Error, result: ?string) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet([key], function(errors, result) { @@ -60,7 +60,7 @@ var AsyncStorage = { setItem: function( key: string, value: string, - callback: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet([[key,value]], function(errors) { @@ -78,7 +78,7 @@ var AsyncStorage = { */ removeItem: function( key: string, - callback: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove([key], function(errors) { @@ -100,7 +100,7 @@ var AsyncStorage = { mergeItem: function( key: string, value: string, - callback: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge([[key,value]], function(errors) { @@ -119,7 +119,7 @@ var AsyncStorage = { * don't want to call this - use removeItem or multiRemove to clear only your * own keys instead. Returns a `Promise` object. */ - clear: function(callback: ?(error: ?Error) => void): Promise { + clear: function(callback?: ?(error: ?Error) => void): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.clear(function(error) { callback && callback(convertError(error)); @@ -135,7 +135,7 @@ var AsyncStorage = { /** * Gets *all* keys known to the system, for all callers, libraries, etc. Returns a `Promise` object. */ - getAllKeys: function(callback: (error: ?Error) => void): Promise { + getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.getAllKeys(function(error, keys) { callback && callback(convertError(error), keys); @@ -166,7 +166,7 @@ var AsyncStorage = { */ multiGet: function( keys: Array, - callback: (errors: ?Array, result: ?Array>) => void + callback?: ?(errors: ?Array, result: ?Array>) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet(keys, function(errors, result) { @@ -189,7 +189,7 @@ var AsyncStorage = { */ multiSet: function( keyValuePairs: Array>, - callback: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { @@ -209,7 +209,7 @@ var AsyncStorage = { */ multiRemove: function( keys: Array, - callback: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove(keys, function(errors) { @@ -232,7 +232,7 @@ var AsyncStorage = { */ multiMerge: function( keyValuePairs: Array>, - callback: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { From a4616bff3fd397d5d37efe15596b439ed0e96185 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Sat, 2 May 2015 10:09:36 -0700 Subject: [PATCH 24/51] [ReactNative] Test case for text update bug --- Libraries/Text/TextUpdateTest.js | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Libraries/Text/TextUpdateTest.js diff --git a/Libraries/Text/TextUpdateTest.js b/Libraries/Text/TextUpdateTest.js new file mode 100644 index 000000000..367ccd1fc --- /dev/null +++ b/Libraries/Text/TextUpdateTest.js @@ -0,0 +1,64 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule TextUpdateTest + * @flow + */ +'use strict'; + +var React = require('react-native'); +var TimerMixin = require('react-timer-mixin'); +var { + NativeModules, + StyleSheet, + Text, +} = React; + +var MIX_TYPES = false; // TODO(#6916648): fix bug and set true + +var TestManager = NativeModules.TestManager || NativeModules.SnapshotTestManager; + +var TextUpdateTest = React.createClass({ + mixins: [TimerMixin], + getInitialState: function() { + return {seeMore: true}; + }, + componentDidMount: function() { + this.requestAnimationFrame( + () => this.setState( + {seeMore: false}, + TestManager.markTestCompleted + ) + ); + }, + render: function() { + var extraText = MIX_TYPES ? 'raw text' : wrapped text; + return ( + this.setState({seeMore: !this.state.seeMore})}> + Tap to see more (bugs)... + {this.state.seeMore && extraText} + + ); + }, +}); + +var styles = StyleSheet.create({ + container: { + margin: 10, + marginTop: 100, + }, +}); + +module.exports = TextUpdateTest; From 5e110d277651e9b5f7d70f31d4807451b3959871 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Sat, 2 May 2015 10:09:37 -0700 Subject: [PATCH 25/51] [ReactNative] Fix Text Updating Crash Summary: @public {D1953613} added an optimization that allowed for shadow nodes that are not backed by views, but didn't actually work robustly in the remove case because the indices can get out of sync. That diff also started returning nil for raw text nodes, which triggered this bug and broke "see more" functionality in the `FBTextWithEntities` and `ExpandingText` components, leading to crashes in the Groups app. This diff fixes the issue by simply returning `UIView` placeholders again. Slight perf/ memory cost but no more crashes and there should be no other adverse affects. We'll need to think up something more clever in order to properly support `nil` views in the future, probably something that uses the shadow hierarchy to build the View hierarchy, rather than mirroring identical commands to both - see #1102. Test Plan: - TextUpdateTest fails without native changes, now passes with them. - ExpandingText example no longer crashes. - See More in Groups app no longer crashes. --- Libraries/Text/RCTRawTextManager.m | 2 +- Libraries/Text/TextUpdateTest.js | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Libraries/Text/RCTRawTextManager.m b/Libraries/Text/RCTRawTextManager.m index 221b8daeb..b6ad9b110 100644 --- a/Libraries/Text/RCTRawTextManager.m +++ b/Libraries/Text/RCTRawTextManager.m @@ -17,7 +17,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return nil; + return [[UIView alloc] init]; // TODO(#1102) Remove useless views. } - (RCTShadowView *)shadowView diff --git a/Libraries/Text/TextUpdateTest.js b/Libraries/Text/TextUpdateTest.js index 367ccd1fc..c4218f73d 100644 --- a/Libraries/Text/TextUpdateTest.js +++ b/Libraries/Text/TextUpdateTest.js @@ -24,8 +24,6 @@ var { Text, } = React; -var MIX_TYPES = false; // TODO(#6916648): fix bug and set true - var TestManager = NativeModules.TestManager || NativeModules.SnapshotTestManager; var TextUpdateTest = React.createClass({ @@ -42,13 +40,12 @@ var TextUpdateTest = React.createClass({ ); }, render: function() { - var extraText = MIX_TYPES ? 'raw text' : wrapped text; return ( this.setState({seeMore: !this.state.seeMore})}> Tap to see more (bugs)... - {this.state.seeMore && extraText} + {this.state.seeMore && 'raw text'} ); }, From 59997df1c15e33b08afe098a2d2d0d59358c82e4 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Sat, 2 May 2015 10:09:58 -0700 Subject: [PATCH 26/51] [ReactNative] Fix warnings w/h => width/height --- Libraries/Components/ScrollResponder.js | 2 +- Libraries/Components/View/ViewStylePropTypes.js | 2 +- Libraries/Utilities/differ/sizesDiffer.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 1f3d493f3..74be17a46 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -355,7 +355,7 @@ var ScrollResponderMixin = { /** * A helper function to zoom to a specific rect in the scrollview. - * @param {object} rect Should have shape {x, y, w, h} + * @param {object} rect Should have shape {x, y, width, height} */ scrollResponderZoomTo: function(rect: { x: number; y: number; width: number; height: number; }) { RCTUIManagerDeprecated.zoomToRect(this.getNodeHandle(), rect); diff --git a/Libraries/Components/View/ViewStylePropTypes.js b/Libraries/Components/View/ViewStylePropTypes.js index 7bb795f16..a5b591a61 100644 --- a/Libraries/Components/View/ViewStylePropTypes.js +++ b/Libraries/Components/View/ViewStylePropTypes.js @@ -30,7 +30,7 @@ var ViewStylePropTypes = { overflow: ReactPropTypes.oneOf(['visible', 'hidden']), shadowColor: ReactPropTypes.string, shadowOffset: ReactPropTypes.shape( - {h: ReactPropTypes.number, w: ReactPropTypes.number} + {width: ReactPropTypes.number, height: ReactPropTypes.number} ), shadowOpacity: ReactPropTypes.number, shadowRadius: ReactPropTypes.number, diff --git a/Libraries/Utilities/differ/sizesDiffer.js b/Libraries/Utilities/differ/sizesDiffer.js index 3bdc72acb..a431cd072 100644 --- a/Libraries/Utilities/differ/sizesDiffer.js +++ b/Libraries/Utilities/differ/sizesDiffer.js @@ -5,14 +5,14 @@ */ 'use strict'; -var dummySize = {w: undefined, h: undefined}; +var dummySize = {width: undefined, height: undefined}; var sizesDiffer = function(one, two) { one = one || dummySize; two = two || dummySize; return one !== two && ( - one.w !== two.w || - one.h !== two.h + one.width !== two.width || + one.height !== two.height ); }; From 8c2f44d64014daf60483fb60eca995cdbb3e01d4 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Sat, 2 May 2015 09:46:49 -0700 Subject: [PATCH 27/51] [react-native] Don't mutate props in TouchableBounce --- Libraries/Components/Touchable/TouchableBounce.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 7cba22164..30d05210f 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -11,14 +11,13 @@ */ 'use strict'; -var NativeMethodsMixin = require('NativeMethodsMixin'); -var React = require('React'); -var POPAnimation = require('POPAnimation'); var AnimationExperimental = require('AnimationExperimental'); +var NativeMethodsMixin = require('NativeMethodsMixin'); +var POPAnimation = require('POPAnimation'); +var React = require('React'); var Touchable = require('Touchable'); var merge = require('merge'); -var copyProperties = require('copyProperties'); var onlyChild = require('onlyChild'); type State = { @@ -123,9 +122,8 @@ var TouchableBounce = React.createClass({ }, render: function() { - // Note(vjeux): use cloneWithProps once React has been upgraded var child = onlyChild(this.props.children); - copyProperties(child.props, { + return React.cloneElement(child, { accessible: true, testID: this.props.testID, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, @@ -135,7 +133,6 @@ var TouchableBounce = React.createClass({ onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate }); - return child; } }); From 17262db5a986430991bc15e44ef879aa8ab061c2 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Sat, 2 May 2015 13:12:12 -0700 Subject: [PATCH 28/51] [ReactNative] Fix JS calls being lost --- React/Base/RCTBridge.m | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 85486ddc1..134b3659d 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1146,15 +1146,12 @@ static id _latestJSExecutor; { #if BATCHED_BRIDGE - __weak NSMutableArray *weakScheduledCalls = _scheduledCalls; - __weak RCTSparseArray *weakScheduledCallbacks = _scheduledCallbacks; - + __weak RCTBridge *weakSelf = self; [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileBeginEvent(); - NSMutableArray *scheduledCalls = weakScheduledCalls; - RCTSparseArray *scheduledCallbacks = weakScheduledCallbacks; - if (!scheduledCalls || !scheduledCallbacks) { + RCTBridge *strongSelf = weakSelf; + if (!strongSelf.isValid || !strongSelf->_scheduledCallbacks || !strongSelf->_scheduledCalls) { return; } @@ -1170,7 +1167,7 @@ static id _latestJSExecutor; * Keep going if it any event emmiter, e.g. RCT(Device|NativeApp)?EventEmitter */ if ([moduleName hasSuffix:@"EventEmitter"]) { - for (NSDictionary *call in [scheduledCalls copy]) { + for (NSDictionary *call in [strongSelf->_scheduledCalls copy]) { NSArray *callArgs = call[@"args"]; /** * If it's the same module && method call on the bridge && @@ -1193,7 +1190,7 @@ static id _latestJSExecutor; [args[2][0] isEqual:callArgs[2][0]] && ([moduleName isEqualToString:@"RCTEventEmitter"] ? [args[2][1] isEqual:callArgs[2][1]] : YES) ) { - [scheduledCalls removeObject:call]; + [strongSelf->_scheduledCalls removeObject:call]; } } } @@ -1208,9 +1205,9 @@ static id _latestJSExecutor; }; if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { - scheduledCallbacks[args[0]] = call; + strongSelf->_scheduledCallbacks[args[0]] = call; } else { - [scheduledCalls addObject:call]; + [strongSelf->_scheduledCalls addObject:call]; } RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); From 09460cf21b7b796af169cf45910d6fb461706a29 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Sat, 2 May 2015 14:11:09 -0700 Subject: [PATCH 29/51] [ReactNative] Use explicit doubles on RCTLocationOptions to avoid NSInvocation bug --- Libraries/Geolocation/RCTLocationObserver.m | 6 +++--- React/Base/RCTBridge.m | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 3e864657b..f21233dbf 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -28,9 +28,9 @@ typedef NS_ENUM(NSInteger, RCTPositionErrorCode) { #define RCT_DEFAULT_LOCATION_ACCURACY kCLLocationAccuracyHundredMeters typedef struct { - NSTimeInterval timeout; - NSTimeInterval maximumAge; - CLLocationAccuracy accuracy; + double timeout; + double maximumAge; + double accuracy; } RCTLocationOptions; @implementation RCTConvert (RCTLocationOptions) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 134b3659d..9ce2e0370 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -381,10 +381,8 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) case '{': { [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { - NSUInteger size; - NSGetSizeAndAlignment(argumentType, &size, NULL); - void *returnValue = malloc(size); NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; + void *returnValue = malloc(methodSignature.methodReturnLength); NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [_invocation setTarget:[RCTConvert class]]; [_invocation setSelector:selector]; From d29a0c67685e19fb7a3c5e2fb030c5e29011af7f Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Sat, 2 May 2015 18:43:34 -0700 Subject: [PATCH 30/51] Fix for nil array crash --- React/Base/RCTBridge.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 9ce2e0370..22fce74fc 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -49,7 +49,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { * Temporarily allow to turn on and off the call batching in case someone wants * to profile both */ -#define BATCHED_BRIDGE 1 +#define BATCHED_BRIDGE 0 #ifdef __LP64__ typedef uint64_t RCTHeaderValue; @@ -206,10 +206,14 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) arguments:(NSArray *)args context:(NSNumber *)context; +#if BATCHED_BRIDGE + - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context; +#endif + @end /** From c5a6ec5b53f333db58e35d08c5c2cad22b5b3b42 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Sun, 3 May 2015 15:39:27 -0700 Subject: [PATCH 31/51] Disable React Native dev menu in release mode --- React/Base/RCTDevMenu.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 82b4fa968..c6063caa2 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -66,7 +66,9 @@ RCT_EXPORT_MODULE() // 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. +#if RCT_DEV RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:)); +#endif } - (instancetype)init From 548a0a6a4fc8df4c528e244fe3bf939291a73961 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Mon, 4 May 2015 02:40:45 -0700 Subject: [PATCH 32/51] [ReactNative] Fix Navigator scene hiding logic Summary: Scenes with 0 opacity are being rendered on top of other scenes with full absolute positioning. On iOS this is fine, because the platform will not send touch events to a view with no opacity. On Android is poses a problem because the view on top, even with no opacity, is catching the touch events for the presented scene below. This change enhances the scene enabling and hiding logic, has better naming, and improves the documentation of it. @public Test Plan: Tested transitions and gestures in slow motion on iOS and Android --- .../CustomComponents/Navigator/Navigator.js | 99 +++++++++++++++---- 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index b740cfaea..6d09b656e 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -28,6 +28,7 @@ var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; var BackAndroid = require('BackAndroid'); +var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); var NavigatorInterceptor = require('NavigatorInterceptor'); @@ -39,6 +40,7 @@ var Platform = require('Platform'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); +var StyleSheetRegistry = require('StyleSheetRegistry'); var Subscribable = require('Subscribable'); var TimerMixin = require('react-timer-mixin'); var View = require('View'); @@ -52,13 +54,37 @@ var rebound = require('rebound'); var PropTypes = React.PropTypes; -var OFF_SCREEN = {style: {opacity: 0}}; +var SCREEN_WIDTH = Dimensions.get('window').width; +var SCENE_DISABLED_NATIVE_PROPS = { + style: { + left: SCREEN_WIDTH, + opacity: 0, + }, +}; var __uid = 0; function getuid() { return __uid++; } +function resolveStyle(styles) { + // The styles for a scene are a prop in the style format, which can be an object, + // a number (which refers to the StyleSheetRegistry), or an arry of objects/numbers. + // This function resolves the actual style values so we can call setNativeProps with + // matching styles. + var resolvedStyle = {}; + if (!Array.isArray(styles)) { + styles = [styles]; + } + styles.forEach((style) => { + if (typeof style === 'number') { + style = StyleSheetRegistry.getStyleByID(style); + } + resolvedStyle = merge(resolvedStyle, style); + }); + return resolvedStyle; +} + // styles moved to the top of the file so getDefaultProps can refer to it var styles = StyleSheet.create({ container: { @@ -72,7 +98,7 @@ var styles = StyleSheet.create({ bottom: 0, top: 0, }, - currentScene: { + baseScene: { position: 'absolute', overflow: 'hidden', left: 0, @@ -80,11 +106,8 @@ var styles = StyleSheet.create({ bottom: 0, top: 0, }, - futureScene: { - overflow: 'hidden', - position: 'absolute', - left: 0, - opacity: 0, + disabledScene: { + left: SCREEN_WIDTH, }, transitioner: { flex: 1, @@ -571,10 +594,12 @@ var Navigator = React.createClass({ }, /** - * This happens at the end of a transition started by transitionTo + * This happens at the end of a transition started by transitionTo, and when the spring catches up to a pending gesture */ _completeTransition: function() { if (this.spring.getCurrentValue() !== 1) { + // The spring has finished catching up to a gesture in progress. Remove the pending progress + // and we will be in a normal activeGesture state if (this.state.pendingGestureProgress) { this.state.pendingGestureProgress = null; } @@ -599,11 +624,16 @@ var Navigator = React.createClass({ this._interactionHandle = null; } if (this.state.pendingGestureProgress) { + // A transition completed, but there is already another gesture happening. + // Enable the scene and set the spring to catch up with the new gesture + var gestureToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + this._enableScene(gestureToIndex); this.spring.setEndValue(this.state.pendingGestureProgress); return; } if (this.state.transitionQueue.length) { var queuedTransition = this.state.transitionQueue.shift(); + this._enableScene(queuedTransition.destIndex); this._transitionTo( queuedTransition.destIndex, queuedTransition.velocity, @@ -644,21 +674,44 @@ var Navigator = React.createClass({ }, /** - * Does not delete the scenes - merely hides them. + * Hides scenes that we are not currently on or transitioning from */ _hideScenes: function() { for (var i = 0; i < this.state.routeStack.length; i++) { - // This gets called when we detach a gesture, so there will not be a - // current gesture, but there might be a transition in progress if (i === this.state.presentedIndex || i === this.state.transitionFromIndex) { continue; } - var sceneRef = 'scene_' + i; - this.refs[sceneRef] && - this.refs['scene_' + i].setNativeProps(OFF_SCREEN); + this._disableScene(i); } }, + /** + * Push a scene off the screen, so that opacity:0 scenes will not block touches sent to the presented scenes + */ + _disableScene: function(sceneIndex) { + this.refs['scene_' + sceneIndex] && + this.refs['scene_' + sceneIndex].setNativeProps(SCENE_DISABLED_NATIVE_PROPS); + }, + + /** + * Put the scene back into the state as defined by props.sceneStyle, so transitions can happen normally + */ + _enableScene: function(sceneIndex) { + // First, determine what the defined styles are for scenes in this navigator + var sceneStyle = resolveStyle(this.props.sceneStyle); + // Then restore the left value for this scene + var enabledSceneNativeProps = { + left: sceneStyle.left, + }; + if (sceneIndex !== this.state.transitionFromIndex) { + // If we are not in a transition from this index, make sure opacity is 0 + // to prevent the enabled scene from flashing over the presented scene + enabledSceneNativeProps.opacity = 0; + } + this.refs['scene_' + sceneIndex] && + this.refs['scene_' + sceneIndex].setNativeProps(enabledSceneNativeProps); + }, + _onAnimationStart: function() { var fromIndex = this.state.presentedIndex; var toIndex = this.state.presentedIndex; @@ -669,7 +722,6 @@ var Navigator = React.createClass({ } this._setRenderSceneToHarwareTextureAndroid(fromIndex, true); this._setRenderSceneToHarwareTextureAndroid(toIndex, true); - var navBar = this._navBar; if (navBar && navBar.onAnimationStart) { navBar.onAnimationStart(fromIndex, toIndex); @@ -801,6 +853,8 @@ var Navigator = React.createClass({ _attachGesture: function(gestureId) { this.state.activeGesture = gestureId; + var gesturingToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + this._enableScene(gesturingToIndex); }, _detachGesture: function() { @@ -831,6 +885,7 @@ var Navigator = React.createClass({ (gesture.fullDistance - gestureDetectMovement); if (nextProgress < 0 && gesture.isDetachable) { this._detachGesture(); + this.spring.setCurrentValue(0); } if (this._doesGestureOverswipe(this.state.activeGesture)) { var frictionConstant = gesture.overswipe.frictionConstant; @@ -945,6 +1000,7 @@ var Navigator = React.createClass({ _jumpN: function(n) { var destIndex = this._getDestIndexWithinBounds(n); var requestTransitionAndResetUpdatingRange = () => { + this._enableScene(destIndex); this._transitionTo(destIndex); this._resetUpdatingRange(); }; @@ -978,12 +1034,14 @@ var Navigator = React.createClass({ var activeIDStack = this.state.idStack.slice(0, activeLength); var activeAnimationConfigStack = this.state.sceneConfigStack.slice(0, activeLength); var nextStack = activeStack.concat([route]); + var destIndex = nextStack.length - 1; var nextIDStack = activeIDStack.concat([getuid()]); var nextAnimationConfigStack = activeAnimationConfigStack.concat([ this.props.configureScene(route), ]); var requestTransitionAndResetUpdatingRange = () => { - this._transitionTo(nextStack.length - 1); + this._enableScene(destIndex); + this._transitionTo(destIndex); this._resetUpdatingRange(); }; this.setState({ @@ -1004,6 +1062,7 @@ var Navigator = React.createClass({ 'Cannot pop below zero' ); var popIndex = this.state.presentedIndex - n; + this._enableScene(popIndex); this._transitionTo( popIndex, null, // default velocity @@ -1211,8 +1270,10 @@ var Navigator = React.createClass({ route, sceneNavigatorContext ); - var initialSceneStyle = i === this.state.presentedIndex ? - styles.currentScene : styles.futureScene; + var disabledSceneStyle = null; + if (i !== this.state.presentedIndex) { + disabledSceneStyle = styles.disabledScene; + } return ( { return i !== this.state.presentedIndex; }} - style={[initialSceneStyle, this.props.sceneStyle]}> + style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}> {React.cloneElement(child, { ref: this._handleItemRef.bind(null, this.state.idStack[i]), })} From b532ec000fad08f7c4e66b94e276a7dbcf142cfe Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Mon, 4 May 2015 10:41:38 -0700 Subject: [PATCH 33/51] [react-packager] Update sane to use watch-project --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 40a3e9b51..54d7ada11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native", - "version": "0.3.7", + "version": "0.4.1", "description": "A framework for building native apps using React", "repository": { "type": "git", @@ -57,7 +57,7 @@ "react-timer-mixin": "^0.13.1", "react-tools": "0.13.2", "rebound": "^0.0.12", - "sane": "1.0.3", + "sane": "^1.1.1", "source-map": "0.1.31", "stacktrace-parser": "git://github.com/frantic/stacktrace-parser.git#493c5e5638", "uglify-js": "~2.4.16", From 132a9170f12b262fd15226d38b7ba83285d76da9 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Mon, 4 May 2015 10:35:49 -0700 Subject: [PATCH 34/51] [ReactNative] Create private underlying bridge to prevent retain cycles --- Libraries/RCTTest/RCTTestRunner.m | 10 +- Libraries/Utilities/MessageQueue.js | 33 +- React/Base/RCTBridge.h | 19 - React/Base/RCTBridge.m | 591 +++++++++++++++++----------- React/Base/RCTDevMenu.m | 20 +- React/Base/RCTJavaScriptLoader.h | 2 +- React/Base/RCTJavaScriptLoader.m | 18 +- React/Base/RCTRootView.h | 2 +- React/Base/RCTRootView.m | 130 ++++-- React/Modules/RCTTiming.m | 7 +- React/Modules/RCTUIManager.m | 31 +- React/Views/RCTNavigator.m | 8 +- 12 files changed, 522 insertions(+), 349 deletions(-) diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 75a811831..0aa148fbc 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -17,6 +17,12 @@ #define TIMEOUT_SECONDS 240 +@interface RCTBridge (RCTTestRunner) + +@property (nonatomic, weak) RCTBridge *batchedBridge; + +@end + @implementation RCTTestRunner { FBSnapshotTestController *_testController; @@ -66,7 +72,7 @@ rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]); - RCTTestModule *testModule = rootView.bridge.modules[testModuleName]; + RCTTestModule *testModule = rootView.bridge.batchedBridge.modules[testModuleName]; testModule.controller = _testController; testModule.testSelector = test; testModule.view = rootView; @@ -83,8 +89,6 @@ error = [[RCTRedBox sharedInstance] currentErrorMessage]; } [rootView removeFromSuperview]; - [rootView.bridge invalidate]; - [rootView invalidate]; RCTAssert(vc.view.subviews.count == 0, @"There shouldn't be any other views: %@", vc.view); vc.view = nil; [[RCTRedBox sharedInstance] dismiss]; diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 97658c235..f15dd70e0 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -322,23 +322,24 @@ var MessageQueueMixin = { processBatch: function(batch) { var self = this; - ReactUpdates.batchedUpdates(function() { - batch.forEach(function(call) { - invariant( - call.module === 'BatchedBridge', - 'All the calls should pass through the BatchedBridge module' - ); - if (call.method === 'callFunctionReturnFlushedQueue') { - self.callFunction.apply(self, call.args); - } else if (call.method === 'invokeCallbackAndReturnFlushedQueue') { - self.invokeCallback.apply(self, call.args); - } else { - throw new Error( - 'Unrecognized method called on BatchedBridge: ' + call.method); - } + return guardReturn(function () { + ReactUpdates.batchedUpdates(function() { + batch.forEach(function(call) { + invariant( + call.module === 'BatchedBridge', + 'All the calls should pass through the BatchedBridge module' + ); + if (call.method === 'callFunctionReturnFlushedQueue') { + self._callFunction.apply(self, call.args); + } else if (call.method === 'invokeCallbackAndReturnFlushedQueue') { + self._invokeCallback.apply(self, call.args); + } else { + throw new Error( + 'Unrecognized method called on BatchedBridge: ' + call.method); + } + }); }); - }); - return this.flushedQueue(); + }, null, this._flushedQueueUnguarded, this); }, setLoggingEnabled: function(enabled) { diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 6dfbe2414..77cbb215e 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -80,15 +80,6 @@ RCT_EXTERN NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); __attribute__((used, section("__DATA,RCTImport"))) \ static const char *__rct_import_##module##_##method##__ = #module"."#method; -/** - * This method is used to execute a new application script. It is called - * internally whenever a JS application bundle is loaded/reloaded, but should - * probably not be used at any other time. - */ -- (void)enqueueApplicationScript:(NSString *)script - url:(NSURL *)url - onComplete:(RCTJavaScriptCompleteBlock)onComplete; - /** * URL of the script that was loaded into the bridge. */ @@ -122,14 +113,4 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method; */ - (void)reload; -/** - * Add a new observer that will be called on every screen refresh. - */ -- (void)addFrameUpdateObserver:(id)observer; - -/** - * Stop receiving screen refresh updates for the given observer. - */ -- (void)removeFrameUpdateObserver:(id)observer; - @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 22fce74fc..349cc725e 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -25,6 +25,7 @@ #import "RCTProfile.h" #import "RCTRedBox.h" #import "RCTRootView.h" +#import "RCTSourceCode.h" #import "RCTSparseArray.h" #import "RCTUtils.h" @@ -45,12 +46,6 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; -/** - * Temporarily allow to turn on and off the call batching in case someone wants - * to profile both - */ -#define BATCHED_BRIDGE 0 - #ifdef __LP64__ typedef uint64_t RCTHeaderValue; typedef struct section_64 RCTHeaderSection; @@ -61,6 +56,11 @@ typedef struct section RCTHeaderSection; #define RCTGetSectByNameFromHeader getsectbynamefromheader #endif +#define RCTAssertJSThread() \ + RCTAssert(![NSStringFromClass([_javaScriptExecutor class]) isEqualToString:@"RCTContextExecutor"] || \ + [[[NSThread currentThread] name] isEqualToString:@"com.facebook.React.JavaScript"], \ + @"This method must be called on JS thread") + NSString *const RCTEnqueueNotification = @"RCTEnqueueNotification"; NSString *const RCTDequeueNotification = @"RCTDequeueNotification"; @@ -122,6 +122,7 @@ static NSArray *RCTJSMethods(void) * RTCBridgeModule protocol to ensure they've been exported. This scanning * functionality is disabled in release mode to improve startup performance. */ +static NSDictionary *RCTModuleIDsByName; static NSArray *RCTModuleNamesByID; static NSArray *RCTModuleClassesByID; static NSArray *RCTBridgeModuleClassesByModuleID(void) @@ -129,8 +130,9 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - RCTModuleNamesByID = [NSMutableArray array]; - RCTModuleClassesByID = [NSMutableArray array]; + RCTModuleIDsByName = [[NSMutableDictionary alloc] init]; + RCTModuleNamesByID = [[NSMutableArray alloc] init]; + RCTModuleClassesByID = [[NSMutableArray alloc] init]; Dl_info info; dladdr(&RCTBridgeModuleClassesByModuleID, &info); @@ -162,7 +164,9 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) NSStringFromClass(cls)); // Register module - [(NSMutableArray *)RCTModuleNamesByID addObject:RCTBridgeModuleNameForClass(cls)]; + NSString *moduleName = RCTBridgeModuleNameForClass(cls); + ((NSMutableDictionary *)RCTModuleIDsByName)[moduleName] = @(RCTModuleNamesByID.count); + [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; [(NSMutableArray *)RCTModuleClassesByID addObject:cls]; } } @@ -199,20 +203,31 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) return RCTModuleClassesByID; } +@class RCTBatchedBridge; + @interface RCTBridge () +@property (nonatomic, strong) RCTBatchedBridge *batchedBridge; +@property (nonatomic, strong) RCTBridgeModuleProviderBlock moduleProvider; +@property (nonatomic, strong, readwrite) RCTEventDispatcher *eventDispatcher; + - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context; -#if BATCHED_BRIDGE +@end + +@interface RCTBatchedBridge : RCTBridge + +@property (nonatomic, weak) RCTBridge *parentBridge; + +- (instancetype)initWithParentBridge:(RCTBridge *)bridge; - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context; -#endif @end @@ -238,8 +253,6 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) dispatch_block_t _methodQueue; } -static Class _globalExecutorClass; - static NSString *RCTStringUpToFirstArgument(NSString *methodName) { NSRange colonRange = [methodName rangeOfString:@":"]; @@ -702,6 +715,7 @@ static NSDictionary *RCTLocalModulesConfig() @"methods": [[NSMutableDictionary alloc] init] }; localModules[moduleName] = module; + [RCTLocalModuleNames addObject:moduleName]; } // Add method if it doesn't already exist @@ -712,72 +726,18 @@ static NSDictionary *RCTLocalModulesConfig() @"methodID": @(methods.count), @"type": @"local" }; + [RCTLocalMethodNames addObject:methodName]; } // Add module and method lookup RCTLocalModuleIDs[moduleDotMethod] = module[@"moduleID"]; RCTLocalMethodIDs[moduleDotMethod] = methods[methodName][@"methodID"]; - [RCTLocalModuleNames addObject:moduleName]; - [RCTLocalMethodNames addObject:methodName]; } }); return localModules; } -@interface RCTDisplayLink : NSObject - -- (instancetype)initWithBridge:(RCTBridge *)bridge selector:(SEL)selector NS_DESIGNATED_INITIALIZER; - -@end - -@interface RCTBridge (RCTDisplayLink) - -- (void)_update:(CADisplayLink *)displayLink; - -@end - -@implementation RCTDisplayLink -{ - __weak RCTBridge *_bridge; - CADisplayLink *_displayLink; - SEL _selector; -} - -- (instancetype)initWithBridge:(RCTBridge *)bridge selector:(SEL)selector -{ - if ((self = [super init])) { - _bridge = bridge; - _selector = selector; - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - } - return self; -} - -- (BOOL)isValid -{ - return _displayLink != nil; -} - -- (void)invalidate -{ - if (self.isValid) { - [_displayLink invalidate]; - _displayLink = nil; - } -} - -- (void)_update:(CADisplayLink *)displayLink -{ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - [_bridge performSelector:_selector withObject:displayLink]; -#pragma clang diagnostic pop -} - -@end - @interface RCTFrameUpdate (Private) - (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; @@ -798,22 +758,6 @@ static NSDictionary *RCTLocalModulesConfig() @end @implementation RCTBridge -{ - RCTSparseArray *_modulesByID; - RCTSparseArray *_queuesByID; - dispatch_queue_t _methodQueue; - NSDictionary *_modulesByName; - id _javaScriptExecutor; - Class _executorClass; - NSURL *_bundleURL; - RCTBridgeModuleProviderBlock _moduleProvider; - RCTDisplayLink *_displayLink; - RCTDisplayLink *_vsyncDisplayLink; - NSMutableSet *_frameUpdateObservers; - NSMutableArray *_scheduledCalls; - RCTSparseArray *_scheduledCallbacks; - BOOL _loading; -} static id _latestJSExecutor; @@ -821,36 +765,226 @@ static id _latestJSExecutor; moduleProvider:(RCTBridgeModuleProviderBlock)block launchOptions:(NSDictionary *)launchOptions { + RCTAssertMainThread(); + if ((self = [super init])) { + /** + * Pre register modules + */ + RCTLocalModulesConfig(); + _bundleURL = bundleURL; _moduleProvider = block; _launchOptions = [launchOptions copy]; - - [self setUp]; [self bindKeys]; + [self setUp]; } return self; } +- (void)dealloc +{ + /** + * This runs only on the main thread, but crashes the subclass + * RCTAssertMainThread(); + */ + [self invalidate]; +} + +- (void)bindKeys +{ + RCTAssertMainThread(); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reload) + name:RCTReloadNotification + object:nil]; + +#if TARGET_IPHONE_SIMULATOR + + __weak RCTBridge *weakSelf = self; + RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; + + // reload in current mode + [commands registerKeyCommandWithInput:@"r" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [weakSelf reload]; + }]; + +#endif + +} + +- (void)reload +{ +/** + * AnyThread + */ + dispatch_async(dispatch_get_main_queue(), ^{ + [self invalidate]; + [self setUp]; + }); +} + - (void)setUp { - Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class]; - _javaScriptExecutor = RCTCreateExecutor(executorClass); - _latestJSExecutor = _javaScriptExecutor; - _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; - _methodQueue = dispatch_queue_create("com.facebook.React.BridgeMethodQueue", DISPATCH_QUEUE_SERIAL); - _frameUpdateObservers = [[NSMutableSet alloc] init]; - _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[RCTSparseArray alloc] init]; + RCTAssertMainThread(); - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - _displayLink = [[RCTDisplayLink alloc] initWithBridge:self selector:@selector(_jsThreadUpdate:)]; - }]; - _vsyncDisplayLink = [[RCTDisplayLink alloc] initWithBridge:self selector:@selector(_mainThreadUpdate:)]; + _batchedBridge = [[RCTBatchedBridge alloc] initWithParentBridge:self]; +} + +- (BOOL)isValid +{ + return _batchedBridge.isValid; +} + +- (void)invalidate +{ + RCTAssertMainThread(); + + [_batchedBridge invalidate]; + _batchedBridge = nil; +} + ++ (void)logMessage:(NSString *)message level:(NSString *)level +{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (!_latestJSExecutor.isValid) { + return; + } + + [_latestJSExecutor executeJSCall:@"RCTLog" + method:@"logIfNoNativeHook" + arguments:@[level, message] + context:RCTGetExecutorID(_latestJSExecutor) + callback:^(id json, NSError *error) {}]; + }); +} + +- (NSDictionary *)modules +{ + return _batchedBridge.modules; +} + +#define RCT_BRIDGE_WARN(...) \ +- (void)__VA_ARGS__ \ +{ \ + RCTLogMustFix(@"Called method \"%@\" on top level bridge. This method should \ + only be called from bridge instance in a bridge module", @(__func__)); \ +} + +RCT_BRIDGE_WARN(enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args) +RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context) + +@end + +@implementation RCTBatchedBridge +{ + BOOL _loading; + id _javaScriptExecutor; + RCTSparseArray *_modulesByID; + RCTSparseArray *_queuesByID; + dispatch_queue_t _methodQueue; + NSDictionary *_modulesByName; + CADisplayLink *_mainDisplayLink; + CADisplayLink *_jsDisplayLink; + NSMutableSet *_frameUpdateObservers; + NSMutableArray *_scheduledCalls; + RCTSparseArray *_scheduledCallbacks; +} + +@synthesize valid = _valid; + +- (instancetype)initWithParentBridge:(RCTBridge *)bridge +{ + if (self = [super init]) { + RCTAssertMainThread(); + + _parentBridge = bridge; + + /** + * Set Initial State + */ + _valid = YES; + _loading = YES; + _frameUpdateObservers = [[NSMutableSet alloc] init]; + _scheduledCalls = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; + _queuesByID = [[RCTSparseArray alloc] init]; + _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + + /** + * Initialize executor to allow enqueueing calls + */ + Class executorClass = self.executorClass ?: [RCTContextExecutor class]; + _javaScriptExecutor = RCTCreateExecutor(executorClass); + _latestJSExecutor = _javaScriptExecutor; + + /** + * Setup event dispatcher before initializing modules to allow init calls + */ + self.eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; + + /** + * Initialize and register bridge modules *before* adding the display link + * so we don't have threading issues + */ + _methodQueue = dispatch_queue_create("com.facebook.React.BridgeMethodQueue", DISPATCH_QUEUE_SERIAL); + [self registerModules]; + + /** + * Start the application script + */ + [self initJS]; + } + return self; +} + +- (NSDictionary *)launchOptions +{ + return _parentBridge.launchOptions; +} + +/** + * Override to ensure that we won't create another nested bridge + */ +- (void)setUp {} + +- (void)reload +{ + [_parentBridge reload]; +} + +- (Class)executorClass +{ + return _parentBridge.executorClass; +} + +- (void)setExecutorClass:(Class)executorClass +{ + RCTAssertMainThread(); + + _parentBridge.executorClass = executorClass; +} + +- (BOOL)isLoading +{ + return _loading; +} + +- (BOOL)isValid +{ + return _valid; +} + +- (void)registerModules +{ + RCTAssertMainThread(); // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; - for (id module in _moduleProvider ? _moduleProvider() : nil) { + for (id module in _parentBridge.moduleProvider ? _parentBridge.moduleProvider() : nil) { preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module; } @@ -897,7 +1031,6 @@ static id _latestJSExecutor; } // Get method queues - _queuesByID = [[RCTSparseArray alloc] init]; [_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, BOOL *stop) { if ([module respondsToSelector:@selector(methodQueue)]) { dispatch_queue_t queue = [module methodQueue]; @@ -907,7 +1040,16 @@ static id _latestJSExecutor; _queuesByID[moduleID] = [NSNull null]; } } + + if ([module conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { + [_frameUpdateObservers addObject:module]; + } }]; +} + +- (void)initJS +{ + RCTAssertMainThread(); // Inject module data into JS context NSString *configJSON = RCTJSONStringify(@{ @@ -920,7 +1062,9 @@ static id _latestJSExecutor; dispatch_semaphore_signal(semaphore); }]; - _loading = YES; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW); + + NSURL *bundleURL = _parentBridge.bundleURL; if (_javaScriptExecutor == nil) { /** @@ -929,11 +1073,17 @@ static id _latestJSExecutor; */ _loading = NO; - } else if (_bundleURL) { // Allow testing without a script + } else if (bundleURL) { // Allow testing without a script RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; - [loader loadBundleAtURL:_bundleURL onComplete:^(NSError *error) { + [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { _loading = NO; + if (!self.isValid) { + return; + } + RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = bundleURL; + sourceCodeModule.scriptText = script; if (error != nil) { NSArray *stack = [[error userInfo] objectForKey:@"stack"]; @@ -946,37 +1096,25 @@ static id _latestJSExecutor; } } else { + [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification - object:self]; + if (!loadError) { + /** + * Register the display link to start sending js calls after everything + * is setup + */ + [_jsDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:_parentBridge + userInfo:@{ @"bridge": self }]; + } + }]; } - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reload) - name:RCTReloadNotification - object:nil]; }]; } } -- (void)bindKeys -{ - -#if TARGET_IPHONE_SIMULATOR - - __weak RCTBridge *weakSelf = self; - RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; - - // reload in current mode - [commands registerKeyCommandWithInput:@"r" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - [weakSelf reload]; - }]; - -#endif - -} - - (NSDictionary *)modules { RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. " @@ -985,53 +1123,48 @@ static id _latestJSExecutor; return _modulesByName; } -- (void)dealloc -{ - [self invalidate]; -} - #pragma mark - RCTInvalidating -- (BOOL)isValid -{ - return _javaScriptExecutor != nil; -} - - (void)invalidate { - if (!self.isValid && _modulesByID == nil) { + if (!self.isValid) { return; } - if (![NSThread isMainThread]) { - [self performSelectorOnMainThread:@selector(invalidate) withObject:nil waitUntilDone:YES]; - return; - } + RCTAssertMainThread(); - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - // Release executor + _valid = NO; if (_latestJSExecutor == _javaScriptExecutor) { _latestJSExecutor = nil; } - [_javaScriptExecutor invalidate]; - _javaScriptExecutor = nil; - [_displayLink invalidate]; - [_vsyncDisplayLink invalidate]; - _frameUpdateObservers = nil; + /** + * Main Thread deallocations + */ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_mainDisplayLink invalidate]; - // Invalidate modules - for (id target in _modulesByID.allObjects) { - if ([target respondsToSelector:@selector(invalidate)]) { - [(id)target invalidate]; + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + /** + * JS Thread deallocations + */ + [_javaScriptExecutor invalidate]; + [_jsDisplayLink invalidate]; + + // Invalidate modules + for (id target in _modulesByID.allObjects) { + if ([target respondsToSelector:@selector(invalidate)]) { + [(id)target invalidate]; + } } - } - // Release modules (breaks retain cycle if module has strong bridge reference) - _modulesByID = nil; - _queuesByID = nil; - _modulesByName = nil; + // Release modules (breaks retain cycle if module has strong bridge reference) + _javaScriptExecutor = nil; + _frameUpdateObservers = nil; + _modulesByID = nil; + _queuesByID = nil; + _modulesByName = nil; + }]; } /** @@ -1066,6 +1199,8 @@ static id _latestJSExecutor; */ - (void)_immediatelyCallTimer:(NSNumber *)timer { + RCTAssertJSThread(); + NSString *moduleDotMethod = @"RCTJSTimers.callTimers"; NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod]; RCTAssert(moduleID != nil, @"Module '%@' not registered.", @@ -1074,35 +1209,29 @@ static id _latestJSExecutor; NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); - if (!_loading) { -#if BATCHED_BRIDGE - dispatch_block_t block = ^{ - [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]] - context:RCTGetExecutorID(_javaScriptExecutor)]; - }; - if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { - [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; - } else { - [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } + dispatch_block_t block = ^{ + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + }; -#else - - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]] - context:RCTGetExecutorID(_javaScriptExecutor)]; -#endif + if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { + [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; + } else { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; } } - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); + RCTProfileBeginEvent(); + [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { + RCTAssertJSThread(); + RCTProfileEndEvent(@"ApplicationScript", @"js_call,init", scriptLoadError); if (scriptLoadError) { onComplete(scriptLoadError); @@ -1132,7 +1261,13 @@ static id _latestJSExecutor; - (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID { - id queue = _queuesByID[moduleID]; + RCTAssertJSThread(); + + id queue = nil; + if (moduleID) { + queue = _queuesByID[moduleID]; + } + if (queue == [NSNull null]) { [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; } else { @@ -1146,13 +1281,15 @@ static id _latestJSExecutor; */ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { -#if BATCHED_BRIDGE + /** + * AnyThread + */ - __weak RCTBridge *weakSelf = self; + __weak RCTBatchedBridge *weakSelf = self; [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileBeginEvent(); - RCTBridge *strongSelf = weakSelf; + RCTBatchedBridge *strongSelf = weakSelf; if (!strongSelf.isValid || !strongSelf->_scheduledCallbacks || !strongSelf->_scheduledCalls) { return; } @@ -1190,7 +1327,7 @@ static id _latestJSExecutor; */ if ( [args[2][0] isEqual:callArgs[2][0]] && - ([moduleName isEqualToString:@"RCTEventEmitter"] ? [args[2][1] isEqual:callArgs[2][1]] : YES) + (![moduleName isEqualToString:@"RCTEventEmitter"] || [args[2][1] isEqual:callArgs[2][1]]) ) { [strongSelf->_scheduledCalls removeObject:call]; } @@ -1218,10 +1355,14 @@ static id _latestJSExecutor; - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { -#endif + RCTAssertJSThread(); + [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { + if (!self.isValid) { + return; + } [[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil]; [self _handleBuffer:json context:context]; }; @@ -1237,6 +1378,8 @@ static id _latestJSExecutor; - (void)_handleBuffer:(id)buffer context:(NSNumber *)context { + RCTAssertJSThread(); + if (buffer == nil || buffer == (id)kCFNull) { return; } @@ -1307,6 +1450,11 @@ static id _latestJSExecutor; params:(NSArray *)params context:(NSNumber *)context { + RCTAssertJSThread(); + + if (!self.isValid) { + return NO; + } if (RCT_DEBUG && ![params isKindOfClass:[NSArray class]]) { RCTLogError(@"Invalid module/method/params tuple for request #%zd", i); @@ -1330,10 +1478,10 @@ static id _latestJSExecutor; return NO; } - __weak RCTBridge *weakSelf = self; + __weak RCTBatchedBridge *weakSelf = self; [self dispatchBlock:^{ RCTProfileBeginEvent(); - __strong RCTBridge *strongSelf = weakSelf; + RCTBatchedBridge *strongSelf = weakSelf; if (!strongSelf.isValid) { // strongSelf has been invalidated since the dispatch_async call and this @@ -1367,18 +1515,21 @@ static id _latestJSExecutor; - (void)_jsThreadUpdate:(CADisplayLink *)displayLink { + RCTAssertJSThread(); + RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); + RCTProfileBeginEvent(); RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; for (id observer in _frameUpdateObservers) { if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { - [observer didUpdateFrame:frameUpdate]; + [self dispatchBlock:^{ + [observer didUpdateFrame:frameUpdate]; + } forModule:RCTModuleIDsByName[RCTBridgeModuleNameForClass([observer class])]]; } } -#if BATCHED_BRIDGE - NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls]; NSNumber *currentExecutorID = RCTGetExecutorID(_javaScriptExecutor); calls = [calls filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *call, NSDictionary *bindings) { @@ -1393,67 +1544,41 @@ static id _latestJSExecutor; context:RCTGetExecutorID(_javaScriptExecutor)]; } -#endif - RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink { + RCTAssertMainThread(); + RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); } -- (void)addFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers addObject:observer]; -} - -- (void)removeFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers removeObject:observer]; -} - -- (void)reload -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (!_loading) { - // If the bridge has not loaded yet, the context will be already invalid at - // the time the javascript gets executed. - // It will crash the javascript, and even the next `load` won't render. - [self invalidate]; - [self setUp]; - } - }); -} - -+ (void)logMessage:(NSString *)message level:(NSString *)level -{ - if (![_latestJSExecutor isValid]) { - return; - } - - // Note: the js executor could get invalidated while we're trying to call - // this...need to watch out for that. - [_latestJSExecutor executeJSCall:@"RCTLog" - method:@"logIfNoNativeHook" - arguments:@[level, message] - context:RCTGetExecutorID(_latestJSExecutor) - callback:^(id json, NSError *error) {}]; -} - - (void)startProfiling { - if (![_bundleURL.scheme isEqualToString:@"http"]) { + RCTAssertMainThread(); + + if (![_parentBridge.bundleURL.scheme isEqualToString:@"http"]) { RCTLogError(@"To run the profiler you must be running from the dev server"); return; } + + [_mainDisplayLink invalidate]; + _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; + [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + RCTProfileInit(); } - (void)stopProfiling { + RCTAssertMainThread(); + + [_mainDisplayLink invalidate]; + NSString *log = RCTProfileEnd(); - NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", _bundleURL.scheme, _bundleURL.host, _bundleURL.port]; + NSURL *bundleURL = _parentBridge.bundleURL; + NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; NSURL *URL = [NSURL URLWithString:URLString]; NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; URLRequest.HTTPMethod = @"POST"; diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index c6063caa2..6c64e4677 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -123,10 +123,12 @@ RCT_EXPORT_MODULE() { _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"]); + dispatch_async(dispatch_get_main_queue(), ^{ + 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 @@ -147,10 +149,12 @@ RCT_EXPORT_MODULE() _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; + dispatch_async(dispatch_get_main_queue(), ^{ + // Hit these setters again after bridge has finished loading + self.profilingEnabled = _profilingEnabled; + self.liveReloadEnabled = _liveReloadEnabled; + self.executorClass = _executorClass; + }); } - (void)dealloc diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h index 8d52529e6..01f51d7e9 100755 --- a/React/Base/RCTJavaScriptLoader.h +++ b/React/Base/RCTJavaScriptLoader.h @@ -22,6 +22,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; -- (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(RCTJavaScriptCompleteBlock)onComplete; +- (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(void (^)(NSError *, NSString *))onComplete; @end diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index 2e7d21b94..0210986dc 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -27,7 +27,7 @@ return self; } -- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete +- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *, NSString *))onComplete { // Sanitize the script URL scriptURL = [RCTConvert NSURL:scriptURL.absoluteString]; @@ -37,7 +37,7 @@ NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{ NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided" }]; - onComplete(error); + onComplete(error, nil); return; } @@ -57,7 +57,7 @@ code:error.code userInfo:userInfo]; } - onComplete(error); + onComplete(error, nil); return; } @@ -96,18 +96,10 @@ code:[(NSHTTPURLResponse *)response statusCode] userInfo:userInfo]; - onComplete(error); + onComplete(error, nil); return; } - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - sourceCodeModule.scriptURL = scriptURL; - sourceCodeModule.scriptText = rawText; - - [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { - dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(scriptError); - }); - }]; + onComplete(nil, rawText); }]; [task resume]; diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index ee5a35d7f..d55094c37 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -11,7 +11,7 @@ #import "RCTBridge.h" -@interface RCTRootView : UIView +@interface RCTRootView : UIView /** * - Designated initializer - diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 54556d418..9ee09d495 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -20,25 +20,37 @@ #import "RCTTouchHandler.h" #import "RCTUIManager.h" #import "RCTUtils.h" +#import "RCTView.h" #import "RCTWebViewExecutor.h" #import "UIView+React.h" +@interface RCTBridge (RCTRootView) + +@property (nonatomic, weak, readonly) RCTBridge *batchedBridge; + +@end + @interface RCTUIManager (RCTRootView) - (NSNumber *)allocateRootTag; @end +@interface RCTRootContentView : RCTView + +- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge; + +@end + @implementation RCTRootView { RCTBridge *_bridge; - RCTTouchHandler *_touchHandler; NSString *_moduleName; NSDictionary *_launchOptions; - UIView *_contentView; + RCTRootContentView *_contentView; } -- (instancetype)initWithBridge:(RCTBridge *)bridge + - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName { RCTAssert(bridge, @"A bridge instance is required to create an RCTRootView"); @@ -52,11 +64,11 @@ _moduleName = moduleName; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(bundleFinishedLoading) + selector:@selector(javaScriptDidLoad:) name:RCTJavaScriptDidLoadNotification object:_bridge]; - if (!_bridge.loading) { - [self bundleFinishedLoading]; + if (!_bridge.batchedBridge.isLoading) { + [self bundleFinishedLoading:_bridge.batchedBridge]; } } return self; @@ -73,25 +85,6 @@ return [self initWithBridge:bridge moduleName:moduleName]; } -- (BOOL)isValid -{ - return _contentView.userInteractionEnabled; -} - -- (void)invalidate -{ - _contentView.userInteractionEnabled = NO; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - if (_contentView) { - [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" - args:@[_contentView.reactTag]]; - } -} - - (UIViewController *)backingViewController { return _backingViewController ?: [super backingViewController]; @@ -105,9 +98,19 @@ RCT_IMPORT_METHOD(AppRegistry, runApplication) RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) -- (void)bundleFinishedLoading + +- (void)javaScriptDidLoad:(NSNotification *)notification +{ + RCTBridge *bridge = notification.userInfo[@"bridge"]; + [self bundleFinishedLoading:bridge]; +} + +- (void)bundleFinishedLoading:(RCTBridge *)bridge { dispatch_async(dispatch_get_main_queue(), ^{ + if (!bridge.isValid) { + return; + } /** * Every root view that is created must have a unique React tag. @@ -117,19 +120,16 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) * the React tag is assigned every time we load new content. */ [_contentView removeFromSuperview]; - _contentView = [[UIView alloc] initWithFrame:self.bounds]; - _contentView.reactTag = [_bridge.uiManager allocateRootTag]; - _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; - [_contentView addGestureRecognizer:_touchHandler]; + _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds + bridge:bridge]; [self addSubview:_contentView]; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": _contentView.reactTag, - @"initialProps": self.initialProperties ?: @{}, + @"initialProps": _initialProperties ?: @{}, }; - [_bridge.uiManager registerRootView:_contentView]; - [_bridge enqueueJSCall:@"AppRegistry.runApplication" + [bridge enqueueJSCall:@"AppRegistry.runApplication" args:@[moduleName, appParameters]]; }); } @@ -139,7 +139,6 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) [super layoutSubviews]; if (_contentView) { _contentView.frame = self.bounds; - [_bridge.uiManager setFrame:self.bounds forRootView:_contentView]; } } @@ -148,6 +147,12 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) return _contentView.reactTag; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_contentView removeFromSuperview]; +} + @end @implementation RCTUIManager (RCTRootView) @@ -160,3 +165,60 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) } @end + +@implementation RCTRootContentView +{ + __weak RCTBridge *_bridge; + RCTTouchHandler *_touchHandler; +} + +- (instancetype)initWithFrame:(CGRect)frame + bridge:(RCTBridge *)bridge +{ + if ((self = [super init])) { + _bridge = bridge; + [self setUp]; + self.frame = frame; + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + if (self.reactTag && _bridge.isValid) { + [_bridge.uiManager setFrame:self.bounds forRootView:self]; + } +} + +- (void)setUp +{ + /** + * Every root view that is created must have a unique react tag. + * Numbering of these tags goes from 1, 11, 21, 31, etc + * + * NOTE: Since the bridge persists, the RootViews might be reused, so now + * the react tag is assigned every time we load new content. + */ + self.reactTag = [_bridge.uiManager allocateRootTag]; + [self addGestureRecognizer:[[RCTTouchHandler alloc] initWithBridge:_bridge]]; + [_bridge.uiManager registerRootView:self]; +} + +- (BOOL)isValid +{ + return self.userInteractionEnabled; +} + +- (void)invalidate +{ + self.userInteractionEnabled = NO; +} + +- (void)dealloc +{ + [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" + args:@[self.reactTag]]; +} + +@end diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index e2df5befc..e21c9d16f 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -70,6 +70,7 @@ } @synthesize bridge = _bridge; +@synthesize paused = _paused; RCT_EXPORT_MODULE() @@ -78,7 +79,7 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) - (instancetype)init { if ((self = [super init])) { - + _paused = YES; _timers = [[RCTSparseArray alloc] init]; for (NSString *name in @[UIApplicationWillResignActiveNotification, @@ -126,7 +127,7 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) - (void)stopTimers { - [_bridge removeFrameUpdateObserver:self]; + _paused = YES; } - (void)startTimers @@ -135,7 +136,7 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) return; } - [_bridge addFrameUpdateObserver:self]; + _paused = NO; } - (void)didUpdateFrame:(RCTFrameUpdate *)update diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 496b9c351..d35cae03a 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -267,11 +267,6 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa return self; } -- (void)dealloc -{ - RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); -} - - (BOOL)isValid { return _viewRegistry != nil; @@ -279,20 +274,24 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa - (void)invalidate { - RCTAssertMainThread(); + /** + * Called on the JS Thread since all modules are invalidated on the JS thread + */ - for (NSNumber *rootViewTag in _rootViewTags) { - ((UIView *)_viewRegistry[rootViewTag]).userInteractionEnabled = NO; - } + dispatch_async(dispatch_get_main_queue(), ^{ + for (NSNumber *rootViewTag in _rootViewTags) { + ((UIView *)_viewRegistry[rootViewTag]).userInteractionEnabled = NO; + } - _rootViewTags = nil; - _shadowViewRegistry = nil; - _viewRegistry = nil; - _bridge = nil; + _rootViewTags = nil; + _shadowViewRegistry = nil; + _viewRegistry = nil; + _bridge = nil; - [_pendingUIBlocksLock lock]; - _pendingUIBlocks = nil; - [_pendingUIBlocksLock unlock]; + [_pendingUIBlocksLock lock]; + _pendingUIBlocks = nil; + [_pendingUIBlocksLock unlock]; + }); } - (void)setBridge:(RCTBridge *)bridge diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index a4cb338fb..f947a6128 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -264,9 +264,13 @@ NSInteger kNeverProgressed = -10000; NSInteger _numberOfViewControllerMovesToIgnore; } +@synthesize paused = _paused; + - (id)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithFrame:CGRectZero])) { + _paused = YES; + _bridge = bridge; _mostRecentProgress = kNeverProgressed; _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; @@ -341,14 +345,14 @@ NSInteger kNeverProgressed = -10000; _dummyView.frame = (CGRect){{destination}}; _currentlyTransitioningFrom = indexOfFrom; _currentlyTransitioningTo = indexOfTo; - [_bridge addFrameUpdateObserver:self]; + _paused = NO; } completion:^(id context) { [weakSelf freeLock]; _currentlyTransitioningFrom = 0; _currentlyTransitioningTo = 0; _dummyView.frame = CGRectZero; - [_bridge removeFrameUpdateObserver:self]; + _paused = YES; // Reset the parallel position tracker }]; } From 50de4a67b0c06a5a3f54c81fadf56b34fe8ccb52 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Mon, 4 May 2015 14:33:05 -0700 Subject: [PATCH 35/51] [ReactNative] Navigator use flattenStyle Summary: A hack was implemented in Navigator to do this manually, but there is a proper function to get these styles for setting native props @public Test Plan: Testing Navigator behavior on iOS and Android --- .../CustomComponents/Navigator/Navigator.js | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 6d09b656e..26c6a46bd 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -40,13 +40,13 @@ var Platform = require('Platform'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); -var StyleSheetRegistry = require('StyleSheetRegistry'); var Subscribable = require('Subscribable'); var TimerMixin = require('react-timer-mixin'); var View = require('View'); -var getNavigatorContext = require('getNavigatorContext'); var clamp = require('clamp'); +var flattenStyle = require('flattenStyle'); +var getNavigatorContext = require('getNavigatorContext'); var invariant = require('invariant'); var keyMirror = require('keyMirror'); var merge = require('merge'); @@ -67,24 +67,6 @@ function getuid() { return __uid++; } -function resolveStyle(styles) { - // The styles for a scene are a prop in the style format, which can be an object, - // a number (which refers to the StyleSheetRegistry), or an arry of objects/numbers. - // This function resolves the actual style values so we can call setNativeProps with - // matching styles. - var resolvedStyle = {}; - if (!Array.isArray(styles)) { - styles = [styles]; - } - styles.forEach((style) => { - if (typeof style === 'number') { - style = StyleSheetRegistry.getStyleByID(style); - } - resolvedStyle = merge(resolvedStyle, style); - }); - return resolvedStyle; -} - // styles moved to the top of the file so getDefaultProps can refer to it var styles = StyleSheet.create({ container: { @@ -698,7 +680,7 @@ var Navigator = React.createClass({ */ _enableScene: function(sceneIndex) { // First, determine what the defined styles are for scenes in this navigator - var sceneStyle = resolveStyle(this.props.sceneStyle); + var sceneStyle = flattenStyle(this.props.sceneStyle); // Then restore the left value for this scene var enabledSceneNativeProps = { left: sceneStyle.left, From 66d2f600dddf724bfd84d6eb2d35ee8622f60446 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 4 May 2015 18:34:29 -0700 Subject: [PATCH 36/51] [ReactNative] improve console logging a little bit --- Libraries/Utilities/stringifySafe.js | 11 ++++++-- .../haste/polyfills/console.js | 27 ++++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Libraries/Utilities/stringifySafe.js b/Libraries/Utilities/stringifySafe.js index 053ea6984..750a932b6 100644 --- a/Libraries/Utilities/stringifySafe.js +++ b/Libraries/Utilities/stringifySafe.js @@ -17,12 +17,19 @@ */ function stringifySafe(arg: any): string { var ret; + var type = typeof arg; if (arg === undefined) { ret = 'undefined'; } else if (arg === null) { ret = 'null'; - } else if (typeof arg === 'string') { + } else if (type === 'string') { ret = '"' + arg + '"'; + } else if (type === 'function') { + try { + ret = arg.toString(); + } catch (e) { + ret = '[function unknown]'; + } } else { // Perform a try catch, just in case the object has a circular // reference or stringify throws for some other reason. @@ -36,7 +43,7 @@ function stringifySafe(arg: any): string { } } } - return ret || '["' + typeof arg + '" failed to stringify]'; + return ret || '["' + type + '" failed to stringify]'; } module.exports = stringifySafe; diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js index 91fb970f8..575769612 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js @@ -35,25 +35,34 @@ function getNativeLogFunction(level) { return function() { var str = Array.prototype.map.call(arguments, function(arg) { - if (arg == null) { - return arg === null ? 'null' : 'undefined'; - } else if (typeof arg === 'string') { - return '"' + arg + '"'; + var ret; + var type = typeof arg; + if (arg === null) { + ret = 'null'; + } else if (arg === undefined) { + ret = 'undefined'; + } else if (type === 'string') { + ret = '"' + arg + '"'; + } else if (type === 'function') { + try { + ret = arg.toString(); + } catch (e) { + ret = '[function unknown]'; + } } else { // Perform a try catch, just in case the object has a circular // reference or stringify throws for some other reason. try { - return JSON.stringify(arg); + ret = JSON.stringify(arg); } catch (e) { if (typeof arg.toString === 'function') { try { - return arg.toString(); - } catch (E) { - return 'unknown'; - } + ret = arg.toString(); + } catch (E) {} } } } + return ret || '["' + type + '" failed to stringify]'; }).join(', '); global.nativeLoggingHook(str, level); }; From af921542b5f0be2a96f169cf397c530e4968a3f8 Mon Sep 17 00:00:00 2001 From: Andrew Rasmussen Date: Mon, 4 May 2015 19:59:25 -0700 Subject: [PATCH 37/51] [ReactNative] update Layout Summary: I made some changes to css-layout that changes how layout is computed for absolutely positioned nodes inside absolutely positioned parents that have borders/padding. There were also some other changes made to css-layout that haven't been merged in yet. @public Test Plan: Made a node as described above, saw that the layout is computed differently than in the browser, updated Layout, saw that the Layout is not computed correctly. --- React/Layout/Layout.c | 234 +++++++++++++++++++++++++++-------------- React/Layout/Layout.h | 18 ++-- React/Layout/import.sh | 15 +++ 3 files changed, 175 insertions(+), 92 deletions(-) create mode 100755 React/Layout/import.sh diff --git a/React/Layout/Layout.c b/React/Layout/Layout.c index 2b168e44a..50d69b522 100644 --- a/React/Layout/Layout.c +++ b/React/Layout/Layout.c @@ -1,21 +1,15 @@ /** - * @generated SignedSource<<24fa633b4dd81b7fb40c2b2b0b7c97d0>> - * - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * !! This file is a check-in from github! !! - * !! !! - * !! You should not modify this file directly. Instead: !! - * !! 1) Go to https://github.com/facebook/css-layout !! - * !! 2) Make a pull request and get it merged !! - * !! 3) Execute ./import.sh to pull in the latest version !! - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * * Copyright (c) 2014, 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. + * + * WARNING: You should not modify this file directly. Instead: + * 1) Go to https://github.com/facebook/css-layout + * 2) Make a pull request and get it merged + * 3) Run import.sh to copy Layout.* to react-native-github */ #include @@ -44,6 +38,12 @@ void init_css_node(css_node_t *node) { node->style.dimensions[CSS_WIDTH] = CSS_UNDEFINED; node->style.dimensions[CSS_HEIGHT] = CSS_UNDEFINED; + node->style.minDimensions[CSS_WIDTH] = CSS_UNDEFINED; + node->style.minDimensions[CSS_HEIGHT] = CSS_UNDEFINED; + + node->style.maxDimensions[CSS_WIDTH] = CSS_UNDEFINED; + node->style.maxDimensions[CSS_HEIGHT] = CSS_UNDEFINED; + node->style.position[CSS_LEFT] = CSS_UNDEFINED; node->style.position[CSS_TOP] = CSS_UNDEFINED; node->style.position[CSS_RIGHT] = CSS_UNDEFINED; @@ -249,6 +249,10 @@ static float getPaddingAndBorder(css_node_t *node, int location) { return getPadding(node, location) + getBorder(node, location); } +static float getBorderAxis(css_node_t *node, css_flex_direction_t axis) { + return getBorder(node, leading[axis]) + getBorder(node, trailing[axis]); +} + static float getMarginAxis(css_node_t *node, css_flex_direction_t axis) { return getMargin(node, leading[axis]) + getMargin(node, trailing[axis]); } @@ -298,7 +302,8 @@ static float getDimWithMargin(css_node_t *node, css_flex_direction_t axis) { } static bool isDimDefined(css_node_t *node, css_flex_direction_t axis) { - return !isUndefined(node->style.dimensions[dim[axis]]); + float value = node->style.dimensions[dim[axis]]; + return !isUndefined(value) && value > 0.0; } static bool isPosDefined(css_node_t *node, css_position_t position) { @@ -317,6 +322,30 @@ static float getPosition(css_node_t *node, css_position_t position) { return 0; } +static float boundAxis(css_node_t *node, css_flex_direction_t axis, float value) { + float min = CSS_UNDEFINED; + float max = CSS_UNDEFINED; + + if (axis == CSS_FLEX_DIRECTION_COLUMN) { + min = node->style.minDimensions[CSS_HEIGHT]; + max = node->style.maxDimensions[CSS_HEIGHT]; + } else if (axis == CSS_FLEX_DIRECTION_ROW) { + min = node->style.minDimensions[CSS_WIDTH]; + max = node->style.maxDimensions[CSS_WIDTH]; + } + + float boundValue = value; + + if (!isUndefined(max) && max >= 0.0 && boundValue > max) { + boundValue = max; + } + if (!isUndefined(min) && min >= 0.0 && boundValue < min) { + boundValue = min; + } + + return boundValue; +} + // When the user specifically sets a value for width or height static void setDimensionFromStyle(css_node_t *node, css_flex_direction_t axis) { // The parent already computed us a width or height. We just skip it @@ -330,7 +359,7 @@ static void setDimensionFromStyle(css_node_t *node, css_flex_direction_t axis) { // The dimensions can never be smaller than the padding and border node->layout.dimensions[dim[axis]] = fmaxf( - node->style.dimensions[dim[axis]], + boundAxis(node, axis, node->style.dimensions[dim[axis]]), getPaddingAndBorderAxis(node, axis) ); } @@ -347,6 +376,7 @@ static float getRelativePosition(css_node_t *node, css_flex_direction_t axis) { static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { /** START_GENERATED **/ + css_flex_direction_t mainAxis = getFlexDirection(node); css_flex_direction_t crossAxis = mainAxis == CSS_FLEX_DIRECTION_ROW ? CSS_FLEX_DIRECTION_COLUMN : @@ -385,25 +415,31 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // Let's not measure the text if we already know both dimensions if (isRowUndefined || isColumnUndefined) { - css_dim_t measure_dim = node->measure( + css_dim_t measureDim = node->measure( node->context, + width ); if (isRowUndefined) { - node->layout.dimensions[CSS_WIDTH] = measure_dim.dimensions[CSS_WIDTH] + + node->layout.dimensions[CSS_WIDTH] = measureDim.dimensions[CSS_WIDTH] + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); } if (isColumnUndefined) { - node->layout.dimensions[CSS_HEIGHT] = measure_dim.dimensions[CSS_HEIGHT] + + node->layout.dimensions[CSS_HEIGHT] = measureDim.dimensions[CSS_HEIGHT] + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); } } return; } + int i; + int ii; + css_node_t* child; + css_flex_direction_t axis; + // Pre-fill some dimensions straight from the parent - for (int i = 0; i < node->children_count; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = 0; i < node->children_count; ++i) { + child = node->get_child(node->context, i); // Pre-fill cross axis dimensions when the child is using stretch before // we call the recursive layout pass if (getAlignItem(node, child) == CSS_ALIGN_STRETCH && @@ -411,27 +447,27 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { !isUndefined(node->layout.dimensions[dim[crossAxis]]) && !isDimDefined(child, crossAxis)) { child->layout.dimensions[dim[crossAxis]] = fmaxf( - node->layout.dimensions[dim[crossAxis]] - + boundAxis(child, crossAxis, node->layout.dimensions[dim[crossAxis]] - getPaddingAndBorderAxis(node, crossAxis) - - getMarginAxis(child, crossAxis), + getMarginAxis(child, crossAxis)), // You never want to go smaller than padding getPaddingAndBorderAxis(child, crossAxis) ); } else if (getPositionType(child) == CSS_POSITION_ABSOLUTE) { // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both // left and right or top and bottom). - for (int ii = 0; ii < 2; ii++) { - css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (!isUndefined(node->layout.dimensions[dim[axis]]) && !isDimDefined(child, axis) && isPosDefined(child, leading[axis]) && isPosDefined(child, trailing[axis])) { child->layout.dimensions[dim[axis]] = fmaxf( - node->layout.dimensions[dim[axis]] - - getPaddingAndBorderAxis(node, axis) - - getMarginAxis(child, axis) - - getPosition(child, leading[axis]) - - getPosition(child, trailing[axis]), + boundAxis(child, axis, node->layout.dimensions[dim[axis]] - + getPaddingAndBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, leading[axis]) - + getPosition(child, trailing[axis])), // You never want to go smaller than padding getPaddingAndBorderAxis(child, axis) ); @@ -449,11 +485,12 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // We want to execute the next two loops one per line with flex-wrap int startLine = 0; int endLine = 0; - int nextLine = 0; + // int nextOffset = 0; + int alreadyComputedNextLayout = 0; // We aggregate the total dimensions of the container in those two variables float linesCrossDim = 0; float linesMainDim = 0; - while (endLine != node->children_count) { + while (endLine < node->children_count) { // Layout non flexible children and count children by type // mainContentDim is accumulation of the dimensions and margin of all the @@ -467,8 +504,10 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { int flexibleChildrenCount = 0; float totalFlexible = 0; int nonFlexibleChildrenCount = 0; - for (int i = startLine; i < node->children_count; ++i) { - css_node_t* child = node->get_child(node->context, i); + + float maxWidth; + for (i = startLine; i < node->children_count; ++i) { + child = node->get_child(node->context, i); float nextContentDim = 0; // It only makes sense to consider a child flexible if we have a computed @@ -478,26 +517,27 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { totalFlexible += getFlex(child); // Even if we don't know its exact size yet, we already know the padding, - // border and margin. We'll use this partial information to compute the - // remaining space. + // border and margin. We'll use this partial information, which represents + // the smallest possible size for the child, to compute the remaining + // available space. nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + getMarginAxis(child, mainAxis); } else { - float maxWidth = CSS_UNDEFINED; - if (mainAxis == CSS_FLEX_DIRECTION_ROW) { - // do nothing - } else if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { - maxWidth = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); - } else { + maxWidth = CSS_UNDEFINED; + if (mainAxis != CSS_FLEX_DIRECTION_ROW) { maxWidth = parentMaxWidth - getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); + + if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { + maxWidth = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); + } } // This is the main recursive call. We layout non flexible children. - if (nextLine == 0) { + if (alreadyComputedNextLayout == 0) { layoutNode(child, maxWidth); } @@ -513,11 +553,14 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // The element we are about to add would make us go to the next line if (isFlexWrap(node) && !isUndefined(node->layout.dimensions[dim[mainAxis]]) && - mainContentDim + nextContentDim > definedMainDim) { - nextLine = i + 1; + mainContentDim + nextContentDim > definedMainDim && + // If there's only one element, then it's bigger than the content + // and needs its own line + i != startLine) { + alreadyComputedNextLayout = 1; break; } - nextLine = 0; + alreadyComputedNextLayout = 0; mainContentDim += nextContentDim; endLine = i + 1; } @@ -542,6 +585,26 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // remaining space if (flexibleChildrenCount != 0) { float flexibleMainDim = remainingMainDim / totalFlexible; + float baseMainDim; + float boundMainDim; + + // Iterate over every child in the axis. If the flex share of remaining + // space doesn't meet min/max bounds, remove this child from flex + // calculations. + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); + if (isFlex(child)) { + baseMainDim = flexibleMainDim * getFlex(child) + + getPaddingAndBorderAxis(child, mainAxis); + boundMainDim = boundAxis(child, mainAxis, baseMainDim); + + if (baseMainDim != boundMainDim) { + remainingMainDim -= boundMainDim; + totalFlexible -= getFlex(child); + } + } + } + flexibleMainDim = remainingMainDim / totalFlexible; // The non flexible children can overflow the container, in this case // we should just assume that there is no space available. @@ -551,21 +614,20 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // We iterate over the full array and only apply the action on flexible // children. This is faster than actually allocating a new array that // contains only flexible children. - for (int i = startLine; i < endLine; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); if (isFlex(child)) { // At this point we know the final size of the element in the main // dimension - child->layout.dimensions[dim[mainAxis]] = flexibleMainDim * getFlex(child) + - getPaddingAndBorderAxis(child, mainAxis); + child->layout.dimensions[dim[mainAxis]] = boundAxis(child, mainAxis, + flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) + ); - float maxWidth = CSS_UNDEFINED; - if (mainAxis == CSS_FLEX_DIRECTION_ROW) { - // do nothing - } else if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { + maxWidth = CSS_UNDEFINED; + if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { maxWidth = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); - } else { + } else if (mainAxis != CSS_FLEX_DIRECTION_ROW) { maxWidth = parentMaxWidth - getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); @@ -580,9 +642,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // space available } else { css_justify_t justifyContent = getJustifyContent(node); - if (justifyContent == CSS_JUSTIFY_FLEX_START) { - // Do nothing - } else if (justifyContent == CSS_JUSTIFY_CENTER) { + if (justifyContent == CSS_JUSTIFY_CENTER) { leadingMainDim = remainingMainDim / 2; } else if (justifyContent == CSS_JUSTIFY_FLEX_END) { leadingMainDim = remainingMainDim; @@ -612,8 +672,8 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { float mainDim = leadingMainDim + getPaddingAndBorder(node, leading[mainAxis]); - for (int i = startLine; i < endLine; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); if (getPositionType(child) == CSS_POSITION_ABSOLUTE && isPosDefined(child, leading[mainAxis])) { @@ -638,25 +698,38 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); // The cross dimension is the max of the elements dimension since there // can only be one element in that cross dimension. - crossDim = fmaxf(crossDim, getDimWithMargin(child, crossAxis)); + crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); } } + float containerMainAxis = node->layout.dimensions[dim[mainAxis]]; + // If the user didn't specify a width or height, and it has not been set + // by the container, then we set it via the children. + if (isUndefined(containerMainAxis)) { + containerMainAxis = fmaxf( + // We're missing the last padding at this point to get the final + // dimension + boundAxis(node, mainAxis, mainDim + getPaddingAndBorder(node, trailing[mainAxis])), + // We can never assign a width smaller than the padding and borders + getPaddingAndBorderAxis(node, mainAxis) + ); + } + float containerCrossAxis = node->layout.dimensions[dim[crossAxis]]; if (isUndefined(node->layout.dimensions[dim[crossAxis]])) { containerCrossAxis = fmaxf( // For the cross dim, we add both sides at the end because the value // is aggregate via a max function. Intermediate negative values // can mess this computation otherwise - crossDim + getPaddingAndBorderAxis(node, crossAxis), + boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), getPaddingAndBorderAxis(node, crossAxis) ); } // Position elements in the cross axis - for (int i = startLine; i < endLine; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); if (getPositionType(child) == CSS_POSITION_ABSOLUTE && isPosDefined(child, leading[crossAxis])) { @@ -674,21 +747,19 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // alignSelf (child) in order to determine the position in the cross axis if (getPositionType(child) == CSS_POSITION_RELATIVE) { css_align_t alignItem = getAlignItem(node, child); - if (alignItem == CSS_ALIGN_FLEX_START) { - // Do nothing - } else if (alignItem == CSS_ALIGN_STRETCH) { + if (alignItem == CSS_ALIGN_STRETCH) { // You can only stretch if the dimension has not already been set // previously. if (!isDimDefined(child, crossAxis)) { child->layout.dimensions[dim[crossAxis]] = fmaxf( - containerCrossAxis - + boundAxis(child, crossAxis, containerCrossAxis - getPaddingAndBorderAxis(node, crossAxis) - - getMarginAxis(child, crossAxis), + getMarginAxis(child, crossAxis)), // You never want to go smaller than padding getPaddingAndBorderAxis(child, crossAxis) ); } - } else { + } else if (alignItem != CSS_ALIGN_FLEX_START) { // The remaining space between the parent dimensions+padding and child // dimensions+margin. float remainingCrossDim = containerCrossAxis - @@ -719,7 +790,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { node->layout.dimensions[dim[mainAxis]] = fmaxf( // We're missing the last padding at this point to get the final // dimension - linesMainDim + getPaddingAndBorder(node, trailing[mainAxis]), + boundAxis(node, mainAxis, linesMainDim + getPaddingAndBorder(node, trailing[mainAxis])), // We can never assign a width smaller than the padding and borders getPaddingAndBorderAxis(node, mainAxis) ); @@ -730,37 +801,38 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // For the cross dim, we add both sides at the end because the value // is aggregate via a max function. Intermediate negative values // can mess this computation otherwise - linesCrossDim + getPaddingAndBorderAxis(node, crossAxis), + boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), getPaddingAndBorderAxis(node, crossAxis) ); } // Calculate dimensions for absolutely positioned elements - for (int i = 0; i < node->children_count; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = 0; i < node->children_count; ++i) { + child = node->get_child(node->context, i); if (getPositionType(child) == CSS_POSITION_ABSOLUTE) { // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both // left and right or top and bottom). - for (int ii = 0; ii < 2; ii++) { - css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (!isUndefined(node->layout.dimensions[dim[axis]]) && !isDimDefined(child, axis) && isPosDefined(child, leading[axis]) && isPosDefined(child, trailing[axis])) { child->layout.dimensions[dim[axis]] = fmaxf( - node->layout.dimensions[dim[axis]] - - getPaddingAndBorderAxis(node, axis) - - getMarginAxis(child, axis) - - getPosition(child, leading[axis]) - - getPosition(child, trailing[axis]), + boundAxis(child, axis, node->layout.dimensions[dim[axis]] - + getBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, leading[axis]) - + getPosition(child, trailing[axis]) + ), // You never want to go smaller than padding getPaddingAndBorderAxis(child, axis) ); } } - for (int ii = 0; ii < 2; ii++) { - css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (isPosDefined(child, trailing[axis]) && !isPosDefined(child, leading[axis])) { child->layout.position[leading[axis]] = diff --git a/React/Layout/Layout.h b/React/Layout/Layout.h index 51f72493b..fe383ea57 100644 --- a/React/Layout/Layout.h +++ b/React/Layout/Layout.h @@ -1,21 +1,15 @@ /** - * @generated SignedSource<<58298c7a8815a8675e970b0347dedfed>> - * - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * !! This file is a check-in from github! !! - * !! !! - * !! You should not modify this file directly. Instead: !! - * !! 1) Go to https://github.com/facebook/css-layout !! - * !! 2) Make a pull request and get it merged !! - * !! 3) Execute ./import.sh to pull in the latest version !! - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * * Copyright (c) 2014, 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. + * + * WARNING: You should not modify this file directly. Instead: + * 1) Go to https://github.com/facebook/css-layout + * 2) Make a pull request and get it merged + * 3) Run import.sh to copy Layout.* to react-native-github */ #ifndef __LAYOUT_H @@ -113,6 +107,8 @@ typedef struct { float padding[4]; float border[4]; float dimensions[2]; + float minDimensions[2]; + float maxDimensions[2]; } css_style_t; typedef struct css_node { diff --git a/React/Layout/import.sh b/React/Layout/import.sh new file mode 100755 index 000000000..7e69403f3 --- /dev/null +++ b/React/Layout/import.sh @@ -0,0 +1,15 @@ +LAYOUT_C=`curl https://raw.githubusercontent.com/facebook/css-layout/master/src/Layout.c` +LAYOUT_H=`curl https://raw.githubusercontent.com/facebook/css-layout/master/src/Layout.h` + +REPLACE_STRING="* + * WARNING: You should not modify this file directly. Instead: + * 1) Go to https://github.com/facebook/css-layout + * 2) Make a pull request and get it merged + * 3) Run import.sh to copy Layout.* to react-native-github + */" + +LAYOUT_C=${LAYOUT_C/\*\//$REPLACE_STRING} +LAYOUT_H=${LAYOUT_H/\*\//$REPLACE_STRING} + +echo "$LAYOUT_C" > Layout.c +echo "$LAYOUT_H" > Layout.h From d713a711c423a71ed004e54b1b779fb753baab5e Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Tue, 5 May 2015 02:31:20 -0700 Subject: [PATCH 38/51] [ReactNative] Fix packager assets --- React/Base/RCTBridge.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 349cc725e..35e85b3c9 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -941,6 +941,11 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me return self; } +- (NSURL *)bundleURL +{ + return _parentBridge.bundleURL; +} + - (NSDictionary *)launchOptions { return _parentBridge.launchOptions; From 4402c4c885d81c5d5a9a0773afce3e1fa0ae1394 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Tue, 5 May 2015 02:51:17 -0700 Subject: [PATCH 39/51] [react_native] JS files from D2038965: Extract view specific commands from UIManager. --- Libraries/Components/ScrollView/ScrollView.js | 18 +++++++++++++----- .../Components/WebView/WebView.android.js | 18 +++++++++++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 76419bce0..f8dc3bcba 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -211,11 +211,19 @@ var ScrollView = React.createClass({ }, scrollTo: function(destY?: number, destX?: number) { - RCTUIManager.scrollTo( - this.getNodeHandle(), - destX || 0, - destY || 0 - ); + if (Platform.OS === 'android') { + RCTUIManager.dispatchViewManagerCommand( + this.getNodeHandle(), + RCTUIManager.RCTScrollView.Commands.scrollTo, + [destX || 0, destY || 0] + ); + } else { + RCTUIManager.scrollTo( + this.getNodeHandle(), + destX || 0, + destY || 0 + ); + } }, scrollWithoutAnimationTo: function(destY?: number, destX?: number) { diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 959422bbc..79ded6506 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -109,15 +109,27 @@ var WebView = React.createClass({ }, goForward: function() { - RCTUIManager.webViewGoForward(this.getWebWiewHandle()); + RCTUIManager.dispatchViewManagerCommand( + this.getWebWiewHandle(), + RCTUIManager.RCTWebView.Commands.goForward, + null + ); }, goBack: function() { - RCTUIManager.webViewGoBack(this.getWebWiewHandle()); + RCTUIManager.dispatchViewManagerCommand( + this.getWebWiewHandle(), + RCTUIManager.RCTWebView.Commands.goBack, + null + ); }, reload: function() { - RCTUIManager.webViewReload(this.getWebWiewHandle()); + RCTUIManager.dispatchViewManagerCommand( + this.getWebWiewHandle(), + RCTUIManager.RCTWebView.Commands.reload, + null + ); }, /** From 65a3da300349fa74308be786fc27c5dee89d8ca4 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Tue, 5 May 2015 05:15:53 -0700 Subject: [PATCH 40/51] [ReactNative] Fix chrome debugger --- React/Base/RCTBridge.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 35e85b3c9..a7c6a30a6 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1108,7 +1108,8 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me * Register the display link to start sending js calls after everything * is setup */ - [_jsDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTContextExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop]; + [_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification object:_parentBridge From 5eca2e1d3cf42f40c648726f0ba7a2a621ab0127 Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Tue, 5 May 2015 05:34:38 -0700 Subject: [PATCH 41/51] [React Native] Added RCTSettings --- .../UIExplorer.xcodeproj/project.pbxproj | 28 ++ .../RCTSettings.xcodeproj/project.pbxproj | 250 ++++++++++++++++++ Libraries/Settings/RCTSettingsManager.h | 18 ++ Libraries/Settings/RCTSettingsManager.m | 100 +++++++ Libraries/Settings/Settings.android.js | 34 +++ Libraries/Settings/Settings.ios.js | 77 ++++++ React/Base/RCTConvert.h | 6 + React/Base/RCTConvert.m | 46 ++++ 8 files changed, 559 insertions(+) create mode 100644 Libraries/Settings/RCTSettings.xcodeproj/project.pbxproj create mode 100644 Libraries/Settings/RCTSettingsManager.h create mode 100644 Libraries/Settings/RCTSettingsManager.m create mode 100644 Libraries/Settings/Settings.android.js create mode 100644 Libraries/Settings/Settings.ios.js diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index cf9440c05..0d16e33e3 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -103,6 +103,13 @@ remoteGlobalIDString = 580C376F1AB104AF0015E709; remoteInfo = RCTTest; }; + 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; @@ -129,6 +136,7 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = UIExplorer/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = UIExplorer/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = UIExplorer/main.m; sourceTree = ""; }; + 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = ../../Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj; sourceTree = ""; }; 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; @@ -200,6 +208,7 @@ 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */, 13417FE31AA91428003F314A /* RCTImage.xcodeproj */, 134180261AA91779003F314A /* RCTNetwork.xcodeproj */, + 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */, 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */, 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */, 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */, @@ -293,6 +302,14 @@ name = Products; sourceTree = ""; }; + 834C36CE1AF8DA610019C93C /* Products */ = { + isa = PBXGroup; + children = ( + 834C36D21AF8DA610019C93C /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( @@ -411,6 +428,10 @@ ProductGroup = 14DC67E81AB71876001358AB /* Products */; ProjectRef = 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */; }, + { + ProductGroup = 834C36CE1AF8DA610019C93C /* Products */; + ProjectRef = 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */; + }, { ProductGroup = 58005BE51ABA80530062E044 /* Products */; ProjectRef = 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */; @@ -511,6 +532,13 @@ remoteRef = 58005BED1ABA80530062E044 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 834C36D21AF8DA610019C93C /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Libraries/Settings/RCTSettings.xcodeproj/project.pbxproj b/Libraries/Settings/RCTSettings.xcodeproj/project.pbxproj new file mode 100644 index 000000000..e4a210466 --- /dev/null +++ b/Libraries/Settings/RCTSettings.xcodeproj/project.pbxproj @@ -0,0 +1,250 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13DBA45E1AEE749000A17CF8 /* RCTSettingsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DBA45D1AEE749000A17CF8 /* RCTSettingsManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libRCTSettings.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTSettings.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 13DBA45C1AEE749000A17CF8 /* RCTSettingsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSettingsManager.h; sourceTree = ""; }; + 13DBA45D1AEE749000A17CF8 /* RCTSettingsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSettingsManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + 13DBA45C1AEE749000A17CF8 /* RCTSettingsManager.h */, + 13DBA45D1AEE749000A17CF8 /* RCTSettingsManager.m */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RCTSettings */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTSettings" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTSettings; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRCTSettings.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTSettings" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RCTSettings */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13DBA45E1AEE749000A17CF8 /* RCTSettingsManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTSettings; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTSettings; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTSettings" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTSettings" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/Libraries/Settings/RCTSettingsManager.h b/Libraries/Settings/RCTSettingsManager.h new file mode 100644 index 000000000..274cc69ae --- /dev/null +++ b/Libraries/Settings/RCTSettingsManager.h @@ -0,0 +1,18 @@ +/** + * 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 + +#import "RCTBridgeModule.h" + +@interface RCTSettingsManager : NSObject + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Libraries/Settings/RCTSettingsManager.m b/Libraries/Settings/RCTSettingsManager.m new file mode 100644 index 000000000..b17439eaf --- /dev/null +++ b/Libraries/Settings/RCTSettingsManager.m @@ -0,0 +1,100 @@ +/** + * 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 "RCTSettingsManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" + +@implementation RCTSettingsManager +{ + BOOL _ignoringUpdates; + NSUserDefaults *_defaults; +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (instancetype)init +{ + return [self initWithUserDefaults:[NSUserDefaults standardUserDefaults]]; +} + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults +{ + if ((self = [super init])) { + _defaults = defaults; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(userDefaultsDidChange:) + name:NSUserDefaultsDidChangeNotification + object:_defaults]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)userDefaultsDidChange:(NSNotification *)note +{ + if (_ignoringUpdates) { + return; + } + + [_bridge.eventDispatcher sendDeviceEventWithName:@"settingsUpdated" body:[_defaults dictionaryRepresentation]]; +} + +- (NSDictionary *)constantsToExport +{ + return @{ + @"settings": [_defaults dictionaryRepresentation] + }; +} + +/** + * Set one or more values in the settings. + * TODO: would it be useful to have a callback for when this has completed? + */ +RCT_EXPORT_METHOD(setValues:(NSDictionary *)values) +{ + _ignoringUpdates = YES; + [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id json, BOOL *stop) { + id plist = [RCTConvert NSPropertyList:json]; + if (plist) { + [_defaults setObject:plist forKey:key]; + } else { + [_defaults removeObjectForKey:key]; + } + }]; + + [_defaults synchronize]; + _ignoringUpdates = NO; +} + +/** + * Remove some values from the settings. + */ +RCT_EXPORT_METHOD(deleteValues:(NSStringArray *)keys) +{ + _ignoringUpdates = YES; + for (NSString *key in keys) { + [_defaults removeObjectForKey:key]; + } + + [_defaults synchronize]; + _ignoringUpdates = NO; +} + +@end diff --git a/Libraries/Settings/Settings.android.js b/Libraries/Settings/Settings.android.js new file mode 100644 index 000000000..d13a32ba9 --- /dev/null +++ b/Libraries/Settings/Settings.android.js @@ -0,0 +1,34 @@ +/** + * 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. + * + * @providesModule Settings + * @flow + */ +'use strict'; + +var Settings = { + get(key: string): mixed { + console.warn('Settings is not yet supported on Android'); + return null; + }, + + set(settings: Object) { + console.warn('Settings is not yet supported on Android'); + }, + + watchKeys(keys: string | Array, callback: Function): number { + console.warn('Settings is not yet supported on Android'); + return -1; + }, + + clearWatch(watchId: number) { + console.warn('Settings is not yet supported on Android'); + }, +}; + +module.exports = Settings; diff --git a/Libraries/Settings/Settings.ios.js b/Libraries/Settings/Settings.ios.js new file mode 100644 index 000000000..c9836f010 --- /dev/null +++ b/Libraries/Settings/Settings.ios.js @@ -0,0 +1,77 @@ +/** + * 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. + * + * @providesModule Settings + * @flow + */ +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTSettingsManager = require('NativeModules').SettingsManager; + +var invariant = require('invariant'); + +var subscriptions: Array<{keys: Array; callback: ?Function}> = []; + +var Settings = { + _settings: RCTSettingsManager.settings, + + get(key: string): mixed { + return this._settings[key]; + }, + + set(settings: Object) { + this._settings = merge(this._settings, settings); + RCTSettingsManager.set(settings); + }, + + watchKeys(keys: string | Array, callback: Function): number { + if (typeof keys == 'string') { + keys = [keys]; + } + + invariant( + Array.isArray(keys), + 'keys should be a string or array of strings' + ); + + var sid = subscriptions.length; + subscriptions.push({keys: keys, callback: callback}) + return sid; + }, + + clearWatch(watchId: number) { + if (watchId < subscriptions.length) { + subscriptions[watchId] = {keys: [], callback: null}; + } + }, + + _sendObservations(body: Object) { + var $this = this; + Object.keys(body).forEach((key) => { + var newValue = body[key]; + var didChange = $this._settings[key] !== newValue; + $this._settings[key] = newValue; + + if (didChange) { + subscriptions.forEach((sub) => { + if (~sub.keys.indexOf(key) && sub.callback) { + sub.callback(); + } + }); + } + }); + }, +}; + +RCTDeviceEventEmitter.addListener( + 'settingsUpdated', + Settings._sendObservations, +); + +module.exports = Settings; diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index eb81c167c..71bf7b302 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -101,6 +101,12 @@ typedef NSArray UIColorArray; typedef NSArray CGColorArray; + (CGColorArray *)CGColorArray:(id)json; +/** + * Convert a JSON object to a Plist-safe equivalent by stripping null values. + */ +typedef id NSPropertyList; ++ (NSPropertyList)NSPropertyList:(id)json; + typedef BOOL css_overflow; + (css_overflow)css_overflow:(id)json; + (css_flex_direction_t)css_flex_direction_t:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 5802a80f6..a5d2239af 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -828,6 +828,52 @@ RCT_ARRAY_CONVERTER(UIColor) return colors; } +static id RCTConvertPropertyListValue(id json) +{ + if (!json || json == (id)kCFNull) { + return nil; + } else if ([json isKindOfClass:[NSDictionary class]]) { + __block BOOL copy = NO; + NSMutableDictionary *values = [[NSMutableDictionary alloc] initWithCapacity:[json count]]; + [json enumerateKeysAndObjectsUsingBlock:^(NSString *key, id jsonValue, BOOL *stop) { + id value = RCTConvertPropertyListValue(jsonValue); + if (value) { + values[key] = value; + } + copy |= value != jsonValue; + }]; + return copy ? values : json; + } else if ([json isKindOfClass:[NSArray class]]) { + __block BOOL copy = NO; + __block NSArray *values = json; + [json enumerateObjectsUsingBlock:^(id jsonValue, NSUInteger idx, BOOL *stop) { + id value = RCTConvertPropertyListValue(jsonValue); + if (copy) { + if (value) { + [(NSMutableArray *)values addObject:value]; + } + } else if (value != jsonValue) { + // Converted value is different, so we'll need to copy the array + values = [[NSMutableArray alloc] initWithCapacity:values.count]; + for (NSInteger i = 0; i < idx; i++) { + [(NSMutableArray *)values addObject:json[i]]; + } + [(NSMutableArray *)values addObject:value]; + copy = YES; + } + }]; + return values; + } else { + // All other JSON types are supported by property lists + return json; + } +} + ++ (NSPropertyList)NSPropertyList:(id)json +{ + return RCTConvertPropertyListValue(json); +} + RCT_ENUM_CONVERTER(css_overflow, (@{ @"hidden": @NO, @"visible": @YES From 1a17cceb17ea9eaa820b0ecbedb10266980ba1ce Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Tue, 5 May 2015 05:34:39 -0700 Subject: [PATCH 42/51] [React Native] Save UIExplorer search state --- .../UIExplorer/UIExplorer.xcodeproj/project.pbxproj | 2 ++ Examples/UIExplorer/UIExplorerList.js | 11 ++++++++++- Libraries/Settings/Settings.ios.js | 12 ++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 0d16e33e3..698fd3b3f 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; }; 14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14DC67F11AB71876001358AB /* libRCTPushNotification.a */; }; 58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58005BEE1ABA80530062E044 /* libRCTTest.a */; }; + 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; }; D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; /* End PBXBuildFile section */ @@ -156,6 +157,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */, 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, 00D2771A1AB8C3E100DC1E48 /* libRCTWebSocketDebugger.a in Frameworks */, 58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */, diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index a10d291bd..a24ec1a54 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -28,6 +28,7 @@ var { } = React; var { TestModule } = React.addons; +var Settings = require('Settings'); var createExamplePage = require('./createExamplePage'); @@ -114,9 +115,14 @@ class UIExplorerList extends React.Component { components: COMPONENTS, apis: APIS, }), + searchText: Settings.get('searchText'), }; } + componentDidMount() { + this._search(this.state.searchText); + } + render() { return ( @@ -128,6 +134,7 @@ class UIExplorerList extends React.Component { onChangeText={this._search.bind(this)} placeholder="Search..." style={styles.searchTextInput} + value={this.state.searchText} /> , callback: Function): number { @@ -52,11 +52,11 @@ var Settings = { }, _sendObservations(body: Object) { - var $this = this; + var _this = this; Object.keys(body).forEach((key) => { var newValue = body[key]; - var didChange = $this._settings[key] !== newValue; - $this._settings[key] = newValue; + var didChange = _this._settings[key] !== newValue; + _this._settings[key] = newValue; if (didChange) { subscriptions.forEach((sub) => { @@ -71,7 +71,7 @@ var Settings = { RCTDeviceEventEmitter.addListener( 'settingsUpdated', - Settings._sendObservations, + Settings._sendObservations.bind(Settings) ); module.exports = Settings; From acafa7e9217465179fb22c90efeb764df56fbcce Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 5 May 2015 05:58:07 -0700 Subject: [PATCH 43/51] [ReactNative] Properly transition RCTTouchHandler state Summary: When touches end or cancel, update self.state in RCTTouchHandler to let iOS know that we are in an ended/canceled state. This way we won't eat other touches because it still thinks we're in a began/changed state. @public Test Plan: Scrolling in the back swipe area no longer busts gesture recognition in Wilde. --- React/Base/RCTTouchHandler.m | 52 +++++++++++++++++++++++++++++++++--- React/Layout/Layout.c | 13 --------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index 7af26da74..2af5c428c 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -55,8 +55,9 @@ _pendingTouches = [[NSMutableArray alloc] init]; _bridgeInteractionTiming = [[NSMutableArray alloc] init]; - // `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower - // level components not build using RCT, will fail to recognize gestures. + // `cancelsTouchesInView` is needed in order to be used as a top level + // event delegated recognizer. Otherwise, lower-level components not built + // using RCT, will fail to recognize gestures. self.cancelsTouchesInView = NO; } return self; @@ -165,7 +166,9 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches); * (start/end/move/cancel) and the indices that represent "changed" `Touch`es * from that array. */ -- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime +- (void)_updateAndDispatchTouches:(NSSet *)touches + eventName:(NSString *)eventName + originatingTime:(CFTimeInterval)originatingTime { // Update touches NSMutableArray *changedIndexes = [[NSMutableArray alloc] init]; @@ -196,15 +199,39 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches); #pragma mark - Gesture Recognizer Delegate Callbacks +static BOOL RCTAllTouchesAreCancelldOrEnded(NSSet *touches) +{ + for (UITouch *touch in touches) { + if (touch.phase == UITouchPhaseBegan || + touch.phase == UITouchPhaseMoved || + touch.phase == UITouchPhaseStationary) { + return NO; + } + } + return YES; +} + +static BOOL RCTAnyTouchesChanged(NSSet *touches) +{ + for (UITouch *touch in touches) { + if (touch.phase == UITouchPhaseBegan || + touch.phase == UITouchPhaseMoved) { + return YES; + } + } + return NO; +} + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; - self.state = UIGestureRecognizerStateBegan; // "start" has to record new touches before extracting the event. // "end"/"cancel" needs to remove the touch *after* extracting the event. [self _recordNewTouches:touches]; [self _updateAndDispatchTouches:touches eventName:@"topTouchStart" originatingTime:event.timestamp]; + + self.state = UIGestureRecognizerStateBegan; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event @@ -213,7 +240,12 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches); if (self.state == UIGestureRecognizerStateFailed) { return; } + [self _updateAndDispatchTouches:touches eventName:@"topTouchMove" originatingTime:event.timestamp]; + + if (self.state == UIGestureRecognizerStateBegan) { + self.state = UIGestureRecognizerStateChanged; + } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event @@ -221,6 +253,12 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches); [super touchesEnded:touches withEvent:event]; [self _updateAndDispatchTouches:touches eventName:@"topTouchEnd" originatingTime:event.timestamp]; [self _recordRemovedTouches:touches]; + + if (RCTAllTouchesAreCancelldOrEnded(event.allTouches)) { + self.state = UIGestureRecognizerStateEnded; + } else if (RCTAnyTouchesChanged(event.allTouches)) { + self.state = UIGestureRecognizerStateChanged; + } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event @@ -228,6 +266,12 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches); [super touchesCancelled:touches withEvent:event]; [self _updateAndDispatchTouches:touches eventName:@"topTouchCancel" originatingTime:event.timestamp]; [self _recordRemovedTouches:touches]; + + if (RCTAllTouchesAreCancelldOrEnded(event.allTouches)) { + self.state = UIGestureRecognizerStateCancelled; + } else if (RCTAnyTouchesChanged(event.allTouches)) { + self.state = UIGestureRecognizerStateChanged; + } } - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer diff --git a/React/Layout/Layout.c b/React/Layout/Layout.c index 50d69b522..9ed711cd0 100644 --- a/React/Layout/Layout.c +++ b/React/Layout/Layout.c @@ -702,19 +702,6 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { } } - float containerMainAxis = node->layout.dimensions[dim[mainAxis]]; - // If the user didn't specify a width or height, and it has not been set - // by the container, then we set it via the children. - if (isUndefined(containerMainAxis)) { - containerMainAxis = fmaxf( - // We're missing the last padding at this point to get the final - // dimension - boundAxis(node, mainAxis, mainDim + getPaddingAndBorder(node, trailing[mainAxis])), - // We can never assign a width smaller than the padding and borders - getPaddingAndBorderAxis(node, mainAxis) - ); - } - float containerCrossAxis = node->layout.dimensions[dim[crossAxis]]; if (isUndefined(node->layout.dimensions[dim[crossAxis]])) { containerCrossAxis = fmaxf( From 929b2999c45dbc457ec2d3c106bb6750fcba8e45 Mon Sep 17 00:00:00 2001 From: jmstout Date: Tue, 5 May 2015 06:21:11 -0700 Subject: [PATCH 44/51] Fix a bug in the Navigator's gesture.edgeHitWidth implementation Summary: Should resolve #1081 cc @ericvicenti Closes https://github.com/facebook/react-native/pull/1082 Github Author: jmstout Test Plan: Imported from GitHub, without a `Test Plan:` line. Revert Plan: This seems legit, but I'm not qualified to review. --- Libraries/CustomComponents/Navigator/Navigator.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 26c6a46bd..ad9ea0e8a 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -54,7 +54,11 @@ var rebound = require('rebound'); var PropTypes = React.PropTypes; +// TODO: this is not ideal because there is no guarantee that the navigator +// is full screen, hwoever we don't have a good way to measure the actual +// size of the navigator right now, so this is the next best thing. var SCREEN_WIDTH = Dimensions.get('window').width; +var SCREEN_HEIGHT = Dimensions.get('window').height; var SCENE_DISABLED_NATIVE_PROPS = { style: { left: SCREEN_WIDTH, @@ -905,13 +909,17 @@ var Navigator = React.createClass({ var travelDist = isTravelVertical ? gestureState.dy : gestureState.dx; var oppositeAxisTravelDist = isTravelVertical ? gestureState.dx : gestureState.dy; + var edgeHitWidth = gesture.edgeHitWidth; if (isTravelInverted) { currentLoc = -currentLoc; travelDist = -travelDist; oppositeAxisTravelDist = -oppositeAxisTravelDist; + edgeHitWidth = isTravelVertical ? + -(SCREEN_HEIGHT - edgeHitWidth) : + -(SCREEN_WIDTH - edgeHitWidth); } var moveStartedInRegion = gesture.edgeHitWidth == null || - currentLoc < gesture.edgeHitWidth; + currentLoc < edgeHitWidth; var moveTravelledFarEnough = travelDist >= gesture.gestureDetectMovement && travelDist > oppositeAxisTravelDist * gesture.directionRatio; From 08246b77dffa90c738c30d69e6c67c37b74e5c03 Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Tue, 5 May 2015 07:16:53 -0700 Subject: [PATCH 45/51] [React Native] Fix immediate animation crash --- Libraries/Animation/AnimationUtils.js | 6 ++++-- Libraries/Animation/RCTAnimationExperimentalManager.m | 2 +- React/Base/RCTConvert.m | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Libraries/Animation/AnimationUtils.js b/Libraries/Animation/AnimationUtils.js index ac62465a0..2ee2d8a70 100644 --- a/Libraries/Animation/AnimationUtils.js +++ b/Libraries/Animation/AnimationUtils.js @@ -231,8 +231,10 @@ module.exports = { var tickCount = Math.round(duration * ticksPerSecond / 1000); var samples = []; - for (var i = 0; i <= tickCount; i++) { - samples.push(easing.call(defaults, i / tickCount)); + if (tickCount > 0) { + for (var i = 0; i <= tickCount; i++) { + samples.push(easing.call(defaults, i / tickCount)); + } } return samples; diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index 64ee577fe..6bcda39ae 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -114,7 +114,7 @@ RCT_EXPORT_METHOD(startAnimation:(NSNumber *)reactTag animationTag:(NSNumber *)animationTag duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay - easingSample:(NSArray *)easingSample + easingSample:(NSNumberArray *)easingSample properties:(NSDictionary *)properties callback:(RCTResponseSenderBlock)callback) { diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index a5d2239af..9c27b95d9 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -805,7 +805,9 @@ NSArray *RCTConvertArrayValue(SEL type, id json) for (NSInteger i = 0; i < idx; i++) { [(NSMutableArray *)values addObject:json[i]]; } - [(NSMutableArray *)values addObject:value]; + if (value) { + [(NSMutableArray *)values addObject:value]; + } copy = YES; } }]; From 3ab4d32538fac43c7dd2320354e30e2e3c07aca3 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Tue, 5 May 2015 07:27:40 -0700 Subject: [PATCH 46/51] [ReactNative] Fix DevMenu crash when launching the app with WebView executor --- React/Base/RCTDevMenu.m | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 6c64e4677..d2e4b7416 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -123,11 +123,17 @@ RCT_EXPORT_MODULE() { _settings = [NSMutableDictionary dictionaryWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]]; + __weak RCTDevMenu *weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ - self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; - self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; - self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; - self.executorClass = NSClassFromString(_settings[@"executorClass"]); + RCTDevMenu *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + strongSelf.shakeToShow = [strongSelf->_settings[@"shakeToShow"] ?: @YES boolValue]; + strongSelf.profilingEnabled = [strongSelf->_settings[@"profilingEnabled"] ?: @NO boolValue]; + strongSelf.liveReloadEnabled = [strongSelf->_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + strongSelf.executorClass = NSClassFromString(strongSelf->_settings[@"executorClass"]); }); } From 88715e5c93af57dbd39c97e3cca5ca66e692e615 Mon Sep 17 00:00:00 2001 From: Iragne Date: Tue, 5 May 2015 09:25:01 -0700 Subject: [PATCH 47/51] Update RCTNavigator.m Summary: Related to this issue I created the PR. Feel free to talk about it. https://github.com/facebook/react-native/issues/65#issuecomment-93240332 Thanks Closes https://github.com/facebook/react-native/pull/1131 Github Author: Iragne Test Plan: Open NavigatorIOS example in UIExplorer, push recurse navigation several times. Press back and observe that it no longer breaks. --- React/Views/RCTNavigator.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index f947a6128..57415fbb7 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -461,6 +461,10 @@ NSInteger kNeverProgressed = -10000; // --- previously caught up -------- ------- still caught up ---------- viewControllerCount == previousReactCount && currentReactCount == previousReactCount; +BOOL jsGettingtooSlow = + // --- previously not caught up -------- ------- no longer caught up ---------- + viewControllerCount < previousReactCount && currentReactCount < previousReactCount; + BOOL reactPushOne = jsGettingAhead && currentReactCount == previousReactCount + 1; BOOL reactPopN = jsGettingAhead && currentReactCount < previousReactCount; @@ -471,7 +475,8 @@ NSInteger kNeverProgressed = -10000; if (!(jsGettingAhead || jsCatchingUp || jsMakingNoProgressButNeedsToCatchUp || - jsMakingNoProgressAndDoesntNeedTo)) { + jsMakingNoProgressAndDoesntNeedTo || + jsGettingtooSlow)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } if (currentReactCount > _currentViews.count) { From bd591505f16ac69e6c00cf6979101f07e3920797 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Tue, 5 May 2015 10:37:30 -0700 Subject: [PATCH 49/51] [react-packager] update sane --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54d7ada11..987347a1d 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "react-timer-mixin": "^0.13.1", "react-tools": "0.13.2", "rebound": "^0.0.12", - "sane": "^1.1.1", + "sane": "^1.1.2", "source-map": "0.1.31", "stacktrace-parser": "git://github.com/frantic/stacktrace-parser.git#493c5e5638", "uglify-js": "~2.4.16", From 286e1b0ae9541c064cfe0644aef7c711bcb260bd Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 6 May 2015 09:31:39 -0700 Subject: [PATCH 50/51] [ReactNative] Re-record OSS snapshot tests --- .../testTextExampleSnapshot_1@2x.png | Bin 270778 -> 270812 bytes .../testViewExampleSnapshot_1@2x.png | Bin 89870 -> 89799 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png index d697ae5b5e3e17787331c45592ba574db67d4f7a..2c6675b7b4eeba914d0ee3fee1d82d18fcae9b85 100644 GIT binary patch delta 51641 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 Lt6HFJ`TYL^<^Zn; diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png index 74e03581c464bc6a278eda27fb8ef9724f9a6bb5..161ddba986f2818fa61219fc48ae626b95c12d4a 100644 GIT binary patch delta 1818 zcmcJP{a4b59>;+*b}EBcna0}%qzkbenpZ9rvKJRx~7yOwJ zUVado{W$dMd*EX+)?EMQ9(Tdh5uW9UE(AQ1UygUpnUxi+_~i$?pP*#R05 zLw~zoUpjNyE%O2JmZp?9Ch8iDLYnuf*CCY?^+31&8B0{awaljR-$?Lw$#kSp6TC3B z{4?w1sS+cnMgQ5}wc{^1pE4?#)2cK`a+Eq%baZT=%T|2xC6X8&G4L?vJ|sYYzp1=}cJji}G;OWOE~{3@08 zaO*KfYp+#{weh+Z)My|tIItXW)VM@hsotQDB{HWfl1K*D1>RWa3LP+F=OQbmb2b&- zxFDH9@oUcx9WAoH6QhBmMTrM5Pz&nJ@8q!?RhIc&qNrD^dVd|;rEBjLnJ?9uMoBqR zK}abIK4g}ht;uL(Q)+PAy_WvY3CMm9Z!$May(BACh;t&Fb!55`Obpht<>?oqUH44# zAzKPQ{H{wYH?iV>_+>gsWrz;$`JfP4TX)i4ow*ujxCD$PLbRm;CLa)e+sSo(-x@VY z*EODZ^5Ki#eF;v+Ly8xLyG3?4xwju1a{L)!7(cd3-p-uvca~$j76lWW!2)NsW|{`( zCj_qxVZxS&SY0jF-!wC=Pd8CsV6P(|8Awsc{#djte2x#&#W=nwDa_6hRh5RgB~sLe9Y)>(~I#ia{4PwX?Uv zH#qN8bL8%ngViRg?B*3%`x=)6j_h35sqN*|x;XYHu~eI7Nl~n#Chdr1k^sP>dpOvE zIyj6Yl##ut&)X~;vP|1X^Rm~pced$9iDw4ncTgBc*q~Gk@rx&f)3gTvh?`wY!i5n) zRzGaU6ceMwM-sIAg>*j-7zT#58MY8|U(@66)0+Gv^3vpW zuX9u$|6%jxfePPB!gMZ>Rj^ZVlC0k6Aa%G4(erNnsBO7IElc5FxIVu6vd1BZpB6Ol z$`;&;kh!=;HW!40x5{ViSQYin5Gv-Yxho!JHz+1*blUy9v7biqAa7`j1Z^jzItP;f zjTMR1a8oS;J(Do(dN^u#y*nW58TVvQ5l6fGQ482Tj8vM~STM&+uIY1EBJl!o1g1AoWS7i-QCaaQ(T{noNvvV?4ApsD-{0Jd190JZ z$@Xi}PXZq<1mY0RKu)F{NLZ<8mB|qw9Ix)_IBUL5*e;xur2LuNyZuSxwrDRx&Gk z^f3xt*7Lkfu>$>Bi@g>n(DX^)2xRbleO>@gZX07p=83ywm9$Tj_v(f zVJE{>5YGr{l;A%&!wNZNbKer<3hV*oD(XaEs7(EXOJ(+49!&V8a&`_2gE#Vg=EdK! z2Cz`OFFx2>iN&8lR_3Brbgrnj8(PWAPJntsLs@TQp#Oj!hOm53KvVh2C!qJCtSmg# nm;W~s6bt?T?O7}O-#K?X;k_H#L7CF>Rc}WRM<03=ntbu!zVy7? delta 2054 zcmd^9`&ZHl76&$)o!TDLn66EuTc&BLIG7J?)6{$3E}0~vnG~U$>e}6kXPi`o#A|?A9;R22yUvP{W9y9^In*gdwd&^_ zKFY53#MPka?9KN!5hgA$$0U{+6Nw{RMVHzqNhW&`-TfLXDh70l)0#8mNbXk>+ z%{4vl>3>ckNjiWb$JJewRAn^*D_uQ$EDilgd9yw@Fq#EfuDT2eV|) z#~8>Ck54ljPJ=p>{j+dol{|-Op72SE(MN+_%dY)3T5miY$W!}P>S#hRlu~l-e8E~I zXCvYmg(kwRQX+r0&+}2o(Wi}G30}X|BKL5GH8GEe5@Q21aK#b}j5?M$lH^gmMpSkQ zdz#FRECQi9izrMqFsV>RpS$6_#27FZ<}}qA;~s-0`Rg%|d($YfMUE~9e%&Uo_%c*G zoaiySYvZ|j5aI9lwb-AwcT!($Qll1FG4q`;z|qsc?iCvdR{f&h+N~yr`|<8hqhy}h zlewktx@Bi^OPU%PnDE__AQZ`bI+}^hYC+pf0Huhq328P{BnUNkqB0&_c;rHep>_{1L%}7?ULj>J~CbRHM}M{m+zEcDI>`aHnS>C5N~@$9B>WjnOc<8 zmqg>{F3%6sCkhQ4lb{9te63AhONXr_#{|8#?LA1`>}%ZT+X0~rcJl%b{)?(;7&>sP zOe5!TJhU_oBJ{!BhO{~L#uWDJySO335ue-N^sW2!$;eTX{(UoR6XP+dJzk#Wp4$KH zX!Lo_S3sPWFn$>d^%4L3h+b8Xd<+<=8Py`mF{q*2k$#}@BDq5hK9UpdC_)`{N0gD=e zE<3B4wUq!(DVaEpJC$f`&n=*6kz32b^N&)|e{ljQ5A(Kca|L{K?jN%=Zr~7<3h}M5 zI3B6U$2JdXn$dE;MoGZS<2B~oFIiI$q1c&l;VWyXBdjRRNxyeP3GCfxqH1Pc&?~h+ zBDl5J2pQBLTNa;|1klE8rHW^Q{7crwDa_;ILl)wlQmsQCHZ0h$i^LBJdVYw3+Jee1 z{pZ@?I0E>=%*(#q(S5hnZZ^z13mcx^VYVc!IsKTlP(xl4?xY`kZ;yp4ZLq9qbf2U; z8!yz&t9dUQsJ}knG851Xo-jUc6mq^bU_9cmQz$XOwfK@M&_NKn{kmW7^-L6+v+~eF zmvptlUT=s6fHTWRvupQ;OW61eW|=sQ zxX_acZgn0+EhTiL1XvN)=3UspZO{39l6qjog z(LC4v1hMadUHTi@#327RJB2~)y^A8wr6pOTsKN@V#GxKmpE%P-&xr{ z7F+D%E1fj<)_HYB&dsKk#Qp74BTlG!=mW From 735f67337a0919b8d2e1addafc6269c854139b3a Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 6 May 2015 09:33:20 -0700 Subject: [PATCH 51/51] [ReactNative] Fail faster in OSS tests --- Examples/SampleApp/SampleAppTests/SampleAppTests.m | 4 ++-- Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m | 5 +++-- Libraries/RCTTest/RCTTestRunner.m | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Examples/SampleApp/SampleAppTests/SampleAppTests.m b/Examples/SampleApp/SampleAppTests/SampleAppTests.m index 647942713..d8dce811d 100644 --- a/Examples/SampleApp/SampleAppTests/SampleAppTests.m +++ b/Examples/SampleApp/SampleAppTests/SampleAppTests.m @@ -44,8 +44,8 @@ NSString *redboxError = nil; while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; diff --git a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m index 2c2359b44..df748ef03 100644 --- a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m +++ b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m @@ -69,8 +69,9 @@ NSString *redboxError = nil; while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; foundElement = [self findSubviewInView:vc.view matching:^(UIView *view) { if ([view respondsToSelector:@selector(attributedText)]) { diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 0aa148fbc..9c0cacf70 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -84,8 +84,8 @@ NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; while ([date timeIntervalSinceNow] > 0 && ![testModule isDone] && error == nil) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; error = [[RCTRedBox sharedInstance] currentErrorMessage]; } [rootView removeFromSuperview];