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 6d3d55c316f097..538c355383f449 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 ab725e84465fb6..4bc933ebaa11b9 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 51f6809ccd9b28..73493aaf6fb029 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 39e5720f520eb9..16052c6fa69605 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 11ad47e320da75..8a7fb4a43278d1 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 @@ - (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher 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 348d04f0d03999..cf9440c05e72e9 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 b231b77ee84fc9..488c0077dcdbd3 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 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona } 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 5b6a92682afbdf..5dd3bac5a89359 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -934,8 +934,6 @@ - (void)setUp _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 @@ - (void)setUp withDetails:[error localizedFailureReason]]; } -#endif - } else { [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification @@ -980,33 +976,6 @@ - (void)bindKeys 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 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args [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 8057e570869383..537675576c3204 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 4af9d4e62c118a..529840e0a4fafb 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 @@ - (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event @end -@interface RCTDevMenu () +@interface RCTDevMenu () @end @@ -68,6 +72,28 @@ - (instancetype)init 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 @@ - (void)show actionSheet.actionSheetStyle = UIBarStyleBlack; [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; + _actionSheet = actionSheet; } - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex @@ -218,6 +245,16 @@ - (void)invalidate @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 058759da7a0a5f..9a3a9b49a872ee 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 6268402526315e..b54d18aa3692bf 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 @@ - (void)dismiss @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 878138282f9d1f..f391e6af0dbf88 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -61,12 +61,8 @@ - (instancetype)init } 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 @@ - (instancetype)init [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 10867e9cbc5aa9..c415cb87ae1e97 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 465d1f0bb65413..5802a80f6fe1bf 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -733,11 +733,6 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family 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 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family } } + // 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 b5868139c7c135..2677a0029c5dfe 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 73493aaf6fb029..1b67c4940eba05 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 24944fb1298b42..3cbd0e787a24ea 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 4cd11bd3fbba9d..7a607a12c23691 100644 --- a/Libraries/ART/RCTConvert+ART.m +++ b/Libraries/ART/RCTConvert+ART.m @@ -64,18 +64,6 @@ + (CGPathRef)CGPath:(id)json 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 @@ + (ARTTextFrame)ARTTextFrame:(id)json 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 9ef2f8843bf7e1..7eb0184717a52f 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 b60a646e416f0f..ef1ba1496e8c62 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 c0bedee5ec8952..4846c885e302e8 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -24,14 +24,8 @@ @implementation RCTPushNotificationManager @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 @@ - (void)dealloc [[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 1b67c4940eba05..bb0aa263c96470 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 538c355383f449..86d068f9f7e06e 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 753c8d76d38324..16a378ccfb6496 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -66,7 +66,8 @@ - (instancetype)initWithURL:(NSURL *)URL 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 09628850fb3aa8..a180bae2cf3c7e 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -42,13 +42,17 @@ @implementation RCTWebViewExecutor { 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 @@ - (id)init return [self initWithWebView:nil]; } -- (BOOL)isValid -{ - return _webView != nil; -} - - (void)invalidate { + _valid = NO; _webView.delegate = nil; _webView = nil; } @@ -129,10 +129,21 @@ - (void)executeApplicationScript:(NSString *)script } 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 | 9 +- React/Base/RCTDevMenu.m | 270 ++++++++++++++------ React/Base/RCTKeyCommands.m | 11 + React/Base/RCTRedBox.m | 2 +- React/React.xcodeproj/project.pbxproj | 1 + 8 files changed, 212 insertions(+), 92 deletions(-) diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index a8f913a07de228..9b748ee33695a6 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 c21184b7da5130..fa87beefc7b513 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 5dd3bac5a89359..85486ddc1d82c1 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -964,18 +964,13 @@ - (void)bindKeys __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 537675576c3204..bb80ac208d7ffc 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -36,14 +36,15 @@ @property (nonatomic, assign) BOOL liveReloadEnabled; /** - * The time between checks for code changes. Defaults to 1 second. + * Manually show the dev menu (can be called from JS). */ -@property (nonatomic, assign) NSTimeInterval liveReloadPeriod; +- (void)show; /** - * Manually show the dev menu. + * Manually reload the application. Equivalent to calling [bridge reload] + * directly, but can be called from JS. */ -- (void)show; +- (void)reload; @end diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 529840e0a4fafb..82b4fa968ae03b 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -28,6 +28,7 @@ - (void)stopProfiling; @end static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification"; +static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu"; @implementation UIWindow (RCTDevMenu) @@ -40,14 +41,20 @@ - (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event @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 @@ - (instancetype)init { 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:@"" + // toggle debug menu + [commands registerKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand - action:NULL]; + action:^(UIKeyCommand *command) { + [weakSelf toggle]; + }]; - // reload in debug mode - [commands registerKeyCommandWithInput:@"d" + // reload in normal mode + [commands registerKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { - __strong RCTDevMenu *strongSelf = weakSelf; - [strongSelf show]; + weakSelf.executorClass = Nil; }]; #endif @@ -98,11 +117,58 @@ - (instancetype)init 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 @@ - (void)showOnShake } } -- (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 @@ - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger } } +- (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) { - return; + if (_liveReloadEnabled != enabled) { + _liveReloadEnabled = enabled; + [self updateSetting:@"liveReloadEnabled" value: @(_liveReloadEnabled)]; } - _liveReloadEnabled = enabled; if (_liveReloadEnabled) { - - _updateTimer = [NSTimer scheduledTimerWithTimeInterval:_liveReloadPeriod - target:self - selector:@selector(pollForUpdates) - userInfo:nil - repeats:YES]; + [self checkForUpdates]; } else { - - [_updateTimer invalidate]; - _updateTimer = nil; + [_updateTask cancel]; + _updateTask = nil; } } -- (void)setLiveReloadPeriod:(NSTimeInterval)liveReloadPeriod +- (void)setExecutorClass:(Class)executorClass { - _liveReloadPeriod = liveReloadPeriod; - if (_liveReloadEnabled) { - self.liveReloadEnabled = NO; - self.liveReloadEnabled = YES; + if (_executorClass != executorClass) { + _executorClass = executorClass; + [self updateSetting:@"executorClass" value: NSStringFromClass(executorClass)]; } -} -- (void)pollForUpdates -{ - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - if (!sourceCodeModule) { - RCTLogError(@"RCTSourceCode module not found"); - self.liveReloadEnabled = NO; - } + if (_bridge.executorClass != executorClass) { - 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) { + // 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; + } - NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; - if (_liveReloadEnabled && HTTPResponse.statusCode == 205) { - [_bridge reload]; - } - }]; + _bridge.executorClass = executorClass; + [self reload]; + } } -- (BOOL)isValid +- (void)checkForUpdates { - return !_liveReloadEnabled || _updateTimer != nil; -} + if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) { + return; + } -- (void)invalidate -{ - [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; - [_updateTimer invalidate]; - _updateTimer = nil; + if (_updateTask) { + [_updateTask cancel]; + _updateTask = nil; + return; + } + + __weak RCTDevMenu *weakSelf = self; + _updateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + 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]; + } + } + }); + + }]; + + [_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 9141dd31d9348b..823acb2418659e 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -90,6 +90,17 @@ - (void)registerKeyCommandWithInput:(NSString *)input { 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 b54d18aa3692bf..0de61d1721c487 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -91,7 +91,7 @@ - (void)openStackFrameInEditor:(NSDictionary *)stackFrame [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 c415cb87ae1e97..42954d36e55092 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 e66612b22886c0..76419bce0aa0e4 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 dcc5d43efa9c25..e70820435ddaef 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 86d068f9f7e06e..280d8c4dd4f430 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 4e361d9dea1293..d46807ce7c2a14 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 72845a2e175460..eb307a02eb558a 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 962eb7fe9b9dfb..33e0170377eab4 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 67e31e47efef45..c621f74700b0c9 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 db596a7bc4beb4..0aaa3971cd8423 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 8e1420a3a6c0dd..3f09344624df16 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 8563e27280f3c3..c03a0432c20c26 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 00000000000000..a5f1d5689106df --- /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 280d8c4dd4f430..f6fd60888287e0 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 f6fd60888287e0..b740cfaead244d 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 e70820435ddaef..efff4c9dc729e1 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 5ee2dc5e355b1e..fe92f5c58c5b96 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 00000000000000..367ccd1fcda9da --- /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 221b8daebb6b59..b6ad9b1101dbb0 100644 --- a/Libraries/Text/RCTRawTextManager.m +++ b/Libraries/Text/RCTRawTextManager.m @@ -17,7 +17,7 @@ @implementation RCTRawTextManager - (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 367ccd1fcda9da..c4218f73d5a40a 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 1f3d493f3c5167..74be17a46ea104 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 7bb795f160972b..a5b591a61f8ef6 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 3bdc72acb23384..a431cd072273c8 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 | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 7cba2216472945..30d05210fb4a2f 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -11,14 +11,13 @@ */ 'use strict'; +var AnimationExperimental = require('AnimationExperimental'); var NativeMethodsMixin = require('NativeMethodsMixin'); -var React = require('React'); var POPAnimation = require('POPAnimation'); -var AnimationExperimental = require('AnimationExperimental'); +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 85486ddc1d82c1..134b3659db974e 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1146,15 +1146,12 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg { #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 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg * 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 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg [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 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg }; 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 3e864657b82c5d..f21233dbfa0576 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 134b3659db974e..9ce2e037048c0c 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -381,10 +381,8 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName 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 9ce2e037048c0c..22fce74fca07bb 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 @@ - (void)_invokeAndProcessModule:(NSString *)module 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 82b4fa968ae03b..c6063caa2301b2 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -66,7 +66,9 @@ + (void)initialize // We're swizzling here because it's poor form to override methods in a category, // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's // no need to call the original implementation. +#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 b740cfaead244d..6d09b656e38b7d 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 40a3e9b517d0a7..54d7ada11c2288 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 | 561 +++++++++++++++++----------- 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, 507 insertions(+), 334 deletions(-) diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 75a8118318ee10..0aa148fbc889bb 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 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona 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 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona 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 97658c2356b3d6..f15dd70e0d9b84 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 6dfbe2414557f8..77cbb215ec6c24 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 22fce74fca07bb..349cc725e13a2b 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 NS_ENUM(NSUInteger, RCTBridgeFields) { #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 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { * 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 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { 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 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { 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 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { 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 @@ @implementation RCTModuleMethod dispatch_block_t _methodQueue; } -static Class _globalExecutorClass; - static NSString *RCTStringUpToFirstArgument(NSString *methodName) { NSRange colonRange = [methodName rangeOfString:@":"]; @@ -702,6 +715,7 @@ - (NSString *)description @"methods": [[NSMutableDictionary alloc] init] }; localModules[moduleName] = module; + [RCTLocalModuleNames addObject:moduleName]; } // Add method if it doesn't already exist @@ -712,145 +726,265 @@ - (NSString *)description @"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 +@interface RCTFrameUpdate (Private) -- (instancetype)initWithBridge:(RCTBridge *)bridge selector:(SEL)selector NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; @end -@interface RCTBridge (RCTDisplayLink) +@implementation RCTFrameUpdate -- (void)_update:(CADisplayLink *)displayLink; +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink +{ + if ((self = [super init])) { + _timestamp = displayLink.timestamp; + _deltaTime = displayLink.duration; + } + return self; +} @end -@implementation RCTDisplayLink -{ - __weak RCTBridge *_bridge; - CADisplayLink *_displayLink; - SEL _selector; -} +@implementation RCTBridge -- (instancetype)initWithBridge:(RCTBridge *)bridge selector:(SEL)selector +static id _latestJSExecutor; + +- (instancetype)initWithBundleURL:(NSURL *)bundleURL + moduleProvider:(RCTBridgeModuleProviderBlock)block + launchOptions:(NSDictionary *)launchOptions { + RCTAssertMainThread(); + if ((self = [super init])) { - _bridge = bridge; - _selector = selector; - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + /** + * Pre register modules + */ + RCTLocalModulesConfig(); + + _bundleURL = bundleURL; + _moduleProvider = block; + _launchOptions = [launchOptions copy]; + [self bindKeys]; + [self setUp]; } return self; } -- (BOOL)isValid +- (void)dealloc { - return _displayLink != nil; + /** + * This runs only on the main thread, but crashes the subclass + * RCTAssertMainThread(); + */ + [self invalidate]; } -- (void)invalidate +- (void)bindKeys { - if (self.isValid) { - [_displayLink invalidate]; - _displayLink = nil; - } + 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)_update:(CADisplayLink *)displayLink +- (void)reload { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - [_bridge performSelector:_selector withObject:displayLink]; -#pragma clang diagnostic pop +/** + * AnyThread + */ + dispatch_async(dispatch_get_main_queue(), ^{ + [self invalidate]; + [self setUp]; + }); } -@end +- (void)setUp +{ + RCTAssertMainThread(); -@interface RCTFrameUpdate (Private) + _batchedBridge = [[RCTBatchedBridge alloc] initWithParentBridge:self]; +} -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; +- (BOOL)isValid +{ + return _batchedBridge.isValid; +} -@end +- (void)invalidate +{ + RCTAssertMainThread(); -@implementation RCTFrameUpdate + [_batchedBridge invalidate]; + _batchedBridge = nil; +} -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink ++ (void)logMessage:(NSString *)message level:(NSString *)level { - if ((self = [super init])) { - _timestamp = displayLink.timestamp; - _deltaTime = displayLink.duration; - } - return self; + 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 RCTBridge +@implementation RCTBatchedBridge { + BOOL _loading; + id _javaScriptExecutor; RCTSparseArray *_modulesByID; RCTSparseArray *_queuesByID; dispatch_queue_t _methodQueue; NSDictionary *_modulesByName; - id _javaScriptExecutor; - Class _executorClass; - NSURL *_bundleURL; - RCTBridgeModuleProviderBlock _moduleProvider; - RCTDisplayLink *_displayLink; - RCTDisplayLink *_vsyncDisplayLink; + CADisplayLink *_mainDisplayLink; + CADisplayLink *_jsDisplayLink; NSMutableSet *_frameUpdateObservers; NSMutableArray *_scheduledCalls; RCTSparseArray *_scheduledCallbacks; - BOOL _loading; } -static id _latestJSExecutor; +@synthesize valid = _valid; -- (instancetype)initWithBundleURL:(NSURL *)bundleURL - moduleProvider:(RCTBridgeModuleProviderBlock)block - launchOptions:(NSDictionary *)launchOptions +- (instancetype)initWithParentBridge:(RCTBridge *)bridge { - if ((self = [super init])) { - _bundleURL = bundleURL; - _moduleProvider = block; - _launchOptions = [launchOptions copy]; + if (self = [super init]) { + RCTAssertMainThread(); - [self setUp]; - [self bindKeys]; + _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; } -- (void)setUp +- (NSDictionary *)launchOptions { - 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]; + return _parentBridge.launchOptions; +} - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - _displayLink = [[RCTDisplayLink alloc] initWithBridge:self selector:@selector(_jsThreadUpdate:)]; - }]; - _vsyncDisplayLink = [[RCTDisplayLink alloc] initWithBridge:self selector:@selector(_mainThreadUpdate:)]; +/** + * 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 @@ - (void)setUp } // 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 @@ - (void)setUp _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 @@ - (void)setUp dispatch_semaphore_signal(semaphore); }]; - _loading = YES; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW); + + NSURL *bundleURL = _parentBridge.bundleURL; if (_javaScriptExecutor == nil) { /** @@ -929,11 +1073,17 @@ - (void)setUp */ _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 @@ - (void)setUp } } 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 @@ - (NSDictionary *)modules return _modulesByName; } -- (void)dealloc -{ - [self invalidate]; -} - #pragma mark - RCTInvalidating -- (BOOL)isValid -{ - return _javaScriptExecutor != nil; -} - - (void)invalidate { - if (!self.isValid && _modulesByID == nil) { - return; - } - - if (![NSThread isMainThread]) { - [self performSelectorOnMainThread:@selector(invalidate) withObject:nil waitUntilDone:YES]; + if (!self.isValid) { return; } - [[NSNotificationCenter defaultCenter] removeObserver:self]; + RCTAssertMainThread(); - // 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 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args */ - (void)_immediatelyCallTimer:(NSNumber *)timer { + RCTAssertJSThread(); + NSString *moduleDotMethod = @"RCTJSTimers.callTimers"; NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod]; RCTAssert(moduleID != nil, @"Module '%@' not registered.", @@ -1074,35 +1209,29 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer 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]; - } - -#else + dispatch_block_t block = ^{ + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + }; - [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 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete: - (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 @@ - (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID */ - (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 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg */ 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 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg - (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 @@ - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)me - (void)_handleBuffer:(id)buffer context:(NSNumber *)context { + RCTAssertJSThread(); + if (buffer == nil || buffer == (id)kCFNull) { return; } @@ -1307,6 +1450,11 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i 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 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i 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 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i - (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 @@ - (void)_jsThreadUpdate:(CADisplayLink *)displayLink context:RCTGetExecutorID(_javaScriptExecutor)]; } -#endif - RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink { - RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); -} - -- (void)addFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers addObject:observer]; -} + RCTAssertMainThread(); -- (void)removeFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers removeObject:observer]; + RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); } -- (void)reload +- (void)startProfiling { - 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]; - } - }); -} + RCTAssertMainThread(); -+ (void)logMessage:(NSString *)message level:(NSString *)level -{ - if (![_latestJSExecutor isValid]) { + if (![_parentBridge.bundleURL.scheme isEqualToString:@"http"]) { + RCTLogError(@"To run the profiler you must be running from the dev server"); 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) {}]; -} + [_mainDisplayLink invalidate]; + _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; + [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; -- (void)startProfiling -{ - if (![_bundleURL.scheme isEqualToString:@"http"]) { - RCTLogError(@"To run the profiler you must be running from the dev server"); - return; - } 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 c6063caa2301b2..6c64e467754ab6 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -123,10 +123,12 @@ - (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"]); + 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 @@ - (void)jsLoaded _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 8d52529e67acfb..01f51d7e99e05c 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 2e7d21b9442fe7..0210986dceab03 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -27,7 +27,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge 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 @@ - (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onCom 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 @@ - (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onCom code:error.code userInfo:userInfo]; } - onComplete(error); + onComplete(error, nil); return; } @@ -96,18 +96,10 @@ - (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onCom 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 ee5a35d7f0be96..d55094c3727328 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 54556d41810ba4..9ee09d495ee447 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 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _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 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL 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 @@ - (BOOL)canBecomeFirstResponder 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 @@ - (void)bundleFinishedLoading * 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 @@ - (void)layoutSubviews [super layoutSubviews]; if (_contentView) { _contentView.frame = self.bounds; - [_bridge.uiManager setFrame:self.bounds forRootView:_contentView]; } } @@ -148,6 +147,12 @@ - (NSNumber *)reactTag return _contentView.reactTag; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_contentView removeFromSuperview]; +} + @end @implementation RCTUIManager (RCTRootView) @@ -160,3 +165,60 @@ - (NSNumber *)allocateRootTag } @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 e2df5befca18eb..e21c9d16fe4732 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -70,6 +70,7 @@ @implementation RCTTiming } @synthesize bridge = _bridge; +@synthesize paused = _paused; RCT_EXPORT_MODULE() @@ -78,7 +79,7 @@ @implementation RCTTiming - (instancetype)init { if ((self = [super init])) { - + _paused = YES; _timers = [[RCTSparseArray alloc] init]; for (NSString *name in @[UIApplicationWillResignActiveNotification, @@ -126,7 +127,7 @@ - (void)invalidate - (void)stopTimers { - [_bridge removeFrameUpdateObserver:self]; + _paused = YES; } - (void)startTimers @@ -135,7 +136,7 @@ - (void)startTimers return; } - [_bridge addFrameUpdateObserver:self]; + _paused = NO; } - (void)didUpdateFrame:(RCTFrameUpdate *)update diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 496b9c3513a677..d35cae03a1e4e9 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -267,11 +267,6 @@ - (instancetype)init return self; } -- (void)dealloc -{ - RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); -} - - (BOOL)isValid { return _viewRegistry != nil; @@ -279,20 +274,24 @@ - (BOOL)isValid - (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 a4cb338fb3c88b..f947a6128e23ba 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -264,9 +264,13 @@ @implementation RCTNavigator 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 @@ - (void)navigationController:(UINavigationController *)navigationController _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 6d09b656e38b7d..26c6a46bdc6a3d 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 053ea69849e821..750a932b644970 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 91fb970f885521..57576961243cac 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 2b168e44a29e19..50d69b522b54d8 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 51f72493bb5b2f..fe383ea5728ed1 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 00000000000000..7e69403f3a44a3 --- /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 349cc725e13a2b..35e85b3c978279 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -941,6 +941,11 @@ - (instancetype)initWithParentBridge:(RCTBridge *)bridge 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 76419bce0aa0e4..f8dc3bcba15356 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 959422bbc9888a..79ded650658a2a 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 35e85b3c978279..a7c6a30a6914da 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1108,7 +1108,8 @@ - (void)initJS * 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 cf9440c05e72e9..0d16e33e30737d 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 00000000000000..e4a210466c66f4 --- /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 00000000000000..274cc69aed4328 --- /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 00000000000000..b17439eafa15dd --- /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 00000000000000..d13a32ba9218bc --- /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 00000000000000..c9836f0101e77a --- /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 eb81c167c9aa02..71bf7b30282546 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 5802a80f6fe1bf..a5d2239af57364 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -828,6 +828,52 @@ + (NSArray *)CGColorArray:(id)json 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 0d16e33e30737d..698fd3b3f42fff 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 a10d291bdb54d2..a24ec1a54a20ff 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 7af26da74eab70..2af5c428c3de74 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -55,8 +55,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _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 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex * (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 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventNa #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 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 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 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event [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 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event [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 50d69b522b54d8..9ed711cd07be52 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 26c6a46bdc6a3d..ad9ea0e8a863f9 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 ac62465a02e219..2ee2d8a7097591 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 64ee577fef30b6..6bcda39ae30d83 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -114,7 +114,7 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS 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 a5d2239af57364..9c27b95d98de4f 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -805,7 +805,9 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family 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 6c64e467754ab6..d2e4b7416e0fa1 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -123,11 +123,17 @@ - (void)updateSettings { _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 f947a6128e23ba..57415fbb79a8ce 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -461,6 +461,10 @@ - (void)reactBridgeDidFinishTransaction // --- 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 @@ - (void)reactBridgeDidFinishTransaction if (!(jsGettingAhead || jsCatchingUp || jsMakingNoProgressButNeedsToCatchUp || - jsMakingNoProgressAndDoesntNeedTo)) { + jsMakingNoProgressAndDoesntNeedTo || + jsGettingtooSlow)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } if (currentReactCount > _currentViews.count) { From 79e33502fc21aec2abf090e0642ff364feac6234 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Tue, 5 May 2015 10:36:44 -0700 Subject: [PATCH 48/51] [react-packager] check in graceful-fs 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 54d7ada11c2288..987347a1d4f88f 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 64794271376b04..d8dce811dac25d 100644 --- a/Examples/SampleApp/SampleAppTests/SampleAppTests.m +++ b/Examples/SampleApp/SampleAppTests/SampleAppTests.m @@ -44,8 +44,8 @@ - (void)testRendersWelcomeScreen { 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 2c2359b4400339..df748ef0376ec2 100644 --- a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m +++ b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m @@ -69,8 +69,9 @@ - (void)testAAA_RootViewLoadsAndRenders 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 0aa148fbc889bb..9c0cacf709c3e1 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -84,8 +84,8 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona 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];