diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index 6f0493e20b948b..d2fc3eadedee94 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; + 00E356F31AD99517003FC87E /* SampleAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* SampleAppTests.m */; }; 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; @@ -74,6 +75,13 @@ remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; remoteInfo = RCTVibration; }; + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = SampleApp; + }; 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; @@ -106,6 +114,9 @@ 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; + 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00E356F21AD99517003FC87E /* SampleAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleAppTests.m; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = iOS/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = iOS/AppDelegate.m; sourceTree = ""; }; @@ -119,6 +130,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -195,6 +213,23 @@ name = Products; sourceTree = ""; }; + 00E356EF1AD99517003FC87E /* SampleAppTests */ = { + isa = PBXGroup; + children = ( + 00E356F21AD99517003FC87E /* SampleAppTests.m */, + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = SampleAppTests; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* SampleApp */ = { isa = PBXGroup; children = ( @@ -255,6 +290,7 @@ children = ( 13B07FAE1A68108700A75B9A /* SampleApp */, 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* SampleAppTests */, 83CBBA001A601CBA00E9B192 /* Products */, ); indentWidth = 2; @@ -265,6 +301,7 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* SampleApp.app */, + 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */, ); name = Products; sourceTree = ""; @@ -272,6 +309,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* SampleAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "SampleAppTests" */; + buildPhases = ( + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = SampleAppTests; + productName = SampleAppTests; + productReference = 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 13B07F861A680F5B00A75B9A /* SampleApp */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleApp" */; @@ -297,6 +352,12 @@ attributes = { LastUpgradeCheck = 0610; ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "SampleApp" */; compatibilityVersion = "Xcode 3.2"; @@ -354,6 +415,7 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* SampleApp */, + 00E356ED1AD99517003FC87E /* SampleAppTests */, ); }; /* End PBXProject section */ @@ -432,6 +494,13 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8E1A680F5B00A75B9A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -445,6 +514,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00E356F31AD99517003FC87E /* SampleAppTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -456,6 +533,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* SampleApp */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; @@ -469,6 +554,43 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = SampleAppTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleApp.app/SampleApp"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = SampleAppTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleApp.app/SampleApp"; + }; + name = Release; + }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -590,6 +712,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "SampleAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Examples/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme b/Examples/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme index a3375a020c0639..7dcff488e2802b 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme +++ b/Examples/SampleApp/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:SampleApp.xcodeproj"> + + + + + + + + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Examples/SampleApp/SampleAppTests/SampleAppTests.m b/Examples/SampleApp/SampleAppTests/SampleAppTests.m new file mode 100644 index 00000000000000..64794271376b04 --- /dev/null +++ b/Examples/SampleApp/SampleAppTests/SampleAppTests.m @@ -0,0 +1,68 @@ +/** + * 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 + +#import "RCTAssert.h" +#import "RCTRedBox.h" +#import "RCTRootView.h" + +#define TIMEOUT_SECONDS 240 +#define TEXT_TO_LOOK_FOR @"Welcome to React Native!" + +@interface SampleAppTests : XCTestCase + +@end + +@implementation SampleAppTests + + +- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test +{ + if (test(view)) { + return YES; + } + for (UIView *subview in [view subviews]) { + if ([self findSubviewInView:subview matching:test]) { + return YES; + } + } + return NO; +} + +- (void)testRendersWelcomeScreen { + UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; + BOOL foundElement = NO; + NSString *redboxError = nil; + + while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + + redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; + + foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { + if ([view respondsToSelector:@selector(attributedText)]) { + NSString *text = [(id)view attributedText].string; + if ([text isEqualToString:TEXT_TO_LOOK_FOR]) { + return YES; + } + } + return NO; + }]; + } + + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); + XCTAssertTrue(foundElement, @"Cound't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); +} + + +@end diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index b115e0f0be938f..b4c54f997d158f 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -226,10 +226,8 @@ exports.examples = [ Contain @@ -238,10 +236,8 @@ exports.examples = [ Cover @@ -250,10 +246,8 @@ exports.examples = [ Stretch diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js index dfe7c5f2180626..dfc09ba7cbc219 100644 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js +++ b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js @@ -74,7 +74,8 @@ var BatchedBridgeFactory = { setLoggingEnabled: messageQueue.setLoggingEnabled.bind(messageQueue), getLoggedOutgoingItems: messageQueue.getLoggedOutgoingItems.bind(messageQueue), getLoggedIncomingItems: messageQueue.getLoggedIncomingItems.bind(messageQueue), - replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue) + replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue), + processBatch: messageQueue.processBatch.bind(messageQueue), }; } }; diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index fc7fc7223c861f..0308688b5768d0 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -17,7 +17,6 @@ var PointPropType = require('PointPropType'); var RCTScrollView = require('NativeModules').UIManager.RCTScrollView; var RCTScrollViewConsts = RCTScrollView.Constants; var React = require('React'); -var ReactIOSTagHandles = require('ReactIOSTagHandles'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var RCTUIManager = require('NativeModules').UIManager; var ScrollResponder = require('ScrollResponder'); @@ -32,6 +31,7 @@ var flattenStyle = require('flattenStyle'); var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); var pointsDiffer = require('pointsDiffer'); +var requireNativeComponent = require('requireNativeComponent'); var PropTypes = React.PropTypes; @@ -73,6 +73,12 @@ var ScrollView = React.createClass({ * the `alwaysBounce*` props are true. The default value is true. */ bounces: PropTypes.bool, + /** + * When true, gestures can drive zoom past min/max and the zoom will animate + * to the min/max value at gesture end, otherwise the zoom will not exceed + * the limits. + */ + bouncesZoom: PropTypes.bool, /** * When true, the scroll view bounces horizontally when it reaches the end * even if the content is smaller than the scroll view itself. The default @@ -120,6 +126,16 @@ var ScrollView = React.createClass({ * instead of vertically in a column. The default value is false. */ horizontal: PropTypes.bool, + /** + * When true, the ScrollView will try to lock to only vertical or horizontal + * scrolling while dragging. The default value is false. + */ + directionalLockEnabled: PropTypes.bool, + /** + * When false, once tracking starts, won't try to drag if the touch moves. + * The default value is true. + */ + canCancelContentTouches: PropTypes.bool, /** * Determines whether the keyboard gets dismissed in response to a drag. * - 'none' (the default), drags do not dismiss the keyboard. @@ -359,6 +375,7 @@ if (Platform.OS === 'android') { validAttributes: validAttributes, uiViewClassName: 'RCTScrollView', }); + var RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView); } module.exports = ScrollView; diff --git a/Libraries/Components/SliderIOS/SliderIOS.js b/Libraries/Components/SliderIOS/SliderIOS.js index ae2475d3364abc..81815ba34c97c5 100644 --- a/Libraries/Components/SliderIOS/SliderIOS.js +++ b/Libraries/Components/SliderIOS/SliderIOS.js @@ -12,6 +12,7 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); +var Platform = require('Platform'); var PropTypes = require('ReactPropTypes'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); @@ -21,6 +22,7 @@ var View = require('View'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); type Event = Object; @@ -96,16 +98,20 @@ var styles = StyleSheet.create({ }, }); -var validAttributes = { - ...ReactIOSViewAttributes.UIView, - value: true, - minimumValue: true, - maximumValue: true, -}; - -var RCTSlider = createReactIOSNativeComponentClass({ - validAttributes: validAttributes, - uiViewClassName: 'RCTSlider', -}); +if (Platform.OS === 'ios') { + var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS); +} else { + var validAttributes = { + ...ReactIOSViewAttributes.UIView, + value: true, + minimumValue: true, + maximumValue: true, + }; + + var RCTSlider = createReactIOSNativeComponentClass({ + validAttributes: validAttributes, + uiViewClassName: 'RCTSlider', + }); +} module.exports = SliderIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js index cdd199ba43c2d0..6eb46c362d2650 100644 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js @@ -11,7 +11,6 @@ 'use strict'; -var Dimensions = require('Dimensions'); var React = require('React'); var View = require('View'); var StyleSheet = require('StyleSheet'); @@ -33,8 +32,10 @@ var styles = StyleSheet.create({ tab: { // TODO(5405356): Implement overflow: visible so position: absolute isn't useless // position: 'absolute', - width: Dimensions.get('window').width, - height: Dimensions.get('window').height, + top: 0, + right: 0, + bottom: 0, + left: 0, borderColor: 'red', borderWidth: 1, } diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js index f795ed7ced6dee..86b38a8cda836d 100644 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js @@ -14,7 +14,6 @@ var Image = require('Image'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); -var Dimensions = require('Dimensions'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); var View = require('View'); @@ -134,8 +133,10 @@ var TabBarItemIOS = React.createClass({ var styles = StyleSheet.create({ tab: { position: 'absolute', - width: Dimensions.get('window').width, - height: Dimensions.get('window').height, + top: 0, + right: 0, + bottom: 0, + left: 0, } }); diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index bf988f5931ae6d..fb5f99949fbdcd 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -72,7 +72,7 @@ var notMultiline = { onSubmitEditing: true, }; -var TextInputAndroidAttributes = { +var AndroidTextInputAttributes = { autoCapitalize: true, autoCorrect: true, autoFocus: true, @@ -80,14 +80,19 @@ var TextInputAndroidAttributes = { multiline: true, password: true, placeholder: true, - value: true, + text: true, testID: true, }; -var AndroidTextInput = createReactIOSNativeComponentClass({ - validAttributes: TextInputAndroidAttributes, +var viewConfigIOS = { + uiViewClassName: 'RCTTextField', + validAttributes: RCTTextFieldAttributes, +}; + +var viewConfigAndroid = { uiViewClassName: 'AndroidTextInput', -}); + validAttributes: AndroidTextInputAttributes, +}; var crossPlatformKeyboardTypeMap = { 'numeric': 'decimal-pad', @@ -293,10 +298,8 @@ var TextInput = React.createClass({ */ mixins: [NativeMethodsMixin, TimerMixin], - viewConfig: { - uiViewClassName: 'RCTTextField', - validAttributes: RCTTextFieldAttributes, - }, + viewConfig: ((Platform.OS === 'ios' ? viewConfigIOS : + (Platform.OS === 'android' ? viewConfigAndroid : {})) : Object), isFocused: function(): boolean { return TextInputState.currentlyFocusedField() === @@ -521,7 +524,7 @@ var TextInput = React.createClass({ onSubmitEditing={this.props.onSubmitEditing} password={this.props.password || this.props.secureTextEntry} placeholder={this.props.placeholder} - value={this.state.bufferedValue} + text={this.state.bufferedValue} />; return ( @@ -591,4 +594,9 @@ var RCTTextField = createReactIOSNativeComponentClass({ uiViewClassName: 'RCTTextField', }); +var AndroidTextInput = createReactIOSNativeComponentClass({ + validAttributes: AndroidTextInputAttributes, + uiViewClassName: 'AndroidTextInput', +}); + module.exports = TextInput; diff --git a/Libraries/Components/UnimplementedViews/UnimplementedView.js b/Libraries/Components/UnimplementedViews/UnimplementedView.js new file mode 100644 index 00000000000000..6aebef38b16bfb --- /dev/null +++ b/Libraries/Components/UnimplementedViews/UnimplementedView.js @@ -0,0 +1,37 @@ +/** + * Common implementation for a simple stubbed view. Simply applies the view's styles to the inner + * View component and renders its children. + * + * @providesModule UnimplementedView + */ + +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var UnimplementedView = React.createClass({ + setNativeProps: function() { + // Do nothing. + // This method is required in order to use this view as a Touchable* child. + // See ensureComponentIsNative.js for more info + }, + render: function() { + return ( + + {this.props.children} + + ); + }, +}); + +var styles = StyleSheet.create({ + unimplementedView: { + borderWidth: 1, + borderColor: 'red', + alignSelf: 'flex-start', + } +}); + +module.exports = UnimplementedView; diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 9cdd1b67dc4ed6..f783c34eaf147d 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -35,6 +35,7 @@ var NavigatorNavigationBar = require('NavigatorNavigationBar'); var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); var NavigatorStaticContextContainer = require('NavigatorStaticContextContainer'); var PanResponder = require('PanResponder'); +var Platform = require('Platform'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); @@ -287,9 +288,10 @@ var Navigator = React.createClass({ ), idStack: routeStack.map(() => getuid()), routeStack, - // These are tracked to avoid rendering everything all the time. - updatingRangeStart: initialRouteIndex, - updatingRangeLength: initialRouteIndex + 1, + // `updatingRange*` allows us to only render the visible or staged scenes + // On first render, we will render every scene in the initialRouteStack + updatingRangeStart: 0, + updatingRangeLength: routeStack.length, // Either animating or gesturing. isAnimating: false, jumpToIndex: routeStack.length - 1, @@ -397,22 +399,22 @@ var Navigator = React.createClass({ this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]); if (this.parentNavigator) { this.parentNavigator.setHandler(this._handleRequest); - } else { + } else if (Platform.OS === 'android') { // There is no navigator in our props or context, so this is the // top-level navigator. We will handle back button presses here - BackAndroid.addEventListener('hardwareBackPress', this._handleBackPress); + BackAndroid.addEventListener('hardwareBackPress', this._handleAndroidBackPress); } }, componentWillUnmount: function() { if (this.parentNavigator) { this.parentNavigator.setHandler(null); - } else { - BackAndroid.removeEventListener('hardwareBackPress', this._handleBackPress); + } else if (Platform.OS === 'android') { + BackAndroid.removeEventListener('hardwareBackPress', this._handleAndroidBackPress); } }, - _handleBackPress: function() { + _handleAndroidBackPress: function() { var didPop = this.pop(); if (!didPop) { BackAndroid.exitApp(); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 5f3dfdd986a0cd..fed358e1337ead 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -14,6 +14,7 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType'); var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeModules = require('NativeModules'); +var Platform = require('Platform'); var PropTypes = require('ReactPropTypes'); var ImageResizeMode = require('ImageResizeMode'); var ImageStylePropTypes = require('ImageStylePropTypes'); @@ -27,7 +28,9 @@ var flattenStyle = require('flattenStyle'); var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); var warning = require('warning'); +var verifyPropTypes = require('verifyPropTypes'); /** * A react component for displaying different types of images, @@ -64,6 +67,13 @@ var Image = React.createClass({ source: PropTypes.shape({ uri: PropTypes.string, }), + /** + * A static image to display while downloading the final image off the + * network. + */ + defaultSource: PropTypes.shape({ + uri: PropTypes.string, + }), /** * Whether this element should be revealed as an accessible element. */ @@ -80,6 +90,11 @@ var Image = React.createClass({ * [Apple documentation](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIImage_Class/index.html#//apple_ref/occ/instm/UIImage/resizableImageWithCapInsets) */ capInsets: EdgeInsetsPropType, + /** + * Determines how to resize the image when the frame doesn't match the raw + * image dimensions. + */ + resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']), style: StyleSheetPropType(ImageStylePropTypes), /** * A unique identifier for this element to be used in UI Automation @@ -104,6 +119,12 @@ var Image = React.createClass({ }, render: function() { + for (var prop in nativeOnlyProps) { + if (this.props[prop] !== undefined) { + console.warn('Prop `' + prop + ' = ' + this.props[prop] + '` should ' + + 'not be set directly on Image.'); + } + } var style = flattenStyle([styles.base, this.props.style]); invariant(style, "style must be initialized"); var source = this.props.source; @@ -119,28 +140,36 @@ var Image = React.createClass({ if (this.props.style && this.props.style.tintColor) { warning(RawImage === RCTStaticImage, 'tintColor style only supported on static images.'); } - + var resizeMode = this.props.resizeMode || style.resizeMode; var contentModes = NativeModules.UIManager.UIView.ContentMode; - var resizeMode; - if (style.resizeMode === ImageResizeMode.stretch) { - resizeMode = contentModes.ScaleToFill; - } else if (style.resizeMode === ImageResizeMode.contain) { - resizeMode = contentModes.ScaleAspectFit; + var contentMode; + if (resizeMode === ImageResizeMode.stretch) { + contentMode = contentModes.ScaleToFill; + } else if (resizeMode === ImageResizeMode.contain) { + contentMode = contentModes.ScaleAspectFit; } else { // ImageResizeMode.cover or undefined - resizeMode = contentModes.ScaleAspectFill; + contentMode = contentModes.ScaleAspectFill; } var nativeProps = merge(this.props, { style, - resizeMode, + contentMode, tintColor: style.tintColor, }); + if (Platform.OS === 'android') { + // TODO: update android native code to not need this + nativeProps.resizeMode = contentMode; + delete nativeProps.contentMode; + } if (isStored) { nativeProps.imageTag = source.uri; } else { nativeProps.src = source.uri; } + if (this.props.defaultSource) { + nativeProps.defaultImageSrc = this.props.defaultSource.uri; + } return ; } }); @@ -151,24 +180,39 @@ var styles = StyleSheet.create({ }, }); -var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, { - accessible: true, - accessibilityLabel: true, - capInsets: {diff: insetsDiffer}, // UIEdgeInsets=UIEdgeInsetsZero - imageTag: true, - resizeMode: true, +if (Platform.OS === 'android') { + var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, { + accessible: true, + accessibilityLabel: true, + capInsets: {diff: insetsDiffer}, // UIEdgeInsets=UIEdgeInsetsZero + imageTag: true, + resizeMode: true, + src: true, + testID: PropTypes.string, + }); + + var RCTStaticImage = createReactIOSNativeComponentClass({ + validAttributes: merge(CommonImageViewAttributes, { tintColor: true }), + uiViewClassName: 'RCTStaticImage', + }); + + var RCTNetworkImage = createReactIOSNativeComponentClass({ + validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }), + uiViewClassName: 'RCTNetworkImageView', + }); +} else { + var RCTStaticImage = requireNativeComponent('RCTStaticImage', null); + var RCTNetworkImage = requireNativeComponent('RCTNetworkImageView', null); +} +var nativeOnlyProps = { src: true, - testID: PropTypes.string, -}); - -var RCTStaticImage = createReactIOSNativeComponentClass({ - validAttributes: merge(CommonImageViewAttributes, { tintColor: true }), - uiViewClassName: 'RCTStaticImage', -}); - -var RCTNetworkImage = createReactIOSNativeComponentClass({ - validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }), - uiViewClassName: 'RCTNetworkImageView', -}); + defaultImageSrc: true, + imageTag: true, + contentMode: true, +}; +if (__DEV__) { + verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps); + verifyPropTypes(Image, RCTNetworkImage.viewConfig, nativeOnlyProps); +} module.exports = Image; diff --git a/Libraries/Image/RCTNetworkImageViewManager.m b/Libraries/Image/RCTNetworkImageViewManager.m index 005b726cf4d4aa..2ecf699714381e 100644 --- a/Libraries/Image/RCTNetworkImageViewManager.m +++ b/Libraries/Image/RCTNetworkImageViewManager.m @@ -29,6 +29,6 @@ - (UIView *)view RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage) RCT_REMAP_VIEW_PROPERTY(src, imageURL, NSURL) -RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) +RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode) @end diff --git a/Libraries/Image/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m index 2d80117e4d386f..87a50d8fe8512e 100644 --- a/Libraries/Image/RCTStaticImageManager.m +++ b/Libraries/Image/RCTStaticImageManager.m @@ -26,7 +26,7 @@ - (UIView *)view } RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets) -RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) +RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode) RCT_CUSTOM_VIEW_PROPERTY(src, NSURL, RCTStaticImage) { if (json) { diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js index 25d0194dc88318..e168daae66201d 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule loadSourceMap + * * -- disabled flow due to mysterious validation errors -- */ diff --git a/Libraries/ReactIOS/ReactIOSStyleAttributes.js b/Libraries/ReactIOS/ReactIOSStyleAttributes.js index b332bec21638ef..5f83523e281f60 100644 --- a/Libraries/ReactIOS/ReactIOSStyleAttributes.js +++ b/Libraries/ReactIOS/ReactIOSStyleAttributes.js @@ -10,22 +10,23 @@ * @flow */ -"use strict"; +'use strict'; +var ImageStylePropTypes = require('ImageStylePropTypes'); var TextStylePropTypes = require('TextStylePropTypes'); var ViewStylePropTypes = require('ViewStylePropTypes'); -var deepDiffer = require('deepDiffer'); var keyMirror = require('keyMirror'); var matricesDiffer = require('matricesDiffer'); -var merge = require('merge'); +var sizesDiffer = require('sizesDiffer'); -var ReactIOSStyleAttributes = merge( - keyMirror(ViewStylePropTypes), - keyMirror(TextStylePropTypes) -); +var ReactIOSStyleAttributes = { + ...keyMirror(ViewStylePropTypes), + ...keyMirror(TextStylePropTypes), + ...keyMirror(ImageStylePropTypes), +}; ReactIOSStyleAttributes.transformMatrix = { diff: matricesDiffer }; -ReactIOSStyleAttributes.shadowOffset = { diff: deepDiffer }; +ReactIOSStyleAttributes.shadowOffset = { diff: sizesDiffer }; module.exports = ReactIOSStyleAttributes; diff --git a/Libraries/ReactIOS/requireNativeComponent.js b/Libraries/ReactIOS/requireNativeComponent.js new file mode 100644 index 00000000000000..55ad8a6b989e9e --- /dev/null +++ b/Libraries/ReactIOS/requireNativeComponent.js @@ -0,0 +1,74 @@ +/** + * 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 requireNativeComponent + * @flow + */ +'use strict'; + +var RCTUIManager = require('NativeModules').UIManager; +var UnimplementedView = require('UnimplementedView'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var deepDiffer = require('deepDiffer'); +var insetsDiffer = require('insetsDiffer'); +var pointsDiffer = require('pointsDiffer'); +var matricesDiffer = require('matricesDiffer'); +var sizesDiffer = require('sizesDiffer'); +var verifyPropTypes = require('verifyPropTypes'); + +/** + * Used to create React components that directly wrap native component + * implementations. Config information is extracted from data exported from the + * RCTUIManager module. You should also wrap the native component in a + * hand-written component with full propTypes definitions and other + * documentation - pass the hand-written component in as `wrapperComponent` to + * verify all the native props are documented via `propTypes`. + * + * If some native props shouldn't be exposed in the wrapper interface, you can + * pass null for `wrapperComponent` and call `verifyPropTypes` directly + * with `nativePropsToIgnore`; + * + * Common types are lined up with the appropriate prop differs with + * `TypeToDifferMap`. Non-scalar types not in the map default to `deepDiffer`. + */ +function requireNativeComponent( + viewName: string, + wrapperComponent: ?Function +): Function { + var viewConfig = RCTUIManager.viewConfigs && RCTUIManager.viewConfigs[viewName]; + if (!viewConfig) { + return UnimplementedView; + } + var nativeProps = { + ...RCTUIManager.viewConfigs.RCTView.nativeProps, + ...viewConfig.nativeProps, + }; + viewConfig.validAttributes = {}; + for (var key in nativeProps) { + // TODO: deep diff by default in diffRawProperties instead of setting it here + var differ = TypeToDifferMap[nativeProps[key].type] || deepDiffer; + viewConfig.validAttributes[key] = {diff: differ}; + } + if (__DEV__) { + wrapperComponent && verifyPropTypes(wrapperComponent, viewConfig); + } + return createReactIOSNativeComponentClass(viewConfig); +} + +var TypeToDifferMap = { + // iOS Types + CATransform3D: matricesDiffer, + CGPoint: pointsDiffer, + CGSize: sizesDiffer, + UIEdgeInsets: insetsDiffer, + // Android Types + // (not yet implemented) +}; + +module.exports = requireNativeComponent; diff --git a/Libraries/ReactIOS/verifyPropTypes.js b/Libraries/ReactIOS/verifyPropTypes.js new file mode 100644 index 00000000000000..032e572ece118d --- /dev/null +++ b/Libraries/ReactIOS/verifyPropTypes.js @@ -0,0 +1,40 @@ +/** + * 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 verifyPropTypes + * @flow + */ +'use strict'; + +var ReactIOSStyleAttributes = require('ReactIOSStyleAttributes'); +var View = require('View'); + +function verifyPropTypes( + component: Function, + viewConfig: Object, + nativePropsToIgnore?: Object +) { + if (!viewConfig) { + return; // This happens for UnimplementedView. + } + var nativeProps = viewConfig.nativeProps; + for (var prop in viewConfig.nativeProps) { + if (!component.propTypes[prop] && + !View.propTypes[prop] && + !ReactIOSStyleAttributes[prop] && + (!nativePropsToIgnore || !nativePropsToIgnore[prop])) { + throw new Error( + '`' + component.displayName + '` has no propType for native prop `' + + viewConfig.uiViewClassName + '.' + prop + '` of native type `' + + nativeProps[prop].type + '`' + ); + } + } +} + +module.exports = verifyPropTypes; diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index 80d564a2b5899f..fe28a7684c9d46 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -17,6 +17,24 @@ var invariant = require('invariant'); var dimensions = NativeModules.UIManager.Dimensions; +// We calculate the window dimensions in JS so that we don't encounter loss of +// precision in transferring the dimensions (which could be non-integers) over +// the bridge. +if (dimensions.windowPhysicalPixels) { + // parse/stringify => Clone hack + dimensions = JSON.parse(JSON.stringify(dimensions)); + + var windowPhysicalPixels = dimensions.windowPhysicalPixels; + dimensions.window = { + width: windowPhysicalPixels.width / windowPhysicalPixels.scale, + height: windowPhysicalPixels.height / windowPhysicalPixels.scale, + scale: windowPhysicalPixels.scale, + }; + + // delete so no callers rely on this existing + delete dimensions.windowPhysicalPixels; +} + class Dimensions { /** * This should only be called from native code. diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 9d6eb867e471c2..c047d06deecf93 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -307,6 +307,25 @@ var MessageQueueMixin = { ); }, + processBatch: function (batch) { + var self = this; + 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(); + }, + setLoggingEnabled: function(enabled) { this._enableLogging = enabled; this._loggedIncomingItems = []; diff --git a/Libraries/Utilities/differ/sizesDiffer.js b/Libraries/Utilities/differ/sizesDiffer.js new file mode 100644 index 00000000000000..3bdc72acb23384 --- /dev/null +++ b/Libraries/Utilities/differ/sizesDiffer.js @@ -0,0 +1,19 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule sizesDiffer + */ +'use strict'; + +var dummySize = {w: undefined, h: undefined}; + +var sizesDiffer = function(one, two) { + one = one || dummySize; + two = two || dummySize; + return one !== two && ( + one.w !== two.w || + one.h !== two.h + ); +}; + +module.exports = sizesDiffer; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 1aa978f46caab7..01bff7eae5dfb0 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -59,6 +59,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { // Plugins DeviceEventEmitter: require('RCTDeviceEventEmitter'), NativeModules: require('NativeModules'), + requireNativeComponent: require('requireNativeComponent'), addons: { LinkedStateMixin: require('LinkedStateMixin'), diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 8aa83723c5ed36..5c6bcf7cb35a5b 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -42,6 +42,45 @@ 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 1 + +#ifdef DEBUG + +#define RCT_PROFILE_START() \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +NSTimeInterval __rct_profile_start = CACurrentMediaTime() \ +_Pragma("clang diagnostic pop") + +#define RCT_PROFILE_END(cat, args, profileName...) \ +do { \ +if (_profile) { \ + [_profileLock lock]; \ + [_profile addObject:@{ \ + @"name": [@[profileName] componentsJoinedByString: @"_"], \ + @"cat": @ #cat, \ + @"ts": @((NSUInteger)((__rct_profile_start - _startingTime) * 1e6)), \ + @"dur": @((NSUInteger)((CACurrentMediaTime() - __rct_profile_start) * 1e6)), \ + @"ph": @"X", \ + @"pid": @([[NSProcessInfo processInfo] processIdentifier]), \ + @"tid": [[NSThread currentThread] description], \ + @"args": args ?: [NSNull null], \ + }]; \ + [_profileLock unlock]; \ +} \ +} while(0) + +#else + +#define RCT_PROFILE_START(...) +#define RCT_PROFILE_END(...) + +#endif + #ifdef __LP64__ typedef uint64_t RCTHeaderValue; typedef struct section_64 RCTHeaderSection; @@ -191,10 +230,16 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { @interface RCTBridge () +@property (nonatomic, copy, readonly) NSArray *profile; + - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args; +- (void)_actuallyInvokeAndProcessModule:(NSString *)module + method:(NSString *)method + arguments:(NSArray *)args; + @end /** @@ -754,7 +799,13 @@ @implementation RCTBridge RCTBridgeModuleProviderBlock _moduleProvider; RCTDisplayLink *_displayLink; NSMutableSet *_frameUpdateObservers; + NSMutableArray *_scheduledCalls; + NSMutableArray *_scheduledCallbacks; BOOL _loading; + + NSUInteger _startingTime; + NSMutableArray *_profile; + NSLock *_profileLock; } static id _latestJSExecutor; @@ -782,6 +833,8 @@ - (void)setUp _shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL); _displayLink = [[RCTDisplayLink alloc] initWithBridge:self]; _frameUpdateObservers = [[NSMutableSet alloc] init]; + _scheduledCalls = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[NSMutableArray alloc] init]; // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; @@ -1005,20 +1058,54 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args } } +/** + * Private hack to support `setTimeout(fn, 0)` + */ +- (void)_immediatelyCallTimer:(NSNumber *)timer +{ + NSString *moduleDotMethod = @"RCTJSTimers.callTimers"; + NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod]; + RCTAssert(moduleID != nil, @"Module '%@' not registered.", + [[moduleDotMethod componentsSeparatedByString:@"."] firstObject]); + + NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; + RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); + + if (!_loading) { +#if BATCHED_BRIDGE + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, @[@[timer]]]]; + +#else + + [self _invokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, @[@[timer]]]]; +#endif + } +} + - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); + RCT_PROFILE_START(); [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { + RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script"); if (scriptLoadError) { onComplete(scriptLoadError); return; } + RCT_PROFILE_START(); [_javaScriptExecutor executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] callback:^(id json, NSError *error) { + RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue"); + RCT_PROFILE_START(); [self _handleBuffer:json]; + RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); onComplete(error); }]; }]; @@ -1028,11 +1115,46 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete: - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args { +#if BATCHED_BRIDGE + RCT_PROFILE_START(); + + if ([module isEqualToString:@"RCTEventEmitter"]) { + for (NSDictionary *call in _scheduledCalls) { + if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) { + [_scheduledCalls removeObject:call]; + } + } + } + + id call = @{ + @"module": module, + @"method": method, + @"args": args, + }; + + if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { + [_scheduledCallbacks addObject:call]; + } else { + [_scheduledCalls addObject:call]; + } + + RCT_PROFILE_END(js_call, args, @"schedule", module, method); +} + +- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args +{ +#endif [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; + NSString *moduleDotMethod = [NSString stringWithFormat:@"%@.%@", module, method]; + RCT_PROFILE_START(); RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { [[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil]; + RCT_PROFILE_END(js_call, args, moduleDotMethod); + + RCT_PROFILE_START(); [self _handleBuffer:json]; + RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); }; [_javaScriptExecutor executeJSCall:module @@ -1151,12 +1273,34 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i - (void)_update:(CADisplayLink *)displayLink { + RCT_PROFILE_START(); + RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; for (id observer in _frameUpdateObservers) { if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { [observer didUpdateFrame:frameUpdate]; } } + + [self _runScheduledCalls]; + + RCT_PROFILE_END(display_link, nil, @"main_thread"); +} + +- (void)_runScheduledCalls +{ +#if BATCHED_BRIDGE + + NSArray *calls = [_scheduledCallbacks arrayByAddingObjectsFromArray:_scheduledCalls]; + if (calls.count > 0) { + _scheduledCalls = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[NSMutableArray alloc] init]; + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"processBatch" + arguments:@[calls]]; + } + +#endif } - (void)addFrameUpdateObserver:(id)observer @@ -1194,4 +1338,42 @@ + (void)logMessage:(NSString *)message level:(NSString *)level callback:^(id json, NSError *error) {}]; } +- (void)startProfiling +{ + if (![_bundleURL.scheme isEqualToString:@"http"]) { + RCTLogError(@"To run the profiler you must be running from the dev server"); + return; + } + _profileLock = [[NSLock alloc] init]; + _startingTime = CACurrentMediaTime(); + + [_profileLock lock]; + _profile = [[NSMutableArray alloc] init]; + [_profileLock unlock]; +} + +- (void)stopProfiling +{ + [_profileLock lock]; + NSArray *profile = _profile; + _profile = nil; + [_profileLock unlock]; + _profileLock = nil; + + NSString *log = RCTJSONStringify(profile, NULL); + NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", _bundleURL.scheme, _bundleURL.host, _bundleURL.port]; + NSURL *URL = [NSURL URLWithString:URLString]; + NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; + URLRequest.HTTPMethod = @"POST"; + [URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + NSURLSessionTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest + fromData:[log dataUsingEncoding:NSUTF8StringEncoding] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + RCTLogError(@"%@", error.localizedDescription); + } + }]; + [task resume]; +} + @end diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index a0ebc25c1e4836..7621b1955f3db8 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -14,6 +14,15 @@ #import "RCTSourceCode.h" #import "RCTWebViewExecutor.h" +@interface RCTBridge (RCTDevMenu) + +@property (nonatomic, copy, readonly) NSArray *profile; + +- (void)startProfiling; +- (void)stopProfiling; + +@end + @interface RCTDevMenu () @end @@ -37,11 +46,12 @@ - (void)show NSString *debugTitleChrome = _bridge.executorClass != Nil && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging"; NSString *debugTitleSafari = _bridge.executorClass == [RCTWebViewExecutor class] ? @"Disable Safari Debugging" : @"Enable Safari Debugging"; NSString *liveReloadTitle = _liveReload ? @"Disable Live Reload" : @"Enable Live Reload"; + NSString *profilingTitle = _bridge.profile ? @"Stop Profiling" : @"Start Profiling"; UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil - otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, nil]; + otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil]; actionSheet.actionSheetStyle = UIBarStyleBlack; [actionSheet showInView:[[[[UIApplication sharedApplication] keyWindow] rootViewController] view]]; } @@ -61,6 +71,12 @@ - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger } else if (buttonIndex == 3) { _liveReload = !_liveReload; [self _pollAndReload]; + } else if (buttonIndex == 4) { + if (_bridge.profile) { + [_bridge stopProfiling]; + } else { + [_bridge startProfiling]; + } } } diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 1dbe714c8d136e..c9a97dfd134e6a 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -68,7 +68,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _bridge = bridge; _moduleName = moduleName; - _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bundleFinishedLoading) @@ -105,7 +104,6 @@ - (void)invalidate - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - [_touchHandler invalidate]; if (_contentView) { [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" args:@[_contentView.reactTag]]; @@ -148,7 +146,6 @@ - (void)bundleFinishedLoading * NOTE: Since the bridge persists, the RootViews might be reused, so now * the react tag is assigned every time we load new content. */ - [_touchHandler invalidate]; [_contentView removeFromSuperview]; _contentView = [[UIView alloc] initWithFrame:self.bounds]; _contentView.reactTag = [_bridge.uiManager allocateRootTag]; diff --git a/React/Base/RCTTouchHandler.h b/React/Base/RCTTouchHandler.h index 7b52e6f806a4b8..9d3f1cbd4f92bd 100644 --- a/React/Base/RCTTouchHandler.h +++ b/React/Base/RCTTouchHandler.h @@ -9,14 +9,12 @@ #import -#import "RCTInvalidating.h" +#import "RCTFrameUpdate.h" @class RCTBridge; -@interface RCTTouchHandler : UIGestureRecognizer +@interface RCTTouchHandler : UIGestureRecognizer - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; -- (void)startOrResetInteractionTiming; -- (NSDictionary *)endAndResetInteractionTiming; @end diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index 910ae950ef14df..bd731a99844715 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -20,39 +20,6 @@ // TODO: this class behaves a lot like a module, and could be implemented as a // module if we were to assume that modules and RootViews had a 1:1 relationship - -@interface RCTTouchEvent : NSObject - -@property (nonatomic, assign, readonly) NSUInteger id; -@property (nonatomic, copy, readonly) NSString *eventName; -@property (nonatomic, copy, readonly) NSArray *touches; -@property (nonatomic, copy, readonly) NSArray *changedIndexes; -@property (nonatomic, assign, readonly) CFTimeInterval originatingTime; - -@end - - -@implementation RCTTouchEvent - -+ (instancetype)touchWithEventName:(NSString *)eventName touches:(NSArray *)touches changedIndexes:(NSArray *)changedIndexes originatingTime:(CFTimeInterval)originatingTime -{ - RCTTouchEvent *touchEvent = [[self alloc] init]; - touchEvent->_id = [self newTaskID]; - touchEvent->_eventName = [eventName copy]; - touchEvent->_touches = [touches copy]; - touchEvent->_changedIndexes = [changedIndexes copy]; - touchEvent->_originatingTime = originatingTime; - return touchEvent; -} - -+ (NSUInteger)newTaskID -{ - static NSUInteger taskID = 0; - return ++taskID; -} - -@end - @implementation RCTTouchHandler { __weak RCTBridge *_bridge; @@ -69,7 +36,6 @@ @implementation RCTTouchHandler BOOL _recordingInteractionTiming; CFTimeInterval _mostRecentEnqueueJS; - CADisplayLink *_displayLink; NSMutableArray *_pendingTouches; NSMutableArray *_bridgeInteractionTiming; } @@ -86,12 +52,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _reactTouches = [[NSMutableArray alloc] init]; _touchViews = [[NSMutableArray alloc] init]; - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; _pendingTouches = [[NSMutableArray alloc] init]; _bridgeInteractionTiming = [[NSMutableArray alloc] init]; - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - // `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. self.cancelsTouchesInView = NO; @@ -99,17 +62,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge return self; } -- (BOOL)isValid -{ - return _displayLink != nil; -} - -- (void)invalidate -{ - [_displayLink invalidate]; - _displayLink = nil; -} - typedef NS_ENUM(NSInteger, RCTTouchEventType) { RCTTouchEventTypeStart, RCTTouchEventTypeMove, @@ -216,7 +168,6 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime { // Update touches - CFTimeInterval enqueueTime = CACurrentMediaTime(); NSMutableArray *changedIndexes = [[NSMutableArray alloc] init]; for (UITouch *touch in touches) { NSInteger index = [_nativeTouches indexOfObject:touch]; @@ -239,71 +190,8 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventNa [reactTouches addObject:[touch copy]]; } - RCTTouchEvent *touch = [RCTTouchEvent touchWithEventName:eventName - touches:reactTouches - changedIndexes:changedIndexes - originatingTime:originatingTime]; - [_pendingTouches addObject:touch]; - - if (_recordingInteractionTiming) { - [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(touch.originatingTime), - @"operation": @"taskOriginated", - @"taskID": @(touch.id), - }]; - [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(enqueueTime), - @"operation": @"taskEnqueuedPending", - @"taskID": @(touch.id), - }]; - } -} - -- (void)_update:(CADisplayLink *)sender -{ - // Dispatch touch event - NSUInteger pendingCount = _pendingTouches.count; - for (RCTTouchEvent *touch in _pendingTouches) { - _mostRecentEnqueueJS = CACurrentMediaTime(); - [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" - args:@[touch.eventName, touch.touches, touch.changedIndexes]]; - } - - if (_recordingInteractionTiming) { - for (RCTTouchEvent *touch in _pendingTouches) { - [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(sender.timestamp), - @"operation": @"frameAlignedDispatch", - @"taskID": @(touch.id), - }]; - } - - if (pendingCount > 0 || sender.timestamp - _mostRecentEnqueueJS < 0.1) { - [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(sender.timestamp), - @"operation": @"mainThreadDisplayLink", - @"taskID": @([RCTTouchEvent newTaskID]), - }]; - } - } - - [_pendingTouches removeAllObjects]; -} - -- (void)startOrResetInteractionTiming -{ - RCTAssertMainThread(); - [_bridgeInteractionTiming removeAllObjects]; - _recordingInteractionTiming = YES; -} - -- (NSDictionary *)endAndResetInteractionTiming -{ - RCTAssertMainThread(); - _recordingInteractionTiming = NO; - NSArray *_prevInteractionTimingData = _bridgeInteractionTiming; - _bridgeInteractionTiming = [[NSMutableArray alloc] init]; - return @{ @"interactionTiming": _prevInteractionTimingData }; + [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" + args:@[eventName, reactTouches, changedIndexes]]; } #pragma mark - Gesture Recognizer Delegate Callbacks diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 1d75e78afa1110..8475e2afa311f2 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -218,7 +218,7 @@ - (void)invalidate - (void)dealloc { - RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); + [self invalidate]; } - (void)executeJSCall:(NSString *)name diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index ce8688f625fecb..aaab5fae006c61 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -15,6 +15,15 @@ #import "RCTSparseArray.h" #import "RCTUtils.h" +@interface RCTBridge (Private) + +/** + * Allow super fast, one time, timers to skip the queue and be directly executed + */ +- (void)_immediatelyCallTimer:(NSNumber *)timer; + +@end + @interface RCTTimer : NSObject @property (nonatomic, strong, readonly) NSDate *target; @@ -160,7 +169,7 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update { if (jsDuration == 0 && repeats == NO) { // For super fast, one-off timers, just enqueue them immediately rather than waiting a frame. - [_bridge enqueueJSCall:@"RCTJSTimers.callTimers" args:@[@[callbackID]]]; + [_bridge _immediatelyCallTimer:callbackID]; return; } diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 86c58d0dd06f0a..b6a350dcd4964e 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -193,6 +193,7 @@ @implementation RCTUIManager NSMutableDictionary *_defaultShadowViews; // RCT thread only NSMutableDictionary *_defaultViews; // Main thread only NSDictionary *_viewManagers; + NSDictionary *_viewConfigs; NSUInteger _rootTag; } @@ -219,6 +220,28 @@ @implementation RCTUIManager return name; } +static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewName) +{ + NSMutableDictionary *nativeProps = [[NSMutableDictionary alloc] init]; + static const char *prefix = "getPropConfig"; + static const NSUInteger prefixLength = sizeof("getPropConfig") - 1; + unsigned int methodCount = 0; + Method *methods = class_copyMethodList(objc_getMetaClass(class_getName(managerClass)), &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + Method method = methods[i]; + SEL getInfo = method_getName(method); + const char *selName = sel_getName(getInfo); + if (strlen(selName) > prefixLength && strncmp(selName, prefix, prefixLength) == 0) { + NSDictionary *info = ((NSDictionary *(*)(id, SEL))method_getImplementation(method))(managerClass, getInfo); + nativeProps[info[@"name"]] = info; + } + } + return @{ + @"uiViewClassName": viewName, + @"nativeProps": nativeProps + }; +} + /** * This private constructor should only be called when creating * isolated UIImanager instances for testing. Normal initialization @@ -292,13 +315,17 @@ - (void)setBridge:(RCTBridge *)bridge // Get view managers from bridge NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *viewConfigs = [[NSMutableDictionary alloc] init]; [_bridge.modules enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, RCTViewManager *manager, BOOL *stop) { if ([manager isKindOfClass:[RCTViewManager class]]) { - viewManagers[RCTViewNameForModuleName(moduleName)] = manager; + NSString *viewName = RCTViewNameForModuleName(moduleName); + viewManagers[viewName] = manager; + viewConfigs[viewName] = RCTViewConfigForModule([manager class], viewName); } }]; _viewManagers = [viewManagers copy]; + _viewConfigs = [viewConfigs copy]; } - (void)registerRootView:(UIView *)rootView; @@ -650,14 +677,12 @@ - (void)_manageChildren:(NSNumber *)containerReactTag [self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry]; - // TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient - // Figure out what to insert - merge temporary inserts and adds NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary]; - for (NSInteger index = 0; index < temporarilyRemovedChildren.count; index++) { + for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) { destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index]; } - for (NSInteger index = 0; index < addAtIndices.count; index++) { + for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) { id view = registry[addChildReactTags[index]]; if (view) { destinationsToChildrenToAdd[addAtIndices[index]] = view; @@ -1374,7 +1399,7 @@ - (NSDictionary *)constantsToExport allJSConstants[name] = [constantsNamespace copy]; } }]; - + allJSConstants[@"viewConfigs"] = _viewConfigs; return allJSConstants; } @@ -1393,41 +1418,6 @@ - (NSDictionary *)constantsToExport callback:callback]; } -RCT_EXPORT_METHOD(startOrResetInteractionTiming) -{ - NSSet *rootViewTags = [_rootViewTags copy]; - [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - for (NSNumber *reactTag in rootViewTags) { - UIView *rootView = viewRegistry[reactTag]; - for (RCTTouchHandler *handler in rootView.gestureRecognizers) { - if ([handler isKindOfClass:[RCTTouchHandler class]]) { - [handler startOrResetInteractionTiming]; - break; - } - } - } - }]; -} - -RCT_EXPORT_METHOD(endAndResetInteractionTiming:(RCTResponseSenderBlock)onSuccess - onError:(RCTResponseSenderBlock)onError) -{ - NSSet *rootViewTags = [_rootViewTags copy]; - [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - NSMutableDictionary *timingData = [[NSMutableDictionary alloc] init]; - for (NSNumber *reactTag in rootViewTags) { - UIView *rootView = viewRegistry[reactTag]; - for (RCTTouchHandler *handler in rootView.gestureRecognizers) { - if ([handler isKindOfClass:[RCTTouchHandler class]]) { - [handler endAndResetInteractionTiming]; - break; - } - } - } - onSuccess(@[timingData]); - }]; -} - static UIView *_jsResponder; + (UIView *)JSResponder diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 187303ac282481..5037d3390b1423 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -47,27 +47,27 @@ - (void)dealloc [_regionChangeObserveTimer invalidate]; } +- (void)reactSetFrame:(CGRect)frame +{ + self.frame = frame; +} + - (void)layoutSubviews { [super layoutSubviews]; - // Force resize subviews - only the layer is resized by default - CGRect mapFrame = self.frame; - self.frame = CGRectZero; - self.frame = mapFrame; - if (_legalLabel) { dispatch_async(dispatch_get_main_queue(), ^{ CGRect frame = _legalLabel.frame; if (_legalLabelInsets.left) { frame.origin.x = _legalLabelInsets.left; } else if (_legalLabelInsets.right) { - frame.origin.x = mapFrame.size.width - _legalLabelInsets.right - frame.size.width; + frame.origin.x = self.frame.size.width - _legalLabelInsets.right - frame.size.width; } if (_legalLabelInsets.top) { frame.origin.y = _legalLabelInsets.top; } else if (_legalLabelInsets.bottom) { - frame.origin.y = mapFrame.size.height - _legalLabelInsets.bottom - frame.size.height; + frame.origin.y = self.frame.size.height - _legalLabelInsets.bottom - frame.size.height; } _legalLabel.frame = frame; }); @@ -93,7 +93,7 @@ - (void)setShowsUserLocation:(BOOL)showsUserLocation } } -- (void)setRegion:(MKCoordinateRegion)region +- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated { // If location is invalid, abort if (!CLLocationCoordinate2DIsValid(region.center)) { @@ -109,7 +109,7 @@ - (void)setRegion:(MKCoordinateRegion)region } // Animate to new position - [super setRegion:region animated:YES]; + [super setRegion:region animated:animated]; } - (void)setAnnotations:(MKShapeArray *)annotations diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index 52b635fd6b1d5d..8de35141586a29 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -41,9 +41,11 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat) RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat) RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets) -RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion) RCT_EXPORT_VIEW_PROPERTY(annotations, MKShapeArray) - +RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) +{ + [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES]; +} #pragma mark MKMapViewDelegate diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index 92336fa410d543..74c7bbd8ce7a17 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -109,6 +109,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * within the view or shadowView. */ #define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) \ +RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forView:(id)view withDefaultView:(id)defaultView { \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ @@ -117,6 +118,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v } #define RCT_REMAP_SHADOW_PROPERTY(name, keyPath, type) \ +RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forShadowView:(id)view withDefaultView:(id)defaultView { \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ @@ -130,9 +132,11 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * refer to "json", "view" and "defaultView" to implement the required logic. */ #define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) \ +RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forView:(viewClass *)view withDefaultView:(viewClass *)defaultView #define RCT_CUSTOM_SHADOW_PROPERTY(name, type, viewClass) \ +RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView /** @@ -160,4 +164,17 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v [self set_##newName:json forView:view withDefaultView:defaultView]; \ } +/** + * PROP_CONFIG macros should only be paired with property setters. + */ +#define RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ ++ (NSDictionary *)getPropConfigView_##name { \ + return @{@"name": @#name, @"type": @#type}; \ +} + +#define RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ ++ (NSDictionary *)getPropConfigShadow_##name { \ + return @{@"name": @#name, @"type": @#type}; \ +} + @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 9758a09443cab1..3c848537454872 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -61,7 +61,6 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) #pragma mark - View properties RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel, NSString) -RCT_EXPORT_VIEW_PROPERTY(hidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_REMAP_VIEW_PROPERTY(accessible, isAccessibilityElement, BOOL) RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString) diff --git a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js new file mode 100644 index 00000000000000..eede72c0ca3cbb --- /dev/null +++ b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -0,0 +1,85 @@ +'use strict'; + +jest + .autoMockOff() + .mock('../../lib/declareOpts') + .mock('fs'); + +var fs = require('fs'); +var AssetServer = require('../'); +var Promise = require('bluebird'); + +describe('AssetServer', function() { + pit('should work for the simple case', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.png': 'b image', + 'b@2x.png': 'b2 image', + } + } + }); + + return Promise.all([ + server.get('imgs/b.png'), + server.get('imgs/b@1x.png'), + ]).then(function(resp) { + resp.forEach(function(data) { + expect(data).toBe('b image'); + }); + }); + }); + + pit.only('should pick the bigger one', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b@1x.png': 'b1 image', + 'b@2x.png': 'b2 image', + 'b@4x.png': 'b4 image', + 'b@4.5x.png': 'b4.5 image', + } + } + }); + + return server.get('imgs/b@3x.png').then(function(data) { + expect(data).toBe('b4 image'); + }); + }); + + pit('should support multiple project roots', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.png': 'b image', + }, + 'root2': { + 'newImages': { + 'imgs': { + 'b@1x.png': 'b1 image', + }, + }, + }, + } + }); + + return server.get('newImages/imgs/b.png').then(function(data) { + expect(data).toBe('b1 image'); + }); + }); +}); diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js new file mode 100644 index 00000000000000..bdabafff4a177a --- /dev/null +++ b/packager/react-packager/src/AssetServer/index.js @@ -0,0 +1,132 @@ +/** + * 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'; + +var declareOpts = require('../lib/declareOpts'); +var extractAssetResolution = require('../lib/extractAssetResolution'); +var path = require('path'); +var Promise = require('bluebird'); +var fs = require('fs'); + +var lstat = Promise.promisify(fs.lstat); +var readDir = Promise.promisify(fs.readdir); +var readFile = Promise.promisify(fs.readFile); + +module.exports = AssetServer; + +var validateOpts = declareOpts({ + projectRoots: { + type: 'array', + required: true, + }, + assetExts: { + type: 'array', + default: ['png'], + }, +}); + +function AssetServer(options) { + var opts = validateOpts(options); + this._roots = opts.projectRoots; + this._assetExts = opts.assetExts; +} + +/** + * Given a request for an image by path. That could contain a resolution + * postfix, we need to find that image (or the closest one to it's resolution) + * in one of the project roots: + * + * 1. We first parse the directory of the asset + * 2. We check to find a matching directory in one of the project roots + * 3. We then build a map of all assets and their resolutions in this directory + * 4. Then pick the closest resolution (rounding up) to the requested one + */ + +AssetServer.prototype.get = function(assetPath) { + var filename = path.basename(assetPath); + + return findRoot( + this._roots, + path.dirname(assetPath) + ).then(function(dir) { + return [ + dir, + readDir(dir), + ]; + }).spread(function(dir, files) { + // Easy case. File exactly what the client requested. + var index = files.indexOf(filename); + if (index > -1) { + return readFile(path.join(dir, filename)); + } + + var assetData = extractAssetResolution(filename); + var map = buildAssetMap(dir, files); + var record = map[assetData.assetName]; + + if (!record) { + throw new Error('Asset not found'); + } + + for (var i = 0; i < record.resolutions.length; i++) { + if (record.resolutions[i] >= assetData.resolution) { + return readFile(record.files[i]); + } + } + + return readFile(record.files[record.files.length - 1]); + }); +}; + +function findRoot(roots, dir) { + return Promise.some( + roots.map(function(root) { + var absPath = path.join(root, dir); + return lstat(absPath).then(function(stat) { + if (!stat.isDirectory()) { + throw new Error('Looking for dirs'); + } + stat._path = absPath; + return stat; + }); + }), + 1 + ).spread( + function(stat) { + return stat._path; + } + ); +} + +function buildAssetMap(dir, files) { + var assets = files.map(extractAssetResolution); + var map = Object.create(null); + assets.forEach(function(asset, i) { + var file = files[i]; + var record = map[asset.assetName]; + if (!record) { + record = map[asset.assetName] = { + resolutions: [], + files: [], + }; + } + + var insertIndex; + var length = record.resolutions.length; + for (insertIndex = 0; insertIndex < length; insertIndex++) { + if (asset.resolution < record.resolutions[insertIndex]) { + break; + } + } + record.resolutions.splice(insertIndex, 0, asset.resolution); + record.files.splice(insertIndex, 0, path.join(dir, file)); + }); + + return map; +} diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index 40705911670419..98ae7eb73f8f73 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -14,6 +14,7 @@ jest .dontMock('absolute-path') .dontMock('../docblock') .dontMock('../../replacePatterns') + .dontMock('../../../../lib/extractAssetResolution') .setMock('../../../ModuleDescriptor', function(data) {return data;}); describe('DependencyGraph', function() { diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index f92a3195084e75..9257d788b1baa9 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -18,6 +18,7 @@ var isAbsolutePath = require('absolute-path'); var debug = require('debug')('DependecyGraph'); var util = require('util'); var declareOpts = require('../../../lib/declareOpts'); +var extractAssetResolution = require('../../../lib/extractAssetResolution'); var readFile = Promise.promisify(fs.readFile); var readDir = Promise.promisify(fs.readdir); @@ -421,7 +422,7 @@ DependecyGraph.prototype._processModule = function(modulePath) { var module; if (this._assetExts.indexOf(extname(modulePath)) > -1) { - var assetData = extractResolutionPostfix(this._lookupName(modulePath)); + var assetData = extractAssetResolution(this._lookupName(modulePath)); moduleData.id = assetData.assetName; moduleData.resolution = assetData.resolution; moduleData.isAsset = true; @@ -772,28 +773,6 @@ function extname(name) { return path.extname(name).replace(/^\./, ''); } -function extractResolutionPostfix(filename) { - var ext = extname(filename); - var re = new RegExp('@([\\d\\.]+)x\\.' + ext + '$'); - - var match = filename.match(re); - var resolution; - - if (!(match && match[1])) { - resolution = 1; - } else { - resolution = parseFloat(match[1], 10); - if (isNaN(resolution)) { - resolution = 1; - } - } - - return { - resolution: resolution, - assetName: match ? filename.replace(re, '.' + ext) : filename, - }; -} - function NotFoundError() { Error.call(this); Error.captureStackTrace(this, this.constructor); diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js index 78298a2fcc26cf..8df5bbcc05ab41 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js @@ -23,7 +23,7 @@ get: function() { console.error( 'Your code is broken! Do not iterate over arrays with ' + - 'for...in. See https://fburl.com/31944000 for more information.' + 'for...in.' ); } } @@ -49,9 +49,6 @@ * - Use a regular for loop with index. * - Use one of the array methods: a.forEach, a.map, etc. * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - * - * More info: - * https://our.intern.facebook.com/intern/dex/qa/669736606441771/ */ if (this == null) { throw new TypeError( @@ -92,9 +89,6 @@ * - Use a regular for loop with index. * - Use one of the array methods: a.forEach, a.map, etc. * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - * - * More info: - * https://our.intern.facebook.com/intern/dex/qa/669736606441771/ */ if (this == null) { throw new TypeError('Array.prototype.find called on null or undefined'); diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Packager/__tests__/Packager-test.js index cc5c4471d79e26..0c9d4a84d6340f 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Packager/__tests__/Packager-test.js @@ -111,7 +111,7 @@ describe('Packager', function() { var imgModule = { isStatic: true, path: '/root/img/new_image.png', - uri: 'img/new_image.png', + uri: 'assets/img/new_image.png', width: 25, height: 50, }; diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index 9e94be21323534..74e2ff4c358709 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -194,7 +194,7 @@ function generateAssetModule(module, relPath) { var img = { isStatic: true, path: module.path, //TODO(amasad): this should be path inside tar file. - uri: relPath, + uri: path.join('assets', relPath), width: dimensions.width / module.resolution, height: dimensions.height / module.resolution, }; diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index a7b6502133b5b0..58a1aca798cb6f 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -229,4 +229,32 @@ describe('processRequest', function() { expect(res.end).not.toBeCalled(); }); }); + + describe.only('/assets endpoint', function() { + var AssetServer; + beforeEach(function() { + AssetServer = require('../../AssetServer'); + }); + + it('should serve simple case', function() { + var req = { + url: '/assets/imgs/a.png', + }; + var res = { + end: jest.genMockFn(), + }; + + AssetServer.prototype.get.mockImpl(function() { + return Promise.resolve('i am image'); + }); + + server.processRequest(req, res); + jest.runAllTimers(); + expect(res.end).toBeCalledWith('i am image'); + }); + + it('should return 404', function() { + + }); + }); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 617359bf8f4073..3c7be04355e558 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -14,8 +14,11 @@ var declareOpts = require('../lib/declareOpts'); var FileWatcher = require('../FileWatcher'); var Packager = require('../Packager'); var Activity = require('../Activity'); +var AssetServer = require('../AssetServer'); var Promise = require('bluebird'); var _ = require('underscore'); +var exec = require('child_process').exec; +var fs = require('fs'); module.exports = Server; @@ -99,6 +102,11 @@ function Server(options) { packagerOpts.fileWatcher = this._fileWatcher; this._packager = new Packager(packagerOpts); + this._assetServer = new AssetServer({ + projectRoots: opts.projectRoots, + assetExts: opts.assetExts, + }); + var onFileChange = this._onFileChange.bind(this); this._fileWatcher.on('all', onFileChange); @@ -230,6 +238,58 @@ Server.prototype._processOnChangeRequest = function(req, res) { }); }; +Server.prototype._processAssetsRequest = function(req, res) { + var urlObj = url.parse(req.url, true); + var assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); + this._assetServer.get(assetPath[1]) + .then( + function(data) { + res.end(data); + }, + function(error) { + console.error(error.stack); + res.writeHead('404'); + res.end('Asset not found'); + } + ).done(); +}; + +Server.prototype._processProfile = function(req, res) { + console.log('Dumping profile information...'); + var dumpName = '/tmp/dump_' + Date.now() + '.json'; + var prefix = process.env.TRACE_VIEWER_PATH || ''; + var cmd = path.join(prefix, 'trace2html') + ' ' + dumpName; + fs.writeFileSync(dumpName, req.rawBody); + exec(cmd, function (error) { + if (error) { + if (error.code === 127) { + console.error( + '\n** Failed executing `' + cmd + '` **\n\n' + + 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + + 'You can get it at:\n\n' + + ' https://github.com/google/trace-viewer\n\n' + + 'If it\'s not in your path, you can set a custom path with:\n\n' + + ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + + 'NOTE: Your profile data was kept at:\n\n' + + ' ' + dumpName + ); + } else { + console.error('Unknown error', error); + } + res.end(); + return; + } else { + exec('rm ' + dumpName); + exec('open ' + dumpName.replace(/json$/, 'html'), function (error) { + if (error) { + console.error(error); + } + res.end(); + }); + } + }); +}; + Server.prototype.processRequest = function(req, res, next) { var urlObj = url.parse(req.url, true); var pathname = urlObj.pathname; @@ -245,6 +305,12 @@ Server.prototype.processRequest = function(req, res, next) { } else if (pathname.match(/^\/onchange\/?$/)) { this._processOnChangeRequest(req, res); return; + } else if (pathname.match(/^\/assets\//)) { + this._processAssetsRequest(req, res); + return; + } else if (pathname.match(/^\/profile\/?$/)) { + this._processProfile(req, res); + return; } else { next(); return; diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js b/packager/react-packager/src/__mocks__/fs.js similarity index 96% rename from packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js rename to packager/react-packager/src/__mocks__/fs.js index 3ebee183c02cf1..0ea13d15d04688 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js +++ b/packager/react-packager/src/__mocks__/fs.js @@ -42,6 +42,11 @@ fs.readdir.mockImpl(function(filepath, callback) { }); fs.readFile.mockImpl(function(filepath, encoding, callback) { + if (arguments.length === 2) { + callback = encoding; + encoding = null; + } + try { var node = getToNode(filepath); // dir check diff --git a/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js b/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js new file mode 100644 index 00000000000000..ad5ac3fbfe5edd --- /dev/null +++ b/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js @@ -0,0 +1,42 @@ +'use strict'; + +jest.autoMockOff(); +var extractAssetResolution = require('../extractAssetResolution'); + +describe('extractAssetResolution', function() { + it('should extract resolution simple case', function() { + var data = extractAssetResolution('test@2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 2, + }); + }); + + it('should default resolution to 1', function() { + var data = extractAssetResolution('test.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1, + }); + }); + + it('should support float', function() { + var data = extractAssetResolution('test@1.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1.1, + }); + + data = extractAssetResolution('test@.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.1, + }); + + data = extractAssetResolution('test@0.2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.2, + }); + }); +}); diff --git a/packager/react-packager/src/lib/extractAssetResolution.js b/packager/react-packager/src/lib/extractAssetResolution.js new file mode 100644 index 00000000000000..8fb91afc4ebcff --- /dev/null +++ b/packager/react-packager/src/lib/extractAssetResolution.js @@ -0,0 +1,28 @@ +'use strict'; + +var path = require('path'); + +function extractAssetResolution(filename) { + var ext = path.extname(filename); + + var re = new RegExp('@([\\d\\.]+)x\\' + ext + '$'); + + var match = filename.match(re); + var resolution; + + if (!(match && match[1])) { + resolution = 1; + } else { + resolution = parseFloat(match[1], 10); + if (isNaN(resolution)) { + resolution = 1; + } + } + + return { + resolution: resolution, + assetName: match ? filename.replace(re, ext) : filename, + }; +} + +module.exports = extractAssetResolution;