diff --git a/Examples/UIExplorer/AssetScaledImageExample.js b/Examples/UIExplorer/AssetScaledImageExample.js
new file mode 100644
index 000000000..dbfe7afb7
--- /dev/null
+++ b/Examples/UIExplorer/AssetScaledImageExample.js
@@ -0,0 +1,98 @@
+/**
+ * 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.
+ *
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Image,
+ StyleSheet,
+ View,
+ ScrollView
+} = React;
+
+var AssetScaledImageExample = React.createClass({
+
+ getInitialState() {
+ return {
+ asset: this.props.asset
+ };
+ },
+
+ render() {
+ var image = this.state.asset.node.image;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+});
+
+var styles = StyleSheet.create({
+ row: {
+ padding: 5,
+ flex: 1,
+ flexDirection: 'row',
+ alignSelf: 'center',
+ },
+ textColumn: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ imageWide: {
+ borderWidth: 1,
+ borderColor: 'black',
+ width: 320,
+ height: 240,
+ margin: 5,
+ },
+ imageThumb: {
+ borderWidth: 1,
+ borderColor: 'black',
+ width: 100,
+ height: 100,
+ margin: 5,
+ },
+ imageT1: {
+ borderWidth: 1,
+ borderColor: 'black',
+ width: 212,
+ height: 320,
+ margin: 5,
+ },
+ imageT2: {
+ borderWidth: 1,
+ borderColor: 'black',
+ width: 100,
+ height: 320,
+ margin: 5,
+ },
+});
+
+exports.title = '';
+exports.description = 'Example component that displays the automatic scaling capabilities of the tag';
+module.exports = AssetScaledImageExample;
diff --git a/Examples/UIExplorer/CameraRollExample.ios.js b/Examples/UIExplorer/CameraRollExample.ios.js
index 736784072..d783d9d8e 100644
--- a/Examples/UIExplorer/CameraRollExample.ios.js
+++ b/Examples/UIExplorer/CameraRollExample.ios.js
@@ -24,9 +24,11 @@ var {
SwitchIOS,
Text,
View,
+ TouchableOpacity
} = React;
var CameraRollView = require('./CameraRollView.ios');
+var AssetScaledImageExampleView = require('./AssetScaledImageExample');
var CAMERA_ROLL_VIEW = 'camera_roll_view';
@@ -54,7 +56,7 @@ var CameraRollExample = React.createClass({
{'Group Type: ' + this.state.groupTypes}
@@ -62,24 +64,35 @@ var CameraRollExample = React.createClass({
);
},
+ loadAsset(asset){
+ this.props.navigator.push({
+ title: 'Camera Roll Image',
+ component: AssetScaledImageExampleView,
+ backButtonTitle: 'Back',
+ passProps: { asset: asset },
+ });
+ },
+
_renderImage(asset) {
var imageSize = this.state.bigImages ? 150 : 75;
var imageStyle = [styles.image, {width: imageSize, height: imageSize}];
var location = asset.node.location.longitude ?
JSON.stringify(asset.node.location) : 'Unknown location';
return (
-
-
-
- {asset.node.image.uri}
- {location}
- {asset.node.group_name}
- {new Date(asset.node.timestamp).toString()}
+
+
+
+
+ {asset.node.image.uri}
+ {location}
+ {asset.node.group_name}
+ {new Date(asset.node.timestamp).toString()}
+
-
+
);
},
@@ -115,7 +128,7 @@ var styles = StyleSheet.create({
},
});
-exports.title = '';
+exports.title = 'Camera Roll';
exports.description = 'Example component that uses CameraRoll to list user\'s photos';
exports.examples = [
{
diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js
index b886bf861..82721993f 100644
--- a/Examples/UIExplorer/ImageExample.js
+++ b/Examples/UIExplorer/ImageExample.js
@@ -32,7 +32,7 @@ var NetworkImageExample = React.createClass({
getInitialState: function() {
return {
error: false,
- loading: true,
+ loading: false,
progress: 0
};
},
@@ -47,10 +47,10 @@ var NetworkImageExample = React.createClass({
this.setState({error: e.nativeEvent.error})}
- onLoadProgress={(e) => this.setState({progress: Math.max(0, Math.round(100 * e.nativeEvent.written / e.nativeEvent.total))}) }
- onLoadEnd={() => this.setState({loading: false, error: false})}
- onLoadAbort={() => this.setState({error: 'Loading has aborted'})} >
+ onLoadStart={(e) => this.setState({loading: true})}
+ onError={(e) => this.setState({error: e.nativeEvent.error, loading: false})}
+ onProgress={(e) => this.setState({progress: Math.round(100 * e.nativeEvent.loaded / e.nativeEvent.total)})}
+ onLoad={() => this.setState({loading: false, error: false})}>
{loader}
;
}
diff --git a/Examples/UIExplorer/ListViewPagingExample.js b/Examples/UIExplorer/ListViewPagingExample.js
index ab67c9160..7167f3eea 100644
--- a/Examples/UIExplorer/ListViewPagingExample.js
+++ b/Examples/UIExplorer/ListViewPagingExample.js
@@ -48,19 +48,19 @@ var Thumb = React.createClass({
},
render: function() {
return (
-
-
-
-
-
- {this.state.dir === 'column' ?
-
- Oooo, look at this new text! So awesome it may just be crazy.
- Let me keep typing here so it wraps at least one line.
- :
-
- }
-
+
+
+
+
+ {this.state.dir === 'column' ?
+
+ Oooo, look at this new text! So awesome it may just be crazy.
+ Let me keep typing here so it wraps at least one line.
+ :
+
+ }
);
}
@@ -127,14 +127,12 @@ var ListViewPagingExample = React.createClass({
1 Like :
null;
return (
-
-
- {headerLikeText}
-
-
- Table Header (click me)
-
-
+
+ {headerLikeText}
+
+
+ Table Header (click me)
+
);
diff --git a/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js b/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js
index 5abdcd11f..08b8571ae 100644
--- a/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js
+++ b/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js
@@ -55,26 +55,24 @@ var BreadcrumbNavSample = React.createClass({
return (
navigator.push(_getRandomRoute())}>
-
- {route.title}
-
+ {route.title}
);
},
iconForRoute: function(route, navigator) {
return (
- {
- navigator.popToRoute(route);
- }}>
-
-
+ { navigator.popToRoute(route); }}
+ style={styles.crumbIconPlaceholder}
+ />
);
},
separatorForRoute: function(route, navigator) {
return (
-
-
-
+
);
}
};
diff --git a/Examples/UIExplorer/Navigator/NavigationBarSample.js b/Examples/UIExplorer/Navigator/NavigationBarSample.js
index 545f76b82..3148ad9c1 100644
--- a/Examples/UIExplorer/Navigator/NavigationBarSample.js
+++ b/Examples/UIExplorer/Navigator/NavigationBarSample.js
@@ -51,12 +51,11 @@ var NavigationBarRouteMapper = {
var previousRoute = navState.routeStack[index - 1];
return (
navigator.pop()}>
-
-
- {previousRoute.title}
-
-
+ onPress={() => navigator.pop()}
+ style={styles.navBarLeftButton}>
+
+ {previousRoute.title}
+
);
},
@@ -64,12 +63,11 @@ var NavigationBarRouteMapper = {
RightButton: function(route, navigator, index, navState) {
return (
navigator.push(newRandomRoute())}>
-
-
- Next
-
-
+ onPress={() => navigator.push(newRandomRoute())}
+ style={styles.navBarRightButton}>
+
+ Next
+
);
},
diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js
index 06cc12ee3..b2a42ffeb 100644
--- a/Examples/UIExplorer/TextInputExample.js
+++ b/Examples/UIExplorer/TextInputExample.js
@@ -33,7 +33,7 @@ var WithLabel = React.createClass({
{this.props.children}
);
- }
+ },
});
var TextEventsExample = React.createClass({
@@ -41,13 +41,17 @@ var TextEventsExample = React.createClass({
return {
curText: '',
prevText: '',
+ prev2Text: '',
};
},
updateText: function(text) {
- this.setState({
- curText: text,
- prevText: this.state.curText,
+ this.setState((state) => {
+ return {
+ curText: text,
+ prevText: state.curText,
+ prev2Text: state.prevText,
+ };
});
},
@@ -73,13 +77,43 @@ var TextEventsExample = React.createClass({
/>
{this.state.curText}{'\n'}
- (prev: {this.state.prevText})
+ (prev: {this.state.prevText}){'\n'}
+ (prev2: {this.state.prev2Text})
);
}
});
+class RewriteExample extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {text: ''};
+ }
+ render() {
+ var limit = 20;
+ var remainder = limit - this.state.text.length;
+ var remainderColor = remainder > 5 ? 'blue' : 'red';
+ return (
+
+ {
+ text = text.replace(/ /g, '_');
+ this.setState({text});
+ }}
+ style={styles.default}
+ value={this.state.text}
+ />
+
+ {remainder}
+
+
+ );
+ }
+}
+
var styles = StyleSheet.create({
page: {
paddingBottom: 300,
@@ -125,12 +159,19 @@ var styles = StyleSheet.create({
flex: 1,
},
label: {
- width: 120,
- justifyContent: 'flex-end',
- flexDirection: 'row',
+ width: 115,
+ alignItems: 'flex-end',
marginRight: 10,
paddingTop: 2,
},
+ rewriteContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ remainder: {
+ textAlign: 'right',
+ width: 24,
+ },
});
exports.displayName = (undefined: ?string);
@@ -143,6 +184,12 @@ exports.examples = [
return ;
}
},
+ {
+ title: "Live Re-Write ( -> '_') + maxLength",
+ render: function() {
+ return ;
+ }
+ },
{
title: 'Auto-capitalize',
render: function() {
@@ -268,7 +315,7 @@ exports.examples = [
return (
-
+
);
@@ -276,7 +323,7 @@ exports.examples = [
},
{
title: 'Event handling',
- render: function(): ReactElement { return },
+ render: function(): ReactElement { return ; },
},
{
title: 'Colored input text',
@@ -285,11 +332,11 @@ exports.examples = [
);
@@ -336,7 +383,7 @@ exports.examples = [
@@ -344,7 +391,7 @@ exports.examples = [
diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj
index db89c32b8..dd7200d21 100644
--- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj
+++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1300627E1B59179B0043FE5A /* RCTGzipTests.m */; };
13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FE81AA91428003F314A /* libRCTImage.a */; };
134180011AA9153C003F314A /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FEF1AA914B8003F314A /* libRCTText.a */; };
1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341802B1AA91779003F314A /* libRCTNetwork.a */; };
@@ -19,9 +20,10 @@
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
+ 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */; };
141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; };
143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; };
- 144D21241B2204C5006DB32B /* RCTClippingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClippingTests.m */; };
+ 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClipRectTests.m */; };
147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; };
1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */; };
1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */; };
@@ -156,6 +158,7 @@
/* Begin PBXFileReference section */
004D289E1AAF61C70097A701 /* UIExplorerUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1300627E1B59179B0043FE5A /* RCTGzipTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGzipTests.m; sourceTree = ""; };
13417FE31AA91428003F314A /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; };
13417FEA1AA914B8003F314A /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; };
134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; };
@@ -172,6 +175,7 @@
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 = ""; };
+ 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = ""; };
141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = ""; };
143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = ""; };
@@ -183,7 +187,7 @@
143BC5951B21E3E100462512 /* UIExplorerIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
143BC5981B21E3E100462512 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerSnapshotTests.m; sourceTree = ""; };
- 144D21231B2204C5006DB32B /* RCTClippingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClippingTests.m; sourceTree = ""; };
+ 144D21231B2204C5006DB32B /* RCTClipRectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClipRectTests.m; sourceTree = ""; };
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAllocationTests.m; sourceTree = ""; };
1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeTests.m; sourceTree = ""; };
1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutorTests.m; sourceTree = ""; };
@@ -348,15 +352,17 @@
children = (
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */,
1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */,
- 144D21231B2204C5006DB32B /* RCTClippingTests.m */,
+ 138D6A151B53CD440074A87E /* RCTCacheTests.m */,
+ 144D21231B2204C5006DB32B /* RCTClipRectTests.m */,
1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */,
1497CFA71B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m */,
1497CFA81B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m */,
1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */,
+ 1300627E1B59179B0043FE5A /* RCTGzipTests.m */,
+ 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */,
138D6A161B53CD440074A87E /* RCTShadowViewTests.m */,
1497CFAA1B21F5E400C1F8F2 /* RCTSparseArrayTests.m */,
1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */,
- 138D6A151B53CD440074A87E /* RCTCacheTests.m */,
143BC57E1B21E18100462512 /* Info.plist */,
14D6D7101B220EB3001FB087 /* libOCMock.a */,
14D6D7011B220AE3001FB087 /* OCMock */,
@@ -781,14 +787,16 @@
buildActionMask = 2147483647;
files = (
1497CFB01B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m in Sources */,
- 144D21241B2204C5006DB32B /* RCTClippingTests.m in Sources */,
+ 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */,
1497CFB21B21F5E400C1F8F2 /* RCTSparseArrayTests.m in Sources */,
+ 1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */,
1497CFAF1B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m in Sources */,
1497CFAE1B21F5E400C1F8F2 /* RCTContextExecutorTests.m in Sources */,
1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */,
1497CFB11B21F5E400C1F8F2 /* RCTEventDispatcherTests.m in Sources */,
1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */,
138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */,
+ 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */,
1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */,
138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */,
);
@@ -1040,6 +1048,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../React/**",
+ "$(SRCROOT)/../../Libraries/**",
);
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = YES;
@@ -1094,6 +1103,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../React/**",
+ "$(SRCROOT)/../../Libraries/**",
);
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
MTL_ENABLE_DEBUG_INFO = NO;
diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp.js
index 519e333a2..339766b54 100644
--- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp.js
+++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp.js
@@ -59,12 +59,12 @@ var IntegrationTestsApp = React.createClass({
{TESTS.map((test) => [
- this.setState({test})}>
-
-
- {test.displayName}
-
-
+ this.setState({test})}
+ style={styles.row}>
+
+ {test.displayName}
+
,
])}
diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTClippingTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m
similarity index 90%
rename from Examples/UIExplorer/UIExplorerUnitTests/RCTClippingTests.m
rename to Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m
index 1f94a80c1..0041a1b46 100644
--- a/Examples/UIExplorer/UIExplorerUnitTests/RCTClippingTests.m
+++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m
@@ -16,10 +16,7 @@
#import
#import
#import
-
-extern CGRect RCTClipRect(CGSize contentSize, CGFloat contentScale,
- CGSize targetSize, CGFloat targetScale,
- UIViewContentMode resizeMode);
+#import "RCTImageUtils.h"
#define RCTAssertEqualPoints(a, b) { \
XCTAssertEqual(a.x, b.x); \
@@ -36,11 +33,11 @@ RCTAssertEqualPoints(a.origin, b.origin); \
RCTAssertEqualSizes(a.size, b.size); \
}
-@interface ClippingTests : XCTestCase
+@interface RCTClipRectTests : XCTestCase
@end
-@implementation ClippingTests
+@implementation RCTClipRectTests
- (void)testLandscapeSourceLandscapeTarget
{
@@ -109,6 +106,18 @@ RCTAssertEqualSizes(a.size, b.size); \
{
CGRect expected = {{0, -37.5}, {10, 100}};
+ CGRect result = RCTClipRect(content, 2, target, 2, UIViewContentModeScaleAspectFill);
+ RCTAssertEqualRects(expected, result);
+ }
+}
+
+- (void)testRounding
+{
+ CGSize content = {10, 100};
+ CGSize target = {20, 50};
+
+ {
+ CGRect expected = {{0, -38}, {10, 100}};
CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill);
RCTAssertEqualRects(expected, result);
}
diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m
new file mode 100644
index 000000000..5e74bf7c4
--- /dev/null
+++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m
@@ -0,0 +1,81 @@
+/**
+ * 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.
+ */
+
+#import
+#import "RCTUtils.h"
+#import "RCTNetworking.h"
+
+extern BOOL RCTIsGzippedData(NSData *data);
+
+@interface RCTNetworking (Private)
+
+- (void)buildRequest:(NSDictionary *)query
+ completionBlock:(void (^)(NSURLRequest *request))block;
+
+@end
+
+@interface RCTGzipTests : XCTestCase
+
+@end
+
+@implementation RCTGzipTests
+
+- (void)testGzip
+{
+ //set up data
+ NSString *inputString = @"Hello World!";
+ NSData *inputData = [inputString dataUsingEncoding:NSUTF8StringEncoding];
+
+ //compress
+ NSData *outputData = RCTGzipData(inputData, -1);
+ XCTAssertTrue(RCTIsGzippedData(outputData));
+}
+
+- (void)testDontRezipZippedData
+{
+ //set up data
+ NSString *inputString = @"Hello World!";
+ NSData *inputData = [inputString dataUsingEncoding:NSUTF8StringEncoding];
+
+ //compress
+ NSData *compressedData = RCTGzipData(inputData, -1);
+ inputString = [[NSString alloc] initWithData:compressedData encoding:NSUTF8StringEncoding];
+
+ //compress again
+ NSData *outputData = RCTGzipData(inputData, -1);
+ NSString *outputString = [[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding];
+ XCTAssertEqualObjects(outputString, inputString);
+}
+
+- (void)testRequestBodyEncoding
+{
+ NSDictionary *query = @{
+ @"url": @"http://example.com",
+ @"method": @"POST",
+ @"data": @{@"string": @"Hello World"},
+ @"headers": @{@"Content-Encoding": @"gzip"},
+ };
+
+ RCTNetworking *networker = [[RCTNetworking alloc] init];
+ __block NSURLRequest *request = nil;
+ [networker buildRequest:query completionBlock:^(NSURLRequest *_request) {
+ request = _request;
+ }];
+
+ XCTAssertNotNil(request);
+ XCTAssertNotNil(request.HTTPBody);
+ XCTAssertTrue(RCTIsGzippedData(request.HTTPBody));
+}
+
+@end
diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m
new file mode 100644
index 000000000..b8951ca02
--- /dev/null
+++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m
@@ -0,0 +1,81 @@
+/**
+ * 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.
+ */
+
+#import
+#import "RCTUtils.h"
+
+@interface RCTJSONTests : XCTestCase
+
+@end
+
+@implementation RCTJSONTests
+
+- (void)testEncodingObject
+{
+ NSDictionary *obj = @{@"foo": @"bar"};
+ NSString *json = @"{\"foo\":\"bar\"}";
+ XCTAssertEqualObjects(json, RCTJSONStringify(obj, NULL));
+}
+
+- (void)testEncodingArray
+{
+ NSArray *array = @[@"foo", @"bar"];
+ NSString *json = @"[\"foo\",\"bar\"]";
+ XCTAssertEqualObjects(json, RCTJSONStringify(array, NULL));
+}
+
+- (void)testEncodingString
+{
+ NSString *text = @"Hello\nWorld";
+ NSString *json = @"\"Hello\\nWorld\"";
+ XCTAssertEqualObjects(json, RCTJSONStringify(text, NULL));
+}
+
+- (void)testDecodingObject
+{
+ NSDictionary *obj = @{@"foo": @"bar"};
+ NSString *json = @"{\"foo\":\"bar\"}";
+ XCTAssertEqualObjects(obj, RCTJSONParse(json, NULL));
+}
+
+- (void)testDecodingArray
+{
+ NSArray *array = @[@"foo", @"bar"];
+ NSString *json = @"[\"foo\",\"bar\"]";
+ XCTAssertEqualObjects(array, RCTJSONParse(json, NULL));
+}
+
+- (void)testDecodingString
+{
+ NSString *text = @"Hello\nWorld";
+ NSString *json = @"\"Hello\\nWorld\"";
+ XCTAssertEqualObjects(text, RCTJSONParse(json, NULL));
+}
+
+- (void)testDecodingMutableArray
+{
+ NSString *json = @"[1,2,3]";
+ NSMutableArray *array = RCTJSONParseMutable(json, NULL);
+ XCTAssertNoThrow([array addObject:@4]);
+ XCTAssertEqualObjects(array, (@[@1, @2, @3, @4]));
+}
+
+- (void)testLeadingWhitespace
+{
+ NSDictionary *obj = @{@"foo": @"bar"};
+ NSString *json = @" \r\n\t{\"foo\":\"bar\"}";
+ XCTAssertEqualObjects(obj, RCTJSONParse(json, NULL));
+}
+
+@end
diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js
index b9137e87c..478c2a995 100644
--- a/Examples/UIExplorer/WebViewExample.js
+++ b/Examples/UIExplorer/WebViewExample.js
@@ -58,19 +58,19 @@ var WebViewExample = React.createClass({
return (
-
-
-
- {'<'}
-
-
+
+
+ {'<'}
+
-
-
-
- {'>'}
-
-
+
+
+ {'>'}
+
formdata.append(param.name, param.value)
);
+ if (xhr.upload) {
+ xhr.upload.onprogress = (event) => {
+ console.log('upload onprogress', event);
+ if (event.lengthComputable) {
+ this.setState({uploadProgress: event.loaded / event.total});
+ }
+ };
+ }
xhr.send(formdata);
this.setState({isUploading: true});
}
@@ -251,6 +260,10 @@ class FormUploader extends React.Component {
));
var uploadButtonLabel = this.state.isUploading ? 'Uploading...' : 'Upload';
+ var uploadProgress = this.state.uploadProgress;
+ if (uploadProgress !== null) {
+ uploadButtonLabel += ' ' + Math.round(uploadProgress * 100) + '%';
+ }
var uploadButton = (
{uploadButtonLabel}
diff --git a/Examples/UIExplorer/createExamplePage.js b/Examples/UIExplorer/createExamplePage.js
index 3d5a1ac88..352f84a4c 100644
--- a/Examples/UIExplorer/createExamplePage.js
+++ b/Examples/UIExplorer/createExamplePage.js
@@ -55,6 +55,7 @@ var createExamplePage = function(title: ?string, exampleModule: ExampleModule)
var result = example.render(null);
if (result) {
renderedComponent = result;
+ result.props.navigator = this.props.navigator;
}
(React: Object).render = originalRender;
(React: Object).renderComponent = originalRenderComponent;
diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.h b/JSCLegacyProfiler/JSCLegacyProfiler.h
new file mode 100644
index 000000000..826e39f21
--- /dev/null
+++ b/JSCLegacyProfiler/JSCLegacyProfiler.h
@@ -0,0 +1,25 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#pragma once
+
+#import "JSContextRef.h"
+
+extern "C" {
+
+JSValueRef nativeProfilerStart(
+ JSContextRef ctx,
+ JSObjectRef function,
+ JSObjectRef thisObject,
+ size_t argumentCount,
+ const JSValueRef arguments[],
+ JSValueRef *exception);
+
+JSValueRef nativeProfilerEnd(
+ JSContextRef ctx,
+ JSObjectRef function,
+ JSObjectRef thisObject,
+ size_t argumentCount,
+ const JSValueRef arguments[],
+ JSValueRef *exception);
+
+}
diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.mm b/JSCLegacyProfiler/JSCLegacyProfiler.mm
new file mode 100644
index 000000000..218c5e55d
--- /dev/null
+++ b/JSCLegacyProfiler/JSCLegacyProfiler.mm
@@ -0,0 +1,161 @@
+//#include "config.h"
+
+#include "JSCLegacyProfiler.h"
+
+#include "APICast.h"
+#include "LegacyProfiler.h"
+#include "OpaqueJSString.h"
+#include "JSProfilerPrivate.h"
+#include "JSStringRef.h"
+
+#include
+
+#define GEN_AND_CHECK(expr) \
+ do { \
+ yajl_gen_status GEN_AND_CHECK_status = (expr); \
+ if (GEN_AND_CHECK_status != yajl_gen_status_ok) { \
+ return GEN_AND_CHECK_status; \
+ } \
+ } while (false)
+
+static inline yajl_gen_status yajl_gen_cstring(yajl_gen gen, const char *str) {
+ return yajl_gen_string(gen, (const unsigned char*)str, strlen(str));
+}
+
+static yajl_gen_status append_children_array_json(yajl_gen gen, const JSC::ProfileNode *node);
+static yajl_gen_status append_node_json(yajl_gen gen, const JSC::ProfileNode *node);
+
+static yajl_gen_status append_root_json(yajl_gen gen, const JSC::Profile *profile) {
+ GEN_AND_CHECK(yajl_gen_map_open(gen));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "rootNodes"));
+ GEN_AND_CHECK(append_children_array_json(gen, profile->head()));
+ GEN_AND_CHECK(yajl_gen_map_close(gen));
+
+ return yajl_gen_status_ok;
+}
+
+static yajl_gen_status append_children_array_json(yajl_gen gen, const JSC::ProfileNode *node) {
+ GEN_AND_CHECK(yajl_gen_array_open(gen));
+ for (RefPtr child : node->children()) {
+ GEN_AND_CHECK(append_node_json(gen, child.get()));
+ }
+ GEN_AND_CHECK(yajl_gen_array_close(gen));
+
+ return yajl_gen_status_ok;
+}
+
+static yajl_gen_status append_node_json(yajl_gen gen, const JSC::ProfileNode *node) {
+ GEN_AND_CHECK(yajl_gen_map_open(gen));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "id"));
+ GEN_AND_CHECK(yajl_gen_integer(gen, node->id()));
+
+ if (!node->functionName().isEmpty()) {
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "functionName"));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, node->functionName().utf8().data()));
+ }
+
+ if (!node->url().isEmpty()) {
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "url"));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, node->url().utf8().data()));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "lineNumber"));
+ GEN_AND_CHECK(yajl_gen_integer(gen, node->lineNumber()));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "columnNumber"));
+ GEN_AND_CHECK(yajl_gen_integer(gen, node->columnNumber()));
+ }
+
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "calls"));
+ GEN_AND_CHECK(yajl_gen_array_open(gen));
+ for (const JSC::ProfileNode::Call &call : node->calls()) {
+ GEN_AND_CHECK(yajl_gen_map_open(gen));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "startTime"));
+ GEN_AND_CHECK(yajl_gen_double(gen, call.startTime()));
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "totalTime"));
+ GEN_AND_CHECK(yajl_gen_double(gen, call.totalTime()));
+ GEN_AND_CHECK(yajl_gen_map_close(gen));
+ }
+ GEN_AND_CHECK(yajl_gen_array_close(gen));
+
+ if (!node->children().isEmpty()) {
+ GEN_AND_CHECK(yajl_gen_cstring(gen, "children"));
+ GEN_AND_CHECK(append_children_array_json(gen, node));
+ }
+
+ GEN_AND_CHECK(yajl_gen_map_close(gen));
+
+ return yajl_gen_status_ok;
+}
+
+static char *render_error_code(yajl_gen_status status) {
+ char err[1024];
+ snprintf(err, sizeof(err), "{\"error\": %d}", (int)status);
+ return strdup(err);
+}
+
+static char *convert_to_json(const JSC::Profile *profile) {
+ yajl_gen_status status;
+ yajl_gen gen = yajl_gen_alloc(NULL);
+
+ status = append_root_json(gen, profile);
+ if (status != yajl_gen_status_ok) {
+ yajl_gen_free(gen);
+ return render_error_code(status);
+ }
+
+ const unsigned char *buf;
+ size_t buf_size;
+ status = yajl_gen_get_buf(gen, &buf, &buf_size);
+ if (status != yajl_gen_status_ok) {
+ yajl_gen_free(gen);
+ return render_error_code(status);
+ }
+
+ char *json_copy = strdup((const char*)buf);
+ yajl_gen_free(gen);
+ return json_copy;
+}
+
+static char *JSEndProfilingAndRender(JSContextRef ctx, JSStringRef title)
+{
+ JSC::ExecState *exec = toJS(ctx);
+ JSC::LegacyProfiler *profiler = JSC::LegacyProfiler::profiler();
+ RefPtr rawProfile = profiler->stopProfiling(exec, title->string());
+ return convert_to_json(rawProfile.get());
+}
+
+JSValueRef nativeProfilerStart(
+ JSContextRef ctx,
+ JSObjectRef function,
+ JSObjectRef thisObject,
+ size_t argumentCount,
+ const JSValueRef arguments[],
+ JSValueRef *exception) {
+ if (argumentCount < 1) {
+ // Could raise an exception here.
+ return JSValueMakeUndefined(ctx);
+ }
+
+ JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL);
+ JSStartProfiling(ctx, title);
+ JSStringRelease(title);
+ return JSValueMakeUndefined(ctx);
+}
+
+JSValueRef nativeProfilerEnd(
+ JSContextRef ctx,
+ JSObjectRef function,
+ JSObjectRef thisObject,
+ size_t argumentCount,
+ const JSValueRef arguments[],
+ JSValueRef *exception) {
+ if (argumentCount < 1) {
+ // Could raise an exception here.
+ return JSValueMakeUndefined(ctx);
+ }
+
+ JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL);
+ char *rendered = JSEndProfilingAndRender(ctx, title);
+ JSStringRelease(title);
+ JSStringRef profile = JSStringCreateWithUTF8CString(rendered);
+ free(rendered);
+ return JSValueMakeString(ctx, profile);
+}
diff --git a/JSCLegacyProfiler/Makefile b/JSCLegacyProfiler/Makefile
new file mode 100644
index 000000000..b825f7764
--- /dev/null
+++ b/JSCLegacyProfiler/Makefile
@@ -0,0 +1,108 @@
+HEADER_PATHS := `find ./tmp/JavaScriptCore -name '*.h' | xargs -I{} dirname {} | uniq | xargs -I{} echo "-I {}"`
+CERT ?= "iPhone Developer"
+
+ios8: prepare build generate
+
+prepare: clean create download
+
+build: x86_64 arm64 armv7
+
+generate: lipo codesign
+
+clean:
+ @rm -rf tmp/ /tmp/RCTJSCProfiler
+
+lipo:
+ lipo -create -output /tmp/RCTJSCProfiler/RCTJSCProfiler.ios8.dylib ./tmp/RCTJSCProfiler_x86_64 ./tmp/RCTJSCProfiler_arm64 ./tmp/RCTJSCProfiler_armv7
+
+codesign:
+ codesign -f -s ${CERT} /tmp/RCTJSCProfiler/RCTJSCProfiler.ios8.dylib
+
+create:
+ mkdir -p ./tmp /tmp/RCTJSCProfiler/ ./tmp/CoreFoundation ./tmp/Foundation
+ for file in ./tmp/CoreFoundation/CFUserNotification.h ./tmp/CoreFoundation/CFXMLNode.h ./tmp/CoreFoundation/CFXMLParser.h ./tmp/Foundation/Foundation.h; do echo '' > "$$file"; done
+
+download: wtf jsc webcore yajl
+
+wtf:
+ curl -o tmp/WTF.tar.gz http://www.opensource.apple.com/tarballs/WTF/WTF-7600.1.24.tar.gz
+ tar -zxvf tmp/WTF.tar.gz -C tmp
+
+jsc:
+ curl -o tmp/JSC.tar.gz http://www.opensource.apple.com/tarballs/JavaScriptCore/JavaScriptCore-7600.1.17.tar.gz
+ tar -zxvf tmp/JSC.tar.gz -C tmp
+ mv ./tmp/JavaScriptCore-7600.1.17 ./tmp/JavaScriptCore
+ python ./tmp/JavaScriptCore/generate-bytecode-files --bytecodes_h ./tmp/JavaScriptCore/Bytecodes.h ./tmp/JavaScriptCore/bytecode/BytecodeList.json
+
+webcore:
+ curl -o tmp/WebCore.tar.gz http://www.opensource.apple.com/tarballs/WebCore/WebCore-7600.1.25.tar.gz
+ tar -zxvf tmp/WebCore.tar.gz -C tmp
+
+yajl:
+ curl -o tmp/yajl.tar.gz https://codeload.github.com/lloyd/yajl/tar.gz/2.1.0
+ tar -zxvf tmp/yajl.tar.gz -C tmp
+ mkdir -p ./tmp/yajl-2.1.0/build && cd ./tmp/yajl-2.1.0/build && cmake .. && make
+ echo `find . -name '*.c'`
+ cd ./tmp/yajl-2.1.0/src && \
+ clang -arch arm64 -arch armv7 -std=c99 \
+ -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/ \
+ -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/machine \
+ -I ../build/yajl-2.1.0/include \
+ -c `find . -name '*.c'`
+ libtool -static -o ./tmp/yajl.a `find ./tmp/yajl-2.1.0/src/ -name '*.o'`
+
+x86_64:
+ clang -w -dynamiclib -o ./tmp/RCTJSCProfiler_x86_64 -std=c++11 \
+ -install_name RCTJSCProfiler.ios8.dylib \
+ -include ./tmp/JavaScriptCore/config.h \
+ -I ./tmp \
+ -I ./tmp/WebCore-7600.1.25/icu \
+ -I ./tmp/WTF-7600.1.24 \
+ -I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \
+ -DNDEBUG=1\
+ -miphoneos-version-min=8.0 \
+ -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib \
+ -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/system \
+ ${HEADER_PATHS} \
+ -undefined dynamic_lookup \
+ ./JSCLegacyProfiler.mm ./tmp/yajl-2.1.0/build/yajl-2.1.0/lib/libyajl_s.a
+
+arm64:
+ echo $(HEADER_PATHS)
+ clang -w -dynamiclib -o ./tmp/RCTJSCProfiler_arm64 -std=c++11 \
+ -install_name RCTJSCProfiler.ios8.dylib \
+ -arch arm64 \
+ -include ./tmp/JavaScriptCore/config.h \
+ -I ./tmp \
+ -I ./tmp/WebCore-7600.1.25/icu \
+ -I ./tmp/WTF-7600.1.24 \
+ -I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \
+ -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include \
+ -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/machine \
+ -DNDEBUG=1\
+ -miphoneos-version-min=8.0 \
+ -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib \
+ -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib/system \
+ ${HEADER_PATHS} \
+ -undefined dynamic_lookup \
+ ./JSCLegacyProfiler.mm ./tmp/yajl.a
+
+armv7:
+ clang -w -dynamiclib -o ./tmp/RCTJSCProfiler_armv7 -std=c++11 \
+ -install_name RCTJSCProfiler.ios8.dylib \
+ -arch armv7 \
+ -include ./tmp/JavaScriptCore/config.h \
+ -I ./tmp \
+ -I ./tmp/WebCore-7600.1.25/icu \
+ -I ./tmp/WTF-7600.1.24 \
+ -I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \
+ -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include \
+ -DNDEBUG=1\
+ -miphoneos-version-min=8.0 \
+ -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib \
+ -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib/system \
+ ${HEADER_PATHS} \
+ -undefined dynamic_lookup \
+ ./JSCLegacyProfiler.mm ./tmp/yajl.a
+
+.PHONY: ios8
diff --git a/Libraries/Animation/Animated/Animated.js b/Libraries/Animation/Animated/Animated.js
index 81223edcf..f99913f50 100644
--- a/Libraries/Animation/Animated/Animated.js
+++ b/Libraries/Animation/Animated/Animated.js
@@ -1177,7 +1177,7 @@ var parallel = function(
}
animations.forEach((animation, idx) => {
- animation.start(endResult => {
+ var cb = function(endResult) {
hasEnded[idx] = true;
doneCount++;
if (doneCount === animations.length) {
@@ -1189,7 +1189,13 @@ var parallel = function(
if (!endResult.finished && stopTogether) {
result.stop();
}
- });
+ };
+
+ if (!animation) {
+ cb({finished: true});
+ } else {
+ animation.start(cb);
+ }
});
},
diff --git a/Libraries/Animation/Animated/__tests__/Animated-test.js b/Libraries/Animation/Animated/__tests__/Animated-test.js
index 8f9e52211..cad752ff0 100644
--- a/Libraries/Animation/Animated/__tests__/Animated-test.js
+++ b/Libraries/Animation/Animated/__tests__/Animated-test.js
@@ -205,6 +205,16 @@ describe('Animated Parallel', () => {
expect(cb).toBeCalledWith({finished: true});
});
+ it('works with an empty element in array', () => {
+ var anim1 = {start: jest.genMockFunction()};
+ var cb = jest.genMockFunction();
+ Animated.parallel([null, anim1]).start(cb);
+
+ expect(anim1.start).toBeCalled();
+ anim1.start.mock.calls[0][0]({finished: true});
+
+ expect(cb).toBeCalledWith({finished: true});
+ });
it('parellelizes well', () => {
var anim1 = {start: jest.genMockFunction()};
diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js
index cc1b00b41..01d6dbbfb 100644
--- a/Libraries/Components/TextInput/TextInput.js
+++ b/Libraries/Components/TextInput/TextInput.js
@@ -31,8 +31,8 @@ var invariant = require('invariant');
var requireNativeComponent = require('requireNativeComponent');
var onlyMultiline = {
- onSelectionChange: true,
- onTextInput: true,
+ onSelectionChange: true, // not supported in Open Source yet
+ onTextInput: true, // not supported in Open Source yet
children: true,
};
@@ -64,10 +64,6 @@ var viewConfigAndroid = {
var RCTTextView = requireNativeComponent('RCTTextView', null);
var RCTTextField = requireNativeComponent('RCTTextField', null);
-type DefaultProps = {
- bufferDelay: number;
-};
-
type Event = Object;
/**
@@ -77,30 +73,29 @@ type Event = Object;
* types, such as a numeric keypad.
*
* The simplest use case is to plop down a `TextInput` and subscribe to the
- * `onChangeText` events to read the user input. There are also other events, such
- * as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple
+ * `onChangeText` events to read the user input. There are also other events,
+ * such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple
* example:
*
* ```
- *
* this.setState({input: text})}
+ * onChangeText={(text) => this.setState({text})}
+ * value={this.state.text}
* />
- * {'user input: ' + this.state.input}
- *
* ```
*
- * The `value` prop can be used to set the value of the input in order to make
- * the state of the component clear, but does not behave as a true
- * controlled component by default because all operations are asynchronous.
- * Setting `value` once is like setting the default value, but you can change it
- * continuously based on `onChangeText` events as well. If you really want to
- * force the component to always revert to the value you are setting, you can
- * set `controlled={true}`.
+ * Note that some props are only available with multiline={true/false}:
*
- * The `multiline` prop is not supported in all releases, and some props are
- * multiline only.
+ * var onlyMultiline = {
+ * onSelectionChange: true, // not supported in Open Source yet
+ * onTextInput: true, // not supported in Open Source yet
+ * children: true,
+ * };
+ *
+ * var notMultiline = {
+ * onSubmitEditing: true,
+ * };
*/
var TextInput = React.createClass({
@@ -179,6 +174,11 @@ var TextInput = React.createClass({
'done',
'emergency-call',
]),
+ /**
+ * Limits the maximum number of characters that can be entered. Use this
+ * instead of implementing the logic in JS to avoid flicker.
+ */
+ maxLength: PropTypes.number,
/**
* If true, the keyboard disables the return key when there is no text and
* automatically enables it when there is text. Default value is false.
@@ -236,22 +236,21 @@ var TextInput = React.createClass({
*/
selectionState: PropTypes.instanceOf(DocumentSelectionState),
/**
- * The default value for the text input
+ * The value to show for the text input. TextInput is a controlled
+ * component, which means the native value will be forced to match this
+ * value prop if provided. For most uses this works great, but in some
+ * cases this may cause flickering - one common cause is preventing edits
+ * by keeping value the same. In addition to simply setting the same value,
+ * either set `editable={false}`, or set/update `maxLength` to prevent
+ * unwanted edits without flicker.
*/
value: PropTypes.string,
/**
- * This helps avoid drops characters due to race conditions between JS and
- * the native text input. The default should be fine, but if you're
- * potentially doing very slow operations on every keystroke then you may
- * want to try increasing this.
+ * Provides an initial value that will change when the user starts typing.
+ * Useful for simple use-cases where you don't want to deal with listening
+ * to events and updating the value prop to keep the controlled state in sync.
*/
- bufferDelay: PropTypes.number,
- /**
- * If you really want this to behave as a controlled component, you can set
- * this true, but you will probably see flickering, dropped keystrokes,
- * and/or laggy typing, depending on how you process onChange events.
- */
- controlled: PropTypes.bool,
+ defaultValue: PropTypes.string,
/**
* When the clear button should appear on the right side of the text view
*/
@@ -297,16 +296,9 @@ var TextInput = React.createClass({
React.findNodeHandle(this.refs.input);
},
- getDefaultProps: function(): DefaultProps {
- return {
- bufferDelay: 100,
- };
- },
-
getInitialState: function() {
return {
- mostRecentEventCounter: 0,
- bufferedValue: this.props.value,
+ mostRecentEventCount: 0,
};
},
@@ -346,52 +338,6 @@ var TextInput = React.createClass({
}
},
- _bufferTimeout: (undefined: ?number),
-
- componentWillReceiveProps: function(newProps: {value: any}) {
- if (newProps.value !== this.props.value) {
- if (!this.isFocused()) {
- // Set the value immediately if the input is not focused since that
- // means there is no risk of the user typing immediately.
- this.setState({bufferedValue: newProps.value});
- } else {
- // The following clear and setTimeout buffers the value such that if more
- // characters are typed in quick succession, generating new values, the
- // out of date values will get cancelled before they are ever sent to
- // native.
- //
- // If we don't do this, it's likely the out of date values will blow
- // away recently typed characters in the native input that JS was not
- // yet aware of (since it is informed asynchronously), then the next
- // character will be appended to the older value, dropping the
- // characters in between. Here is a potential sequence of events
- // (recall we have multiple independently serial, interleaved queues):
- //
- // 1) User types 'R' => send 'R' to JS queue.
- // 2) User types 'e' => send 'Re' to JS queue.
- // 3) JS processes 'R' and sends 'R' back to native.
- // 4) Native recieves 'R' and changes input from 'Re' back to 'R'.
- // 5) User types 'a' => send 'Ra' to JS queue.
- // 6) JS processes 'Re' and sends 'Re' back to native.
- // 7) Native recieves 'Re' and changes input from 'R' back to 'Re'.
- // 8) JS processes 'Ra' and sends 'Ra' back to native.
- // 9) Native recieves final 'Ra' from JS - 'e' has been dropped!
- //
- // This isn't 100% foolproop (e.g. if it takes longer than
- // `props.bufferDelay` ms to process one keystroke), and there are of
- // course other potential algorithms to deal with this, but this is a
- // simple solution that seems to reduce the chance of dropped characters
- // drastically without compromising native input responsiveness (e.g. by
- // introducing delay from a synchronization protocol).
- this.clearTimeout(this._bufferTimeout);
- this._bufferTimeout = this.setTimeout(
- () => this.setState({bufferedValue: newProps.value}),
- this.props.bufferDelay
- );
- }
- }
- },
-
getChildContext: function(): Object {
return {isInAParentText: true};
},
@@ -408,12 +354,17 @@ var TextInput = React.createClass({
}
},
+ _getText: function(): ?string {
+ return typeof this.props.value === 'string' ?
+ this.props.value :
+ this.props.defaultValue;
+ },
+
_renderIOS: function() {
var textContainer;
- var props = Object.assign({},this.props);
+ var props = Object.assign({}, this.props);
props.style = [styles.input, this.props.style];
-
if (!props.multiline) {
for (var propKey in onlyMultiline) {
if (props[propKey]) {
@@ -430,7 +381,8 @@ var TextInput = React.createClass({
onBlur={this._onBlur}
onChange={this._onChange}
onSelectionChangeShouldSetResponder={() => true}
- text={this.state.bufferedValue}
+ text={this._getText()}
+ mostRecentEventCount={this.state.mostRecentEventCount}
/>;
} else {
for (var propKey in notMultiline) {
@@ -459,14 +411,14 @@ var TextInput = React.createClass({
ref="input"
{...props}
children={children}
- mostRecentEventCounter={this.state.mostRecentEventCounter}
+ mostRecentEventCount={this.state.mostRecentEventCount}
onFocus={this._onFocus}
onBlur={this._onBlur}
onChange={this._onChange}
onSelectionChange={this._onSelectionChange}
onTextInput={this._onTextInput}
onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
- text={this.state.bufferedValue}
+ text={this._getText()}
/>;
}
@@ -516,7 +468,7 @@ var TextInput = React.createClass({
password={this.props.password || this.props.secureTextEntry}
placeholder={this.props.placeholder}
placeholderTextColor={this.props.placeholderTextColor}
- text={this.state.bufferedValue}
+ text={this._getText()}
underlineColorAndroid={this.props.underlineColorAndroid}
children={children}
/>;
@@ -543,11 +495,20 @@ var TextInput = React.createClass({
},
_onChange: function(event: Event) {
- if (this.props.controlled && event.nativeEvent.text !== this.props.value) {
- this.refs.input.setNativeProps({text: this.props.value});
- }
+ var text = event.nativeEvent.text;
+ var eventCount = event.nativeEvent.eventCount;
this.props.onChange && this.props.onChange(event);
- this.props.onChangeText && this.props.onChangeText(event.nativeEvent.text);
+ this.props.onChangeText && this.props.onChangeText(text);
+ this.setState({mostRecentEventCount: eventCount}, () => {
+ // This is a controlled component, so make sure to force the native value
+ // to match. Most usage shouldn't need this, but if it does this will be
+ // more correct but might flicker a bit and/or cause the cursor to jump.
+ if (text !== this.props.value && typeof this.props.value === 'string') {
+ this.refs.input.setNativeProps({
+ text: this.props.value,
+ });
+ }
+ });
},
_onBlur: function(event: Event) {
@@ -567,10 +528,6 @@ var TextInput = React.createClass({
_onTextInput: function(event: Event) {
this.props.onTextInput && this.props.onTextInput(event);
- var counter = event.nativeEvent.eventCounter;
- if (counter > this.state.mostRecentEventCounter) {
- this.setState({mostRecentEventCounter: counter});
- }
},
});
diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js
index dcbfbeee1..47d24b811 100644
--- a/Libraries/Components/Touchable/TouchableHighlight.js
+++ b/Libraries/Components/Touchable/TouchableHighlight.js
@@ -181,6 +181,10 @@ var TouchableHighlight = React.createClass({
},
_showUnderlay: function() {
+ if (!this.isMounted()) {
+ return;
+ }
+
this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps);
this.refs[CHILD_REF].setNativeProps(this.state.activeProps);
this.props.onShowUnderlay && this.props.onShowUnderlay();
diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js
index a0891714f..39117aa93 100644
--- a/Libraries/Components/Touchable/TouchableOpacity.js
+++ b/Libraries/Components/Touchable/TouchableOpacity.js
@@ -12,19 +12,16 @@
// Note (avik): add @flow when Flow supports spread properties in propTypes
+var Animated = require('Animated');
var NativeMethodsMixin = require('NativeMethodsMixin');
-var POPAnimationMixin = require('POPAnimationMixin');
var React = require('React');
var TimerMixin = require('react-timer-mixin');
var Touchable = require('Touchable');
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
-var cloneWithProps = require('cloneWithProps');
-var ensureComponentIsNative = require('ensureComponentIsNative');
var ensurePositiveDelayProps = require('ensurePositiveDelayProps');
var flattenStyle = require('flattenStyle');
var keyOf = require('keyOf');
-var onlyChild = require('onlyChild');
/**
* A wrapper for making views respond properly to touches.
@@ -52,7 +49,7 @@ var onlyChild = require('onlyChild');
*/
var TouchableOpacity = React.createClass({
- mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin],
+ mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin],
propTypes: {
...TouchableWithoutFeedback.propTypes,
@@ -70,16 +67,17 @@ var TouchableOpacity = React.createClass({
},
getInitialState: function() {
- return this.touchableGetInitialState();
+ return {
+ ...this.touchableGetInitialState(),
+ anim: new Animated.Value(1),
+ };
},
componentDidMount: function() {
ensurePositiveDelayProps(this.props);
- ensureComponentIsNative(this.refs[CHILD_REF]);
},
componentDidUpdate: function() {
- ensureComponentIsNative(this.refs[CHILD_REF]);
},
componentWillReceiveProps: function(nextProps) {
@@ -87,22 +85,10 @@ var TouchableOpacity = React.createClass({
},
setOpacityTo: function(value) {
- if (POPAnimationMixin) {
- // Reset with animation if POP is available
- this.stopAllAnimations();
- var anim = {
- type: this.AnimationTypes.linear,
- property: this.AnimationProperties.opacity,
- duration: 0.15,
- toValue: value,
- };
- this.startAnimation(CHILD_REF, anim);
- } else {
- // Reset immediately if POP is unavailable
- this.refs[CHILD_REF].setNativeProps({
- opacity: value
- });
- }
+ Animated.timing(
+ this.state.anim,
+ {toValue: value, duration: 150}
+ ).start();
},
/**
@@ -161,25 +147,27 @@ var TouchableOpacity = React.createClass({
_opacityInactive: function() {
this.clearTimeout(this._hideTimeout);
this._hideTimeout = null;
- var child = onlyChild(this.props.children);
- var childStyle = flattenStyle(child.props.style) || {};
+ var childStyle = flattenStyle(this.props.style) || {};
this.setOpacityTo(
childStyle.opacity === undefined ? 1 : childStyle.opacity
);
},
render: function() {
- return cloneWithProps(onlyChild(this.props.children), {
- ref: CHILD_REF,
- accessible: true,
- testID: this.props.testID,
- onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
- onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
- onResponderGrant: this.touchableHandleResponderGrant,
- onResponderMove: this.touchableHandleResponderMove,
- onResponderRelease: this.touchableHandleResponderRelease,
- onResponderTerminate: this.touchableHandleResponderTerminate,
- });
+ return (
+
+ {this.props.children}
+
+ );
},
});
@@ -191,6 +179,5 @@ var TouchableOpacity = React.createClass({
*/
var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
-var CHILD_REF = keyOf({childRef: null});
module.exports = TouchableOpacity;
diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js
index 35ed24e3a..7f6153a33 100644
--- a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js
+++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js
@@ -67,10 +67,10 @@ class NavigationContext {
}
}
- emit(eventType: String, data: any): void {
+ emit(eventType: String, data: any, didEmitCallback: ?Function): void {
var emitter = this._eventEmitter;
if (emitter) {
- emitter.emit(eventType, data);
+ emitter.emit(eventType, data, didEmitCallback);
}
}
diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js
index 343e1f3e6..3a7a3d6de 100644
--- a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js
+++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js
@@ -36,7 +36,7 @@ class NavigationEventPool {
this._list = [];
}
- get(type: String, target: Object, data: any): NavigationEvent {
+ get(type: string, target: Object, data: any): NavigationEvent {
var event;
if (this._list.length > 0) {
event = this._list.pop();
@@ -59,13 +59,13 @@ class NavigationEvent {
_defaultPrevented: boolean;
_disposed: boolean;
_target: ?Object;
- _type: ?String;
+ _type: ?string;
- static pool(type: String, target: Object, data: any): NavigationEvent {
+ static pool(type: string, target: Object, data: any): NavigationEvent {
return _navigationEventPool.get(type, target, data);
}
- constructor(type: String, target: Object, data: any) {
+ constructor(type: string, target: Object, data: any) {
this._type = type;
this._target = target;
this._data = data;
diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js
index db9e78554..ef63bd39d 100644
--- a/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js
+++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js
@@ -31,8 +31,9 @@ var EventEmitter = require('EventEmitter');
var NavigationEvent = require('NavigationEvent');
type EventParams = {
- eventType: String;
data: any;
+ didEmitCallback: ?Function;
+ eventType: string;
};
class NavigationEventEmitter extends EventEmitter {
@@ -47,22 +48,36 @@ class NavigationEventEmitter extends EventEmitter {
this._target = target;
}
- emit(eventType: String, data: any): void {
+ emit(
+ eventType: string,
+ data: any,
+ didEmitCallback: ?Function
+ ): void {
if (this._emitting) {
// An event cycle that was previously created hasn't finished yet.
// Put this event cycle into the queue and will finish them later.
- this._emitQueue.push({eventType, data});
+ this._emitQueue.push({eventType, data, didEmitCallback});
return;
}
this._emitting = true;
+
var event = new NavigationEvent(eventType, this._target, data);
- super.emit(eventType, event);
+
+ // EventEmitter#emit only takes `eventType` as `String`. Casting `eventType`
+ // to `String` to make @flow happy.
+ super.emit(String(eventType), event);
+
+ if (typeof didEmitCallback === 'function') {
+ didEmitCallback.call(this._target, event);
+ }
+ event.dispose();
+
this._emitting = false;
while (this._emitQueue.length) {
var arg = this._emitQueue.shift();
- this.emit(arg.eventType, arg.data);
+ this.emit(arg.eventType, arg.data, arg.didEmitCallback);
}
}
}
diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js
index 91344e170..78fbfd157 100644
--- a/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js
+++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js
@@ -52,6 +52,16 @@ class RouteStack {
return this._routes.get(index);
}
+ indexOf(route: any): number {
+ return this._routes.indexOf(route);
+ }
+
+ slice(begin: ?number, end: ?number): RouteStack {
+ var routes = this._routes.slice(begin, end);
+ var index = Math.min(this._index, routes.size - 1);
+ return this._update(index, routes);
+ }
+
/**
* Returns a new stack with the provided route appended,
* starting at this stack size.
@@ -71,7 +81,7 @@ class RouteStack {
list.slice(0, this._index + 1).push(route);
});
- return new RouteStack(routes.size - 1, routes);
+ return this._update(routes.size - 1, routes);
}
/**
@@ -83,7 +93,7 @@ class RouteStack {
// When popping, removes the rest of the routes past the current index.
var routes = this._routes.slice(0, this._index);
- return new RouteStack(routes.size - 1, routes);
+ return this._update(routes.size - 1, routes);
}
jumpToIndex(index: number): RouteStack {
@@ -92,11 +102,7 @@ class RouteStack {
'index out of bound'
);
- if (index === this._index) {
- return this;
- }
-
- return new RouteStack(index, this._routes);
+ return this._update(index, this._routes);
}
/**
@@ -129,7 +135,14 @@ class RouteStack {
);
var routes = this._routes.set(index, route);
- return new RouteStack(this._index, routes);
+ return this._update(this._index, routes);
+ }
+
+ _update(index: number, routes: List): RouteStack {
+ if (this._index === index && this._routes === routes) {
+ return this;
+ }
+ return new RouteStack(index, routes);
}
}
diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js
index 2a8d7d82a..cc2875c81 100644
--- a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js
+++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js
@@ -34,27 +34,48 @@ jest
var NavigationEventEmitter = require('NavigationEventEmitter');
describe('NavigationEventEmitter', () => {
- it('emit event', () => {
- var target = {};
- var emitter = new NavigationEventEmitter(target);
- var focusCounter = 0;
- var focusTarget;
+ it('emits event', () => {
+ var context = {};
+ var emitter = new NavigationEventEmitter(context);
+ var logs = [];
+
+ emitter.addListener('ping', (event) => {
+ var {type, data, target, defaultPrevented} = event;
+
+ logs.push({
+ data,
+ defaultPrevented,
+ target,
+ type,
+ });
- emitter.addListener('focus', (event) => {
- focusCounter++;
- focusTarget = event.target;
});
- emitter.emit('focus');
- emitter.emit('blur');
+ emitter.emit('ping', 'hello');
- expect(focusCounter).toBe(1);
- expect(focusTarget).toBe(target);
+ expect(logs.length).toBe(1);
+ expect(logs[0].target).toBe(context);
+ expect(logs[0].type).toBe('ping');
+ expect(logs[0].data).toBe('hello');
+ expect(logs[0].defaultPrevented).toBe(false);
});
- it('put nested emit call in queue', () => {
- var target = {};
- var emitter = new NavigationEventEmitter(target);
+ it('does not emit event that has no listeners', () => {
+ var context = {};
+ var emitter = new NavigationEventEmitter(context);
+ var pinged = false;
+
+ emitter.addListener('ping', () => {
+ pinged = true;
+ });
+
+ emitter.emit('yo', 'bo');
+ expect(pinged).toBe(false);
+ });
+
+ it('puts nested emit call in a queue', () => {
+ var context = {};
+ var emitter = new NavigationEventEmitter(context);
var logs = [];
emitter.addListener('one', () => {
@@ -77,4 +98,63 @@ describe('NavigationEventEmitter', () => {
expect(logs).toEqual([1, 2, 3, 4, 5]);
});
+
+ it('calls callback after emitting', () => {
+ var context = {};
+ var emitter = new NavigationEventEmitter(context);
+ var logs = [];
+
+ emitter.addListener('ping', (event) => {
+ var {type, data, target, defaultPrevented} = event;
+ logs.push({
+ data,
+ defaultPrevented,
+ target,
+ type,
+ });
+ event.preventDefault();
+ });
+
+ emitter.emit('ping', 'hello', (event) => {
+ var {type, data, target, defaultPrevented} = event;
+ logs.push({
+ data,
+ defaultPrevented,
+ target,
+ type,
+ });
+ });
+
+ expect(logs.length).toBe(2);
+ expect(logs[1].target).toBe(context);
+ expect(logs[1].type).toBe('ping');
+ expect(logs[1].data).toBe('hello');
+ expect(logs[1].defaultPrevented).toBe(true);
+ });
+
+ it('calls callback after emitting the current event and before ' +
+ 'emitting the next event', () => {
+ var context = {};
+ var emitter = new NavigationEventEmitter(context);
+ var logs = [];
+
+ emitter.addListener('ping', (event) => {
+ logs.push('ping');
+ emitter.emit('pong');
+ });
+
+ emitter.addListener('pong', (event) => {
+ logs.push('pong');
+ });
+
+ emitter.emit('ping', null, () => {
+ logs.push('did-ping');
+ });
+
+ expect(logs).toEqual([
+ 'ping',
+ 'did-ping',
+ 'pong',
+ ]);
+ });
});
diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationRouteStack-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationRouteStack-test.js
index 3b750e054..a80bf8267 100644
--- a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationRouteStack-test.js
+++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationRouteStack-test.js
@@ -36,17 +36,17 @@ describe('NavigationRouteStack:', () => {
// Basic
it('gets index', () => {
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
- expect(stack.index).toEqual(1);
+ expect(stack.index).toBe(1);
});
it('gets size', () => {
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
- expect(stack.size).toEqual(3);
+ expect(stack.size).toBe(3);
});
it('gets route', () => {
var stack = new NavigationRouteStack(0, ['a', 'b', 'c']);
- expect(stack.get(2)).toEqual('c');
+ expect(stack.get(2)).toBe('c');
});
it('converts to an array', () => {
@@ -57,7 +57,7 @@ describe('NavigationRouteStack:', () => {
it('creates a new stack after mutation', () => {
var stack1 = new NavigationRouteStack(0, ['a', 'b']);
var stack2 = stack1.push('c');
- expect(stack1).not.toEqual(stack2);
+ expect(stack1).not.toBe(stack2);
});
it('throws at index out of bound', () => {
@@ -70,15 +70,57 @@ describe('NavigationRouteStack:', () => {
}).toThrow();
});
+ it('finds index', () => {
+ var stack = new NavigationRouteStack(0, ['a', 'b']);
+ expect(stack.indexOf('b')).toBe(1);
+ expect(stack.indexOf('c')).toBe(-1);
+ });
+
+ it('slices', () => {
+ var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c', 'd']);
+ var stack2 = stack1.slice(1, 3);
+ expect(stack2).not.toBe(stack1);
+ expect(stack2.toArray()).toEqual(['b', 'c']);
+ });
+
+ it('may update index after slicing', () => {
+ var stack = new NavigationRouteStack(2, ['a', 'b', 'c']);
+ expect(stack.slice().index).toBe(2);
+ expect(stack.slice(0, 1).index).toBe(0);
+ expect(stack.slice(0, 2).index).toBe(1);
+ expect(stack.slice(0, 3).index).toBe(2);
+ expect(stack.slice(0, 100).index).toBe(2);
+ expect(stack.slice(-2).index).toBe(1);
+ });
+
+ it('slices without specifying params', () => {
+ var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
+ var stack2 = stack1.slice();
+ expect(stack2).toBe(stack1);
+ });
+
+ it('slices to from the end', () => {
+ var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c', 'd']);
+ var stack2 = stack1.slice(-2);
+ expect(stack2.toArray()).toEqual(['c', 'd']);
+ });
+
+ it('throws when slicing to empty', () => {
+ expect(() => {
+ var stack = new NavigationRouteStack(1, ['a', 'b']);
+ stack.slice(100);
+ }).toThrow();
+ });
+
// Push
it('pushes route', () => {
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
var stack2 = stack1.push('c');
- expect(stack2).not.toEqual(stack1);
+ expect(stack2).not.toBe(stack1);
expect(stack2.toArray()).toEqual(['a', 'b', 'c']);
- expect(stack2.index).toEqual(2);
- expect(stack2.size).toEqual(3);
+ expect(stack2.index).toBe(2);
+ expect(stack2.size).toBe(3);
});
it('throws when pushing empty route', () => {
@@ -101,27 +143,27 @@ describe('NavigationRouteStack:', () => {
it('replaces routes on push', () => {
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
var stack2 = stack1.push('d');
- expect(stack2).not.toEqual(stack1);
+ expect(stack2).not.toBe(stack1);
expect(stack2.toArray()).toEqual(['a', 'b', 'd']);
- expect(stack2.index).toEqual(2);
+ expect(stack2.index).toBe(2);
});
// Pop
it('pops route', () => {
var stack1 = new NavigationRouteStack(2, ['a', 'b', 'c']);
var stack2 = stack1.pop();
- expect(stack2).not.toEqual(stack1);
+ expect(stack2).not.toBe(stack1);
expect(stack2.toArray()).toEqual(['a', 'b']);
- expect(stack2.index).toEqual(1);
- expect(stack2.size).toEqual(2);
+ expect(stack2.index).toBe(1);
+ expect(stack2.size).toBe(2);
});
it('replaces routes on pop', () => {
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
var stack2 = stack1.pop();
- expect(stack2).not.toEqual(stack1);
+ expect(stack2).not.toBe(stack1);
expect(stack2.toArray()).toEqual(['a']);
- expect(stack2.index).toEqual(0);
+ expect(stack2.index).toBe(0);
});
it('throws when popping to empty stack', () => {
@@ -136,8 +178,8 @@ describe('NavigationRouteStack:', () => {
var stack1 = new NavigationRouteStack(0, ['a', 'b', 'c']);
var stack2 = stack1.jumpToIndex(2);
- expect(stack2).not.toEqual(stack1);
- expect(stack2.index).toEqual(2);
+ expect(stack2).not.toBe(stack1);
+ expect(stack2.index).toBe(2);
});
it('throws then jumping to index out of bound', () => {
@@ -157,21 +199,20 @@ describe('NavigationRouteStack:', () => {
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
var stack2 = stack1.replaceAtIndex(0, 'x');
- expect(stack2).not.toEqual(stack1);
+ expect(stack2).not.toBe(stack1);
expect(stack2.toArray()).toEqual(['x', 'b']);
- expect(stack2.index).toEqual(1);
+ expect(stack2.index).toBe(1);
});
it('replaces route at negative index', () => {
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
var stack2 = stack1.replaceAtIndex(-1, 'x');
- expect(stack2).not.toEqual(stack1);
+ expect(stack2).not.toBe(stack1);
expect(stack2.toArray()).toEqual(['a', 'x']);
- expect(stack2.index).toEqual(1);
+ expect(stack2.index).toBe(1);
});
-
it('throws when replacing empty route', () => {
expect(() => {
var stack = new NavigationRouteStack(1, ['a', 'b']);
diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js
index bc5d65791..f55f44e35 100644
--- a/Libraries/CustomComponents/Navigator/Navigator.js
+++ b/Libraries/CustomComponents/Navigator/Navigator.js
@@ -67,6 +67,24 @@ function getuid() {
return __uid++;
}
+function getRouteID(route) {
+ if (route === null || typeof route !== 'object') {
+ return String(route);
+ }
+
+ var key = '__navigatorRouteID';
+
+ if (!route.hasOwnProperty(key)) {
+ Object.defineProperty(route, key, {
+ enumerable: false,
+ configurable: false,
+ writable: false,
+ value: getuid(),
+ });
+ }
+ return route[key];
+}
+
// styles moved to the top of the file so getDefaultProps can refer to it
var styles = StyleSheet.create({
container: {
@@ -220,11 +238,6 @@ var Navigator = React.createClass({
*/
onDidFocus: PropTypes.func,
- /**
- * Will be called with (ref, indexInStack, route) when the scene ref changes
- */
- onItemRef: PropTypes.func,
-
/**
* Optionally provide a navigation bar that persists across scene
* transitions
@@ -277,7 +290,6 @@ var Navigator = React.createClass({
sceneConfigStack: routeStack.map(
(route) => this.props.configureScene(route)
),
- idStack: routeStack.map(() => getuid()),
routeStack,
presentedIndex: initialRouteIndex,
transitionFromIndex: null,
@@ -318,7 +330,6 @@ var Navigator = React.createClass({
onPanResponderMove: this._handlePanResponderMove,
onPanResponderTerminate: this._handlePanResponderTerminate,
});
- this._itemRefs = {};
this._interactionHandle = null;
this._emitWillFocus(this.state.routeStack[this.state.presentedIndex]);
},
@@ -345,7 +356,6 @@ var Navigator = React.createClass({
immediatelyResetRouteStack: function(nextRouteStack) {
var destIndex = nextRouteStack.length - 1;
this.setState({
- idStack: nextRouteStack.map(getuid),
routeStack: nextRouteStack,
sceneConfigStack: nextRouteStack.map(
this.props.configureScene
@@ -870,17 +880,14 @@ var Navigator = React.createClass({
invariant(!!route, 'Must supply route to push');
var activeLength = this.state.presentedIndex + 1;
var activeStack = this.state.routeStack.slice(0, activeLength);
- 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),
]);
this._emitWillFocus(nextStack[destIndex]);
this.setState({
- idStack: nextIDStack,
routeStack: nextStack,
sceneConfigStack: nextAnimationConfigStack,
}, () => {
@@ -930,12 +937,8 @@ var Navigator = React.createClass({
return;
}
- // I don't believe we need to lock for a replace since there's no
- // navigation actually happening
- var nextIDStack = this.state.idStack.slice();
var nextRouteStack = this.state.routeStack.slice();
var nextAnimationModeStack = this.state.sceneConfigStack.slice();
- nextIDStack[index] = getuid();
nextRouteStack[index] = route;
nextAnimationModeStack[index] = this.props.configureScene(route);
@@ -943,7 +946,6 @@ var Navigator = React.createClass({
this._emitWillFocus(route);
}
this.setState({
- idStack: nextIDStack,
routeStack: nextRouteStack,
sceneConfigStack: nextAnimationModeStack,
}, () => {
@@ -1006,63 +1008,34 @@ var Navigator = React.createClass({
return this.state.routeStack.slice();
},
- _handleItemRef: function(itemId, route, ref) {
- this._itemRefs[itemId] = ref;
- var itemIndex = this.state.idStack.indexOf(itemId);
- if (itemIndex === -1) {
- return;
- }
- this.props.onItemRef && this.props.onItemRef(ref, itemIndex, route);
- },
-
_cleanScenesPastIndex: function(index) {
var newStackLength = index + 1;
// Remove any unneeded rendered routes.
if (newStackLength < this.state.routeStack.length) {
- this.state.idStack.slice(newStackLength).map((removingId) => {
- this._itemRefs[removingId] = null;
- });
this.setState({
sceneConfigStack: this.state.sceneConfigStack.slice(0, newStackLength),
- idStack: this.state.idStack.slice(0, newStackLength),
routeStack: this.state.routeStack.slice(0, newStackLength),
});
}
},
_renderScene: function(route, i) {
- var child = this.props.renderScene(
- route,
- this
- );
var disabledSceneStyle = null;
if (i !== this.state.presentedIndex) {
disabledSceneStyle = styles.disabledScene;
}
- var originalRef = child.ref;
- if (originalRef != null && typeof originalRef !== 'function') {
- console.warn(
- 'String refs are not supported for navigator scenes. Use a callback ' +
- 'ref instead. Ignoring ref: ' + originalRef
- );
- originalRef = null;
- }
return (
{
return (this.state.transitionFromIndex != null) || (this.state.transitionFromIndex != null);
}}
style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}>
- {React.cloneElement(child, {
- ref: component => {
- this._handleItemRef(this.state.idStack[i], route, component);
- if (originalRef) {
- originalRef(component);
- }
- }
- })}
+ {this.props.renderScene(
+ route,
+ this
+ )}
);
},
diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js
index 4b8724cbf..aa9eb64ce 100644
--- a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js
+++ b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js
@@ -33,6 +33,8 @@ var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet');
var View = require('View');
+var { Map } = require('immutable');
+
var invariant = require('invariant');
var Interpolators = NavigatorBreadcrumbNavigationBarStyles.Interpolators;
@@ -86,7 +88,6 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
}),
navState: React.PropTypes.shape({
routeStack: React.PropTypes.arrayOf(React.PropTypes.object),
- idStack: React.PropTypes.arrayOf(React.PropTypes.number),
presentedIndex: React.PropTypes.number,
}),
style: View.propTypes.style,
@@ -173,11 +174,19 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
}
},
+ componentWillMount: function() {
+ this._descriptors = {
+ crumb: new Map(),
+ title: new Map(),
+ right: new Map(),
+ };
+ },
+
render: function() {
var navState = this.props.navState;
- var icons = navState && navState.routeStack.map(this._renderOrReturnBreadcrumb);
- var titles = navState.routeStack.map(this._renderOrReturnTitle);
- var buttons = navState.routeStack.map(this._renderOrReturnRightButton);
+ var icons = navState && navState.routeStack.map(this._getBreadcrumb);
+ var titles = navState.routeStack.map(this._getTitle);
+ var buttons = navState.routeStack.map(this._getRightButton);
return (
{titles}
@@ -187,104 +196,69 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
);
},
- _renderOrReturnBreadcrumb: function(route, index) {
- var uid = this.props.navState.idStack[index];
- var navBarRouteMapper = this.props.routeMapper;
- var navOps = this.props.navigator;
- var alreadyRendered = this.refs['crumbContainer' + uid];
- if (alreadyRendered) {
- // Don't bother re-calculating the children
- return (
-
- );
+ _getBreadcrumb: function(route, index) {
+ if (this._descriptors.crumb.has(route)) {
+ return this._descriptors.crumb.get(route);
}
+
+ var navBarRouteMapper = this.props.routeMapper;
var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
- return (
-
-
-
- {navBarRouteMapper.iconForRoute(route, navOps)}
-
-
- {navBarRouteMapper.separatorForRoute(route, navOps)}
-
+
+ var breadcrumbDescriptor = (
+
+
+ {navBarRouteMapper.iconForRoute(route, this.props.navigator)}
-
+
+ {navBarRouteMapper.separatorForRoute(route, this.props.navigator)}
+
+
);
+
+ this._descriptors.crumb = this._descriptors.crumb.set(route, breadcrumbDescriptor);
+ return breadcrumbDescriptor;
},
- _renderOrReturnTitle: function(route, index) {
- var navState = this.props.navState;
- var uid = navState.idStack[index];
- var alreadyRendered = this.refs['titleContainer' + uid];
- if (alreadyRendered) {
- // Don't bother re-calculating the children
- return (
-
- );
+ _getTitle: function(route, index) {
+ if (this._descriptors.title.has(route)) {
+ return this._descriptors.title.get(route);
}
- var navBarRouteMapper = this.props.routeMapper;
- var titleContent = navBarRouteMapper.titleContentForRoute(
- navState.routeStack[index],
+
+ var titleContent = this.props.routeMapper.titleContentForRoute(
+ this.props.navState.routeStack[index],
this.props.navigator
);
var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
- return (
-
-
- {titleContent}
-
-
+
+ var titleDescriptor = (
+
+ {titleContent}
+
);
+ this._descriptors.title = this._descriptors.title.set(route, titleDescriptor);
+ return titleDescriptor;
},
- _renderOrReturnRightButton: function(route, index) {
- var navState = this.props.navState;
- var navBarRouteMapper = this.props.routeMapper;
- var uid = navState.idStack[index];
- var alreadyRendered = this.refs['rightContainer' + uid];
- if (alreadyRendered) {
- // Don't bother re-calculating the children
- return (
-
- );
+ _getRightButton: function(route, index) {
+ if (this._descriptors.right.has(route)) {
+ return this._descriptors.right.get(route);
}
- var rightContent = navBarRouteMapper.rightContentForRoute(
- navState.routeStack[index],
+ var rightContent = this.props.routeMapper.rightContentForRoute(
+ this.props.navState.routeStack[index],
this.props.navigator
);
if (!rightContent) {
+ this._descriptors.right = this._descriptors.right.set(route, null);
return null;
}
var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
- return (
-
-
- {rightContent}
-
-
+ var rightButtonDescriptor = (
+
+ {rightContent}
+
);
+ this._descriptors.right = this._descriptors.right.set(route, rightButtonDescriptor);
+ return rightButtonDescriptor;
},
});
diff --git a/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js
index 172819de2..7b69e2635 100644
--- a/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js
+++ b/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js
@@ -32,6 +32,8 @@ var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet');
var View = require('View');
+var { Map } = require('immutable');
+
var COMPONENT_NAMES = ['Title', 'LeftButton', 'RightButton'];
var navStatePresentedIndex = function(navState) {
@@ -53,7 +55,6 @@ var NavigatorNavigationBar = React.createClass({
}),
navState: React.PropTypes.shape({
routeStack: React.PropTypes.arrayOf(React.PropTypes.object),
- idStack: React.PropTypes.arrayOf(React.PropTypes.number),
presentedIndex: React.PropTypes.number,
}),
style: View.propTypes.style,
@@ -63,6 +64,16 @@ var NavigatorNavigationBar = React.createClass({
Styles: NavigatorNavigationBarStyles,
},
+ componentWillMount: function() {
+ this._components = {};
+ this._descriptors = {};
+
+ COMPONENT_NAMES.forEach(componentName => {
+ this._components[componentName] = new Map();
+ this._descriptors[componentName] = new Map();
+ });
+ },
+
_getReusableProps: function(
/*string*/componentName,
/*number*/index
@@ -104,7 +115,7 @@ var NavigatorNavigationBar = React.createClass({
}
COMPONENT_NAMES.forEach(function (componentName) {
- var component = this.refs[componentName + index];
+ var component = this._components[componentName].get(this.props.navState.routeStack[index]);
var props = this._getReusableProps(componentName, index);
if (component && interpolate[componentName](props.style, amount)) {
component.setNativeProps(props);
@@ -128,7 +139,7 @@ var NavigatorNavigationBar = React.createClass({
var navState = this.props.navState;
var components = COMPONENT_NAMES.map(function (componentName) {
return navState.routeStack.map(
- this._renderOrReturnComponent.bind(this, componentName)
+ this._getComponent.bind(this, componentName)
);
}, this);
@@ -139,28 +150,19 @@ var NavigatorNavigationBar = React.createClass({
);
},
- _renderOrReturnComponent: function(
+ _getComponent: function(
/*string*/componentName,
/*object*/route,
/*number*/index
- ) /*object*/ {
- var navState = this.props.navState;
- var uid = navState.idStack[index];
- var containerRef = componentName + 'Container' + uid;
- var alreadyRendered = this.refs[containerRef];
- if (alreadyRendered) {
- // Don't bother re-calculating the children
- return (
-
- );
+ ) /*?Object*/ {
+ if (this._descriptors[componentName].includes(route)) {
+ return this._descriptors[componentName].get(route);
}
+ var rendered = null;
+
var content = this.props.routeMapper[componentName](
- navState.routeStack[index],
+ this.props.navState.routeStack[index],
this.props.navigator,
index,
this.props.navState
@@ -171,16 +173,18 @@ var NavigatorNavigationBar = React.createClass({
var initialStage = index === navStatePresentedIndex(this.props.navState) ?
NavigatorNavigationBarStyles.Stages.Center : NavigatorNavigationBarStyles.Stages.Left;
- return (
-
-
- {content}
-
-
+ rendered = (
+ {
+ this._components[componentName] = this._components[componentName].set(route, ref);
+ }}
+ style={initialStage[componentName]}>
+ {content}
+
);
+
+ this._descriptors[componentName] = this._descriptors[componentName].set(route, rendered);
+ return rendered;
},
});
diff --git a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js
index 439534ddd..37715e678 100644
--- a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js
+++ b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js
@@ -163,6 +163,56 @@ var ToTheLeft = {
},
};
+var ToTheUp = {
+ transformTranslate: {
+ from: {x: 0, y: 0, z: 0},
+ to: {x: 0, y: -Dimensions.get('window').height, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ opacity: {
+ value: 1.0,
+ type: 'constant',
+ },
+ translateY: {
+ from: 0,
+ to: -Dimensions.get('window').height,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+};
+
+var ToTheDown = {
+ transformTranslate: {
+ from: {x: 0, y: 0, z: 0},
+ to: {x: 0, y: Dimensions.get('window').height, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ opacity: {
+ value: 1.0,
+ type: 'constant',
+ },
+ translateY: {
+ from: 0,
+ to: Dimensions.get('window').height,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+};
+
var FromTheRight = {
opacity: {
value: 1.0,
@@ -221,6 +271,50 @@ var FromTheLeft = {
},
};
+var FromTheDown = {
+ ...FromTheRight,
+ transformTranslate: {
+ from: {y: SCREEN_HEIGHT, x: 0, z: 0},
+ to: {x: 0, y: 0, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ translateY: {
+ from: SCREEN_HEIGHT,
+ to: 0,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+};
+
+var FromTheTop = {
+ ...FromTheRight,
+ transformTranslate: {
+ from: {y: -SCREEN_HEIGHT, x: 0, z: 0},
+ to: {x: 0, y: 0, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ translateY: {
+ from: -SCREEN_HEIGHT,
+ to: 0,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+};
+
var ToTheBack = {
// Rotate *requires* you to break out each individual component of
// rotation (x, y, z, w)
@@ -378,6 +472,18 @@ var BaseRightToLeftGesture = {
direction: 'right-to-left',
};
+var BaseDownUpGesture = {
+ ...BaseLeftToRightGesture,
+ fullDistance: SCREEN_HEIGHT,
+ direction: 'down-to-up',
+};
+
+var BaseUpDownGesture = {
+ ...BaseLeftToRightGesture,
+ fullDistance: SCREEN_HEIGHT,
+ direction: 'up-to-down',
+};
+
var BaseConfig = {
// A list of all gestures that are enabled on this scene
gestures: {
@@ -468,6 +574,48 @@ var NavigatorSceneConfigs = {
out: buildStyleInterpolator(ToTheLeft),
},
},
+ VerticalUpSwipeJump: {
+ ...BaseConfig,
+ gestures: {
+ jumpBack: {
+ ...BaseDownUpGesture,
+ overswipe: BaseOverswipeConfig,
+ edgeHitWidth: null,
+ isDetachable: true,
+ },
+ jumpForward: {
+ ...BaseDownUpGesture,
+ overswipe: BaseOverswipeConfig,
+ edgeHitWidth: null,
+ isDetachable: true,
+ },
+ },
+ animationInterpolators: {
+ into: buildStyleInterpolator(FromTheDown),
+ out: buildStyleInterpolator(ToTheUp),
+ },
+ },
+ VerticalDownSwipeJump: {
+ ...BaseConfig,
+ gestures: {
+ jumpBack: {
+ ...BaseUpDownGesture,
+ overswipe: BaseOverswipeConfig,
+ edgeHitWidth: null,
+ isDetachable: true,
+ },
+ jumpForward: {
+ ...BaseUpDownGesture,
+ overswipe: BaseOverswipeConfig,
+ edgeHitWidth: null,
+ isDetachable: true,
+ },
+ },
+ animationInterpolators: {
+ into: buildStyleInterpolator(FromTheTop),
+ out: buildStyleInterpolator(ToTheDown),
+ },
+ },
};
module.exports = NavigatorSceneConfigs;
diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js
index 63534af3e..e1fc6df2f 100644
--- a/Libraries/Image/Image.ios.js
+++ b/Libraries/Image/Image.ios.js
@@ -24,7 +24,6 @@ var StyleSheetPropType = require('StyleSheetPropType');
var flattenStyle = require('flattenStyle');
var invariant = require('invariant');
-var merge = require('merge');
var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');
var verifyPropTypes = require('verifyPropTypes');
@@ -57,6 +56,7 @@ var warning = require('warning');
var Image = React.createClass({
propTypes: {
+ style: StyleSheetPropType(ImageStylePropTypes),
/**
* `uri` is a string representing the resource identifier for the image, which
* could be an http address, a local file path, or the name of a static image
@@ -93,7 +93,6 @@ var Image = React.createClass({
* image dimensions.
*/
resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']),
- style: StyleSheetPropType(ImageStylePropTypes),
/**
* A unique identifier for this element to be used in UI Automation
* testing scripts.
@@ -102,7 +101,7 @@ var Image = React.createClass({
/**
* Invoked on mount and layout changes with
*
- * {nativeEvent: { layout: {x, y, width, height}}}.
+ * {nativeEvent: {layout: {x, y, width, height}}}.
*/
onLayout: PropTypes.func,
/**
@@ -112,25 +111,23 @@ var Image = React.createClass({
/**
* Invoked on download progress with
*
- * {nativeEvent: { written, total}}.
+ * {nativeEvent: {loaded, total}}.
*/
- onLoadProgress: PropTypes.func,
- /**
- * Invoked on load abort
- */
- onLoadAbort: PropTypes.func,
+ onProgress: PropTypes.func,
/**
* Invoked on load error
*
- * {nativeEvent: { error}}.
+ * {nativeEvent: {error}}.
*/
- onLoadError: PropTypes.func,
+ onError: PropTypes.func,
/**
- * Invoked on load end
- *
+ * Invoked when load completes successfully
*/
- onLoaded: PropTypes.func
-
+ onLoad: PropTypes.func,
+ /**
+ * Invoked when load either succeeds or fails
+ */
+ onLoadEnd: PropTypes.func,
},
statics: {
@@ -149,46 +146,27 @@ 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 source = resolveAssetSource(this.props.source) || {};
+ var defaultSource = (this.props.defaultSource && resolveAssetSource(this.props.defaultSource)) || {};
var {width, height} = source;
- var style = flattenStyle([{width, height}, styles.base, this.props.style]);
- invariant(style, 'style must be initialized');
+ var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {};
var isNetwork = source.uri && source.uri.match(/^https?:/);
- invariant(
- !(isNetwork && source.isStatic),
- 'static image uris cannot start with "http": "' + source.uri + '"'
+ var RawImage = isNetwork ? RCTNetworkImageView : RCTImageView;
+ var resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108
+ var tintColor = (style || {}).tintColor; // Workaround for flow bug t7737108
+
+ return (
+
);
- var isStored = !source.isStatic && !isNetwork;
- var RawImage = isNetwork ? RCTNetworkImage : RCTStaticImage;
-
- 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 || 'cover';
-
- var nativeProps = merge(this.props, {
- style,
- resizeMode,
- tintColor: style.tintColor,
- });
- if (isStored) {
- nativeProps.imageTag = source.uri;
- } else {
- nativeProps.src = source.uri;
- }
- if (this.props.defaultSource) {
- nativeProps.defaultImageSrc = this.props.defaultSource.uri;
- }
- nativeProps.progressHandlerRegistered = isNetwork && this.props.onLoadProgress;
- return ;
}
});
@@ -198,18 +176,7 @@ var styles = StyleSheet.create({
},
});
-var RCTNetworkImage = requireNativeComponent('RCTNetworkImageView', null);
-var RCTStaticImage = requireNativeComponent('RCTStaticImage', null);
-
-var nativeOnlyProps = {
- src: true,
- defaultImageSrc: true,
- imageTag: true,
- progressHandlerRegistered: true
-};
-if (__DEV__) {
- verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps);
- verifyPropTypes(Image, RCTNetworkImage.viewConfig, nativeOnlyProps);
-}
+var RCTImageView = requireNativeComponent('RCTImageView', null);
+var RCTNetworkImageView = (NativeModules.NetworkImageViewManager) ? requireNativeComponent('RCTNetworkImageView', null) : RCTImageView;
module.exports = Image;
diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m
index 6fac5d49b..3d331a8f1 100644
--- a/Libraries/Image/RCTCameraRollManager.m
+++ b/Libraries/Image/RCTCameraRollManager.m
@@ -14,6 +14,7 @@
#import
#import
+#import "RCTBridge.h"
#import "RCTImageLoader.h"
#import "RCTLog.h"
#import "RCTUtils.h"
@@ -22,11 +23,13 @@
RCT_EXPORT_MODULE()
+@synthesize bridge = _bridge;
+
RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseErrorBlock)errorCallback)
{
- [RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) {
+ [RCTImageLoader loadImageWithTag:imageTag bridge:_bridge callback:^(NSError *loadError, UIImage *loadedImage) {
if (loadError) {
errorCallback(loadError);
return;
diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj
index 8ecabbafd..6f18a284f 100644
--- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj
+++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj
@@ -8,17 +8,16 @@
/* Begin PBXBuildFile section */
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */; };
- 1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */; };
- 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; };
+ 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */; };
+ 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */; };
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; };
1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1345A8381B26592900583190 /* RCTImageRequestHandler.m */; };
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */; };
137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.m */; };
143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; };
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; };
+ 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; };
58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; };
- 58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */; };
- 58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -36,10 +35,10 @@
/* Begin PBXFileReference section */
03559E7D1B064D3A00730281 /* RCTDownloadTaskWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTaskWrapper.h; sourceTree = ""; };
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTaskWrapper.m; sourceTree = ""; };
- 1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImage.h; sourceTree = ""; };
- 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImage.m; sourceTree = ""; };
- 1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImageManager.h; sourceTree = ""; };
- 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImageManager.m; sourceTree = ""; };
+ 1304D5A71AA8C4A30002E2BE /* RCTImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageView.h; sourceTree = ""; };
+ 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageView.m; sourceTree = ""; };
+ 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageViewManager.h; sourceTree = ""; };
+ 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageViewManager.m; sourceTree = ""; };
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImage.h; sourceTree = ""; };
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = ""; };
1345A8371B26592900583190 /* RCTImageRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageRequestHandler.h; sourceTree = ""; };
@@ -52,13 +51,11 @@
143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; };
143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = ""; };
143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = ""; };
+ 35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = ""; };
+ 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageStoreManager.m; sourceTree = ""; };
58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; };
58B511891A9E6BD600147676 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = ""; };
58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = ""; };
- 58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageView.h; sourceTree = ""; };
- 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageView.m; sourceTree = ""; };
- 58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageViewManager.h; sourceTree = ""; };
- 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageViewManager.m; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -89,14 +86,12 @@
137620341B31C53500677FF0 /* RCTImagePickerManager.m */,
1345A8371B26592900583190 /* RCTImageRequestHandler.h */,
1345A8381B26592900583190 /* RCTImageRequestHandler.m */,
- 58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */,
- 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */,
- 58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */,
- 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */,
- 1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */,
- 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */,
- 1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */,
- 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */,
+ 1304D5A71AA8C4A30002E2BE /* RCTImageView.h */,
+ 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */,
+ 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */,
+ 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */,
+ 35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */,
+ 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */,
134B00A01B54232B00EC8DFB /* RCTImageUtils.h */,
134B00A11B54232B00EC8DFB /* RCTImageUtils.m */,
58B5115E1A9E6B3D00147676 /* Products */,
@@ -169,17 +164,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */,
58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */,
137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */,
- 58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */,
- 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */,
+ 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */,
1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */,
- 58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */,
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */,
143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */,
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */,
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */,
- 1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */,
+ 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */,
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/Libraries/Image/RCTImageDownloader.h b/Libraries/Image/RCTImageDownloader.h
index 43bb9a69d..44ad1cde3 100644
--- a/Libraries/Image/RCTImageDownloader.h
+++ b/Libraries/Image/RCTImageDownloader.h
@@ -43,11 +43,4 @@ typedef void (^RCTImageDownloadCancellationBlock)(void);
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTImageDownloadBlock)block;
-/**
- * Cancel an in-flight download. If multiple requets have been made for the
- * same image, only the request that relates to the token passed will be
- * cancelled.
- */
-- (void)cancelDownload:(RCTImageDownloadCancellationBlock)downloadToken;
-
@end
diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m
index f32d895cb..6cec0f478 100644
--- a/Libraries/Image/RCTImageDownloader.m
+++ b/Libraries/Image/RCTImageDownloader.m
@@ -52,7 +52,9 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
return self;
}
-- (RCTImageDownloadCancellationBlock)_downloadDataForURL:(NSURL *)url progressBlock:progressBlock block:(RCTCachedDataDownloadBlock)block
+- (RCTImageDownloadCancellationBlock)_downloadDataForURL:(NSURL *)url
+ progressBlock:progressBlock
+ block:(RCTCachedDataDownloadBlock)block
{
NSString *const cacheKey = url.absoluteString;
@@ -134,7 +136,9 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
return [cancel copy];
}
-- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock block:(RCTDataDownloadBlock)block
+- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url
+ progressBlock:(RCTDataProgressBlock)progressBlock
+ block:(RCTDataDownloadBlock)block
{
return [self _downloadDataForURL:url progressBlock:progressBlock block:^(BOOL cached, NSURLResponse *response, NSData *data, NSError *error) {
block(data, error);
@@ -150,24 +154,19 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTImageDownloadBlock)block
{
+ scale = scale ?: RCTScreenScale();
+
return [self downloadDataForURL:url progressBlock:progressBlock block:^(NSData *data, NSError *error) {
if (!data || error) {
block(nil, error);
return;
}
- if (CGSizeEqualToSize(size, CGSizeZero)) {
- // Target size wasn't available yet, so abort image drawing
- block(nil, nil);
- return;
- }
-
UIImage *image = [UIImage imageWithData:data scale:scale];
- if (image) {
+ if (image && !CGSizeEqualToSize(size, CGSizeZero)) {
// Get scale and size
- CGFloat destScale = scale ?: RCTScreenScale();
- CGRect imageRect = RCTClipRect(image.size, image.scale, size, destScale, resizeMode);
+ CGRect imageRect = RCTClipRect(image.size, scale, size, scale, resizeMode);
CGSize destSize = RCTTargetSizeForClipRect(imageRect);
// Opacity optimizations
@@ -183,7 +182,7 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
}
// Decompress image at required size
- UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale);
+ UIGraphicsBeginImageContextWithOptions(destSize, opaque, scale);
if (blendColor) {
[blendColor setFill];
UIRectFill((CGRect){CGPointZero, destSize});
@@ -201,11 +200,4 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
}];
}
-- (void)cancelDownload:(RCTImageDownloadCancellationBlock)downloadToken
-{
- if (downloadToken) {
- downloadToken();
- }
-}
-
@end
diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h
index 4337836fd..5498f23b7 100644
--- a/Libraries/Image/RCTImageLoader.h
+++ b/Libraries/Image/RCTImageLoader.h
@@ -10,6 +10,11 @@
#import
@class ALAssetsLibrary;
+@class RCTBridge;
+
+typedef void (^RCTImageLoaderProgressBlock)(int64_t written, int64_t total);
+typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id /* UIImage or CAAnimation */);
+typedef void (^RCTImageLoaderCancellationBlock)(void);
@interface RCTImageLoader : NSObject
@@ -22,22 +27,30 @@
* Can be called from any thread.
* Will always call callback on main thread.
*/
-+ (void)loadImageWithTag:(NSString *)imageTag
- callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;
++ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
+ bridge:(RCTBridge *)bridge
+ callback:(RCTImageLoaderCompletionBlock)callback;
/**
* As above, but includes target size, scale and resizeMode, which are used to
* select the optimal dimensions for the loaded image.
*/
-+ (void)loadImageWithTag:(NSString *)imageTag
- size:(CGSize)size
- scale:(CGFloat)scale
- resizeMode:(UIViewContentMode)resizeMode
- callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;
++ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
+ size:(CGSize)size
+ scale:(CGFloat)scale
+ resizeMode:(UIViewContentMode)resizeMode
+ bridge:(RCTBridge *)bridge
+ progressBlock:(RCTImageLoaderProgressBlock)progress
+ completionBlock:(RCTImageLoaderCompletionBlock)completion;
/**
* Is the specified image tag an asset library image?
*/
+ (BOOL)isAssetLibraryImage:(NSString *)imageTag;
+/**
+ * Is the specified image tag a remote image?
+ */
++ (BOOL)isRemoteImage:(NSString *)imageTag;
+
@end
diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m
index 69d98a60a..c9aeff5fe 100644
--- a/Libraries/Image/RCTImageLoader.m
+++ b/Libraries/Image/RCTImageLoader.m
@@ -15,10 +15,12 @@
#import
#import
+#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTDefines.h"
#import "RCTGIFImage.h"
#import "RCTImageDownloader.h"
+#import "RCTImageStoreManager.h"
#import "RCTImageUtils.h"
#import "RCTLog.h"
#import "RCTUtils.h"
@@ -57,24 +59,73 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
return assetsLibrary;
}
-+ (void)loadImageWithTag:(NSString *)imageTag
- callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback
++ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
+ bridge:(RCTBridge *)bridge
+ callback:(RCTImageLoaderCompletionBlock)callback
{
return [self loadImageWithTag:imageTag
size:CGSizeZero
scale:0
resizeMode:UIViewContentModeScaleToFill
- callback:callback];
+ bridge:bridge
+ progressBlock:nil
+ completionBlock:callback];
}
-+ (void)loadImageWithTag:(NSString *)imageTag
- size:(CGSize)size
- scale:(CGFloat)scale
- resizeMode:(UIViewContentMode)resizeMode
- callback:(void (^)(NSError *error, id image))callback
+// Why use a custom scaling method? Greater efficiency, reduced memory overhead:
+// http://www.mindsea.com/2012/12/downscaling-huge-alassets-without-fear-of-sigkill
+
+static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation,
+ CGSize size, CGFloat scale,
+ UIViewContentMode resizeMode,
+ NSError **error)
+{
+ NSUInteger length = (NSUInteger)representation.size;
+ NSMutableData *data = [NSMutableData dataWithLength:length];
+ if (![representation getBytes:data.mutableBytes
+ fromOffset:0
+ length:length
+ error:error]) {
+ return nil;
+ }
+
+ CGSize sourceSize = representation.dimensions;
+ CGRect targetRect = RCTClipRect(sourceSize, representation.scale, size, scale, resizeMode);
+ CGSize targetSize = targetRect.size;
+
+ NSDictionary *options = @{
+ (id)kCGImageSourceShouldAllowFloat: @YES,
+ (id)kCGImageSourceCreateThumbnailWithTransform: @YES,
+ (id)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
+ (id)kCGImageSourceThumbnailMaxPixelSize: @(MAX(targetSize.width, targetSize.height) * scale)
+ };
+
+ CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
+ CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
+ if (sourceRef) {
+ CFRelease(sourceRef);
+ }
+
+ if (imageRef) {
+ UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale
+ orientation:(UIImageOrientation)representation.orientation];
+ CGImageRelease(imageRef);
+ return image;
+ }
+
+ return nil;
+}
+
++ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
+ size:(CGSize)size
+ scale:(CGFloat)scale
+ resizeMode:(UIViewContentMode)resizeMode
+ bridge:(RCTBridge *)bridge
+ progressBlock:(RCTImageLoaderProgressBlock)progress
+ completionBlock:(RCTImageLoaderCompletionBlock)completion
{
if ([imageTag hasPrefix:@"assets-library://"]) {
- [[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) {
+ [[self assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) {
if (asset) {
// ALAssetLibrary API is async and will be multi-threaded. Loading a few full
// resolution images at once will spike the memory up to store the image data,
@@ -86,42 +137,32 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
@autoreleasepool {
BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero);
- ALAssetOrientation orientation = ALAssetOrientationUp;
- CGImageRef imageRef = NULL;
+ ALAssetRepresentation *representation = [asset defaultRepresentation];
- if (!useMaximumSize) {
- imageRef = asset.thumbnail;
- }
- if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) {
- if (!useMaximumSize) {
- imageRef = asset.aspectRatioThumbnail;
- }
- if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) {
- ALAssetRepresentation *representation = [asset defaultRepresentation];
- orientation = [representation orientation];
- if (!useMaximumSize) {
- imageRef = [representation fullScreenImage];
- }
- if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) {
- imageRef = [representation fullResolutionImage];
- }
- }
+ UIImage *image;
+ NSError *error = nil;
+ if (useMaximumSize) {
+ image = [UIImage imageWithCGImage:representation.fullResolutionImage
+ scale:scale
+ orientation:(UIImageOrientation)representation.orientation];
+ } else {
+ image = RCTScaledImageForAsset(representation, size, scale, resizeMode, &error);
}
- UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:(UIImageOrientation)orientation];
- RCTDispatchCallbackOnMainQueue(callback, nil, image);
+ RCTDispatchCallbackOnMainQueue(completion, error, image);
}
});
} else {
NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageTag];
NSError *error = RCTErrorWithMessage(errorText);
- RCTDispatchCallbackOnMainQueue(callback, error, nil);
+ RCTDispatchCallbackOnMainQueue(completion, error, nil);
}
} failureBlock:^(NSError *loadError) {
NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@.\niOS Error: %@", imageTag, loadError];
NSError *error = RCTErrorWithMessage(errorText);
- RCTDispatchCallbackOnMainQueue(callback, error, nil);
+ RCTDispatchCallbackOnMainQueue(completion, error, nil);
}];
+ return ^{};
} else if ([imageTag hasPrefix:@"ph://"]) {
// Using PhotoKit for iOS 8+
// The 'ph://' prefix is used by FBMediaKit to differentiate between
@@ -132,71 +173,103 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
if (results.count == 0) {
NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", phAssetID];
NSError *error = RCTErrorWithMessage(errorText);
- RCTDispatchCallbackOnMainQueue(callback, error, nil);
- return;
+ RCTDispatchCallbackOnMainQueue(completion, error, nil);
+ return ^{};
}
PHAsset *asset = [results firstObject];
- CGSize targetSize = CGSizeEqualToSize(size, CGSizeZero) ? PHImageManagerMaximumSize : size;
+
+ PHImageRequestOptions *imageOptions = [[PHImageRequestOptions alloc] init];
+
+ BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero);
+ CGSize targetSize;
+
+ if ( useMaximumSize ){
+ targetSize = PHImageManagerMaximumSize;
+ imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone;
+ } else {
+ targetSize = size;
+ imageOptions.resizeMode = PHImageRequestOptionsResizeModeFast;
+ }
+
PHImageContentMode contentMode = PHImageContentModeAspectFill;
if (resizeMode == UIViewContentModeScaleAspectFit) {
contentMode = PHImageContentModeAspectFit;
}
- [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:nil resultHandler:^(UIImage *result, NSDictionary *info) {
+ [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:imageOptions resultHandler:^(UIImage *result, NSDictionary *info) {
if (result) {
- RCTDispatchCallbackOnMainQueue(callback, nil, result);
+ RCTDispatchCallbackOnMainQueue(completion, nil, result);
} else {
NSString *errorText = [NSString stringWithFormat:@"Failed to load PHAsset with local identifier %@ with no error message.", phAssetID];
NSError *error = RCTErrorWithMessage(errorText);
- RCTDispatchCallbackOnMainQueue(callback, error, nil);
+ RCTDispatchCallbackOnMainQueue(completion, error, nil);
return;
}
}];
+ return ^{};
} else if ([imageTag hasPrefix:@"http"]) {
NSURL *url = [NSURL URLWithString:imageTag];
if (!url) {
NSString *errorMessage = [NSString stringWithFormat:@"Invalid URL: %@", imageTag];
- RCTDispatchCallbackOnMainQueue(callback, RCTErrorWithMessage(errorMessage), nil);
- return;
+ RCTDispatchCallbackOnMainQueue(completion, RCTErrorWithMessage(errorMessage), nil);
+ return ^{};
}
- if ([[imageTag lowercaseString] hasSuffix:@".gif"]) {
- [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:nil block:^(NSData *data, NSError *error) {
+ if ([imageTag.lowercaseString hasSuffix:@".gif"]) {
+ return [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:progress block:^(NSData *data, NSError *error) {
id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]);
if (!image && !error) {
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", imageTag];
error = RCTErrorWithMessage(errorMessage);
}
- RCTDispatchCallbackOnMainQueue(callback, error, image);
+ RCTDispatchCallbackOnMainQueue(completion, error, image);
}];
} else {
- [[RCTImageDownloader sharedInstance] downloadImageForURL:url size:size scale:scale resizeMode:resizeMode tintColor:nil backgroundColor:nil progressBlock:NULL block:^(UIImage *image, NSError *error) {
- RCTDispatchCallbackOnMainQueue(callback, error, image);
+ return [[RCTImageDownloader sharedInstance] downloadImageForURL:url size:size scale:scale resizeMode:resizeMode tintColor:nil backgroundColor:nil progressBlock:progress block:^(UIImage *image, NSError *error) {
+ RCTDispatchCallbackOnMainQueue(completion, error, image);
}];
}
- } else if ([[imageTag lowercaseString] hasSuffix:@".gif"]) {
+ } else if ([imageTag hasPrefix:@"rct-image-store://"]) {
+ [bridge.imageStoreManager getImageForTag:imageTag withBlock:^(UIImage *image) {
+ if (image) {
+ RCTDispatchCallbackOnMainQueue(completion, nil, image);
+ } else {
+ NSString *errorMessage = [NSString stringWithFormat:@"Unable to load image from image store: %@", imageTag];
+ NSError *error = RCTErrorWithMessage(errorMessage);
+ RCTDispatchCallbackOnMainQueue(completion, error, nil);
+ }
+ }];
+ return ^{};
+ } else if ([imageTag.lowercaseString hasSuffix:@".gif"]) {
id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]);
if (image) {
- RCTDispatchCallbackOnMainQueue(callback, nil, image);
+ RCTDispatchCallbackOnMainQueue(completion, nil, image);
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", imageTag];
NSError *error = RCTErrorWithMessage(errorMessage);
- RCTDispatchCallbackOnMainQueue(callback, error, nil);
+ RCTDispatchCallbackOnMainQueue(completion, error, nil);
}
+ return ^{};
} else {
UIImage *image = [RCTConvert UIImage:imageTag];
if (image) {
- RCTDispatchCallbackOnMainQueue(callback, nil, image);
+ RCTDispatchCallbackOnMainQueue(completion, nil, image);
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Unrecognized tag protocol: %@", imageTag];
NSError *error = RCTErrorWithMessage(errorMessage);
- RCTDispatchCallbackOnMainQueue(callback, error, nil);
+ RCTDispatchCallbackOnMainQueue(completion, error, nil);
}
+ return ^{};
}
}
+ (BOOL)isAssetLibraryImage:(NSString *)imageTag
{
- return [imageTag hasPrefix:@"assets-library://"] || [imageTag hasPrefix:@"ph:"];
+ return [imageTag hasPrefix:@"assets-library://"] || [imageTag hasPrefix:@"ph://"];
+}
+
++ (BOOL)isRemoteImage:(NSString *)imageTag
+{
+ return [imageTag hasPrefix:@"http://"] || [imageTag hasPrefix:@"https://"];
}
@end
diff --git a/Libraries/Image/RCTImageRequestHandler.m b/Libraries/Image/RCTImageRequestHandler.m
index e5eb3bfd4..c1f485b33 100644
--- a/Libraries/Image/RCTImageRequestHandler.m
+++ b/Libraries/Image/RCTImageRequestHandler.m
@@ -10,6 +10,7 @@
#import
+#import "RCTBridge.h"
#import "RCTImageLoader.h"
#import "RCTUtils.h"
@@ -20,6 +21,8 @@
RCT_EXPORT_MODULE()
+@synthesize bridge = _bridge;
+
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
return [@[@"assets-library", @"ph"] containsObject:[request.URL.scheme lowercaseString]];
@@ -30,7 +33,7 @@ RCT_EXPORT_MODULE()
{
NSNumber *requestToken = @(++_currentToken);
NSString *URLString = [request.URL absoluteString];
- [RCTImageLoader loadImageWithTag:URLString callback:^(NSError *error, UIImage *image) {
+ [RCTImageLoader loadImageWithTag:URLString bridge:_bridge callback:^(NSError *error, UIImage *image) {
if (error) {
[delegate URLRequest:requestToken didCompleteWithError:error];
return;
diff --git a/Libraries/Image/RCTImageStoreManager.h b/Libraries/Image/RCTImageStoreManager.h
new file mode 100644
index 000000000..afb4f24cb
--- /dev/null
+++ b/Libraries/Image/RCTImageStoreManager.h
@@ -0,0 +1,29 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+#import
+
+#import "RCTBridge.h"
+#import "RCTURLRequestHandler.h"
+
+@interface RCTImageStoreManager : NSObject
+
+/**
+ * Set and get cached images. These must be called from the main thread.
+ */
+- (NSString *)storeImage:(UIImage *)image;
+- (UIImage *)imageForTag:(NSString *)imageTag;
+
+/**
+ * Set and get cached images asynchronously. It is safe to call these from any
+ * thread. The callbacks will be called on the main thread.
+ */
+- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block;
+- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block;
+
+@end
+
+@interface RCTBridge (RCTImageStoreManager)
+
+@property (nonatomic, readonly) RCTImageStoreManager *imageStoreManager;
+
+@end
diff --git a/Libraries/Image/RCTImageStoreManager.m b/Libraries/Image/RCTImageStoreManager.m
new file mode 100644
index 000000000..e751466f9
--- /dev/null
+++ b/Libraries/Image/RCTImageStoreManager.m
@@ -0,0 +1,149 @@
+/**
+ * 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 "RCTImageStoreManager.h"
+
+#import "RCTAssert.h"
+#import "RCTUtils.h"
+
+@implementation RCTImageStoreManager
+{
+ NSMutableDictionary *_store;
+}
+
+@synthesize methodQueue = _methodQueue;
+
+RCT_EXPORT_MODULE()
+
+- (id)init
+{
+ if ((self = [super init])) {
+
+ // TODO: need a way to clear this store
+ _store = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (NSString *)storeImage:(UIImage *)image
+{
+ RCTAssertMainThread();
+ NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", [_store count]];
+ _store[tag] = image;
+ return tag;
+}
+
+- (UIImage *)imageForTag:(NSString *)imageTag
+{
+ RCTAssertMainThread();
+ return _store[imageTag];
+}
+
+- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ NSString *imageTag = [self storeImage:image];
+ if (block) {
+ block(imageTag);
+ }
+ });
+}
+
+- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
+{
+ RCTAssert(block != nil, @"block must not be nil");
+ dispatch_async(dispatch_get_main_queue(), ^{
+ block([self imageForTag:imageTag]);
+ });
+}
+
+// TODO (#5906496): Name could be more explicit - something like getBase64EncodedJPEGDataForTag:?
+RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag
+ successCallback:(RCTResponseSenderBlock)successCallback
+ errorCallback:(RCTResponseErrorBlock)errorCallback)
+{
+ [self getImageForTag:imageTag withBlock:^(UIImage *image) {
+ if (!image) {
+ errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]));
+ return;
+ }
+ dispatch_async(_methodQueue, ^{
+ NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
+ NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
+ successCallback(@[[base64 stringByReplacingOccurrencesOfString:@"\n" withString:@""]]);
+ });
+ }];
+}
+
+RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
+ successCallback:(RCTResponseSenderBlock)successCallback
+ errorCallback:(RCTResponseErrorBlock)errorCallback)
+
+{
+ NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
+ if (imageData) {
+ UIImage *image = [[UIImage alloc] initWithData:imageData];
+ [self storeImage:image withBlock:^(NSString *imageTag) {
+ successCallback(@[imageTag]);
+ }];
+ } else {
+ errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String"));
+ }
+}
+
+#pragma mark - RCTURLRequestHandler
+
+- (BOOL)canHandleRequest:(NSURLRequest *)request
+{
+ return [@[@"rct-image-store"] containsObject:[request.URL.scheme lowercaseString]];
+}
+
+- (id)sendRequest:(NSURLRequest *)request
+ withDelegate:(id)delegate
+{
+ NSString *imageTag = [request.URL absoluteString];
+ [self getImageForTag:imageTag withBlock:^(UIImage *image) {
+ if (!image) {
+ NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]);
+ [delegate URLRequest:request didCompleteWithError:error];
+ return;
+ }
+
+ NSString *mimeType = nil;
+ NSData *imageData = nil;
+ if (RCTImageHasAlpha(image.CGImage)) {
+ mimeType = @"image/png";
+ imageData = UIImagePNGRepresentation(image);
+ } else {
+ mimeType = @"image/jpeg";
+ imageData = UIImageJPEGRepresentation(image, 1.0);
+ }
+
+ NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
+ MIMEType:mimeType
+ expectedContentLength:imageData.length
+ textEncodingName:nil];
+
+ [delegate URLRequest:request didReceiveResponse:response];
+ [delegate URLRequest:request didReceiveData:imageData];
+ [delegate URLRequest:request didCompleteWithError:nil];
+ }];
+ return request;
+}
+
+@end
+
+@implementation RCTBridge (RCTImageStoreManager)
+
+- (RCTImageStoreManager *)imageStoreManager
+{
+ return self.modules[RCTBridgeModuleNameForClass([RCTImageStoreManager class])];
+}
+
+@end
diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m
index 89d269532..7b2d88ebc 100644
--- a/Libraries/Image/RCTImageUtils.m
+++ b/Libraries/Image/RCTImageUtils.m
@@ -11,6 +11,24 @@
#import "RCTLog.h"
+static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
+{
+ return ceil(value * scale) / scale;
+}
+
+static CGFloat RCTFloorValue(CGFloat value, CGFloat scale)
+{
+ return floor(value * scale) / scale;
+}
+
+static CGSize RCTCeilSize(CGSize size, CGFloat scale)
+{
+ return (CGSize){
+ RCTCeilValue(size.width, scale),
+ RCTCeilValue(size.height, scale)
+ };
+}
+
CGSize RCTTargetSizeForClipRect(CGRect clipRect)
{
return (CGSize){
@@ -48,7 +66,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
sourceSize.width = MIN(destSize.width, sourceSize.width);
sourceSize.height = MIN(destSize.height, sourceSize.height);
- return (CGRect){CGPointZero, sourceSize};
+ return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)};
case UIViewContentModeScaleAspectFit: // contain
@@ -62,7 +80,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height);
sourceSize.width = sourceSize.height * aspect;
}
- return (CGRect){CGPointZero, sourceSize};
+ return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)};
case UIViewContentModeScaleAspectFill: // cover
@@ -71,20 +89,26 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height);
sourceSize.width = sourceSize.height * aspect;
destSize.width = destSize.height * targetAspect;
- return (CGRect){{(destSize.width - sourceSize.width) / 2, 0}, sourceSize};
+ return (CGRect){
+ {RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale), 0},
+ RCTCeilSize(sourceSize, destScale)
+ };
} else { // target is wider than content
sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width);
sourceSize.height = sourceSize.width / aspect;
destSize.height = destSize.width / targetAspect;
- return (CGRect){{0, (destSize.height - sourceSize.height) / 2}, sourceSize};
+ return (CGRect){
+ {0, RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale)},
+ RCTCeilSize(sourceSize, destScale)
+ };
}
default:
RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode);
- return (CGRect){CGPointZero, destSize};
+ return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)};
}
}
diff --git a/Libraries/Image/RCTStaticImage.h b/Libraries/Image/RCTImageView.h
similarity index 72%
rename from Libraries/Image/RCTStaticImage.h
rename to Libraries/Image/RCTImageView.h
index c8f46a302..fff7c96a0 100644
--- a/Libraries/Image/RCTStaticImage.h
+++ b/Libraries/Image/RCTImageView.h
@@ -9,9 +9,14 @@
#import
-@interface RCTStaticImage : UIImageView
+@class RCTBridge;
+
+@interface RCTImageView : UIImageView
+
+- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
@property (nonatomic, assign) UIEdgeInsets capInsets;
+@property (nonatomic, strong) UIImage *defaultImage;
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
@property (nonatomic, copy) NSString *src;
diff --git a/Libraries/Image/RCTStaticImage.m b/Libraries/Image/RCTImageView.m
similarity index 55%
rename from Libraries/Image/RCTStaticImage.m
rename to Libraries/Image/RCTImageView.m
index 0e9d4b608..ea7b6047e 100644
--- a/Libraries/Image/RCTStaticImage.m
+++ b/Libraries/Image/RCTImageView.m
@@ -7,16 +7,41 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
-#import "RCTStaticImage.h"
+#import "RCTImageView.h"
+#import "RCTBridge.h"
#import "RCTConvert.h"
+#import "RCTEventDispatcher.h"
#import "RCTGIFImage.h"
#import "RCTImageLoader.h"
#import "RCTUtils.h"
#import "UIView+React.h"
-@implementation RCTStaticImage
+@interface RCTImageView ()
+
+@property (nonatomic, assign) BOOL onLoadStart;
+@property (nonatomic, assign) BOOL onProgress;
+@property (nonatomic, assign) BOOL onError;
+@property (nonatomic, assign) BOOL onLoad;
+@property (nonatomic, assign) BOOL onLoadEnd;
+
+@end
+
+@implementation RCTImageView
+{
+ RCTBridge *_bridge;
+}
+
+- (instancetype)initWithBridge:(RCTBridge *)bridge
+{
+ if ((self = [super init])) {
+ _bridge = bridge;
+ }
+ return self;
+}
+
+RCT_NOT_IMPLEMENTED(-init)
- (void)_updateImage
{
@@ -45,7 +70,7 @@
- (void)setImage:(UIImage *)image
{
if (image != super.image) {
- super.image = image;
+ super.image = image ?: _defaultImage;
[self _updateImage];
}
}
@@ -77,19 +102,56 @@
- (void)reloadImage
{
if (_src && !CGSizeEqualToSize(self.frame.size, CGSizeZero)) {
+
+ if (_onLoadStart) {
+ NSDictionary *event = @{ @"target": self.reactTag };
+ [_bridge.eventDispatcher sendInputEventWithName:@"loadStart" body:event];
+ }
+
+ RCTImageLoaderProgressBlock progressHandler = nil;
+ if (_onProgress) {
+ progressHandler = ^(int64_t loaded, int64_t total) {
+ NSDictionary *event = @{
+ @"target": self.reactTag,
+ @"loaded": @(loaded),
+ @"total": @(total),
+ };
+ [_bridge.eventDispatcher sendInputEventWithName:@"progress" body:event];
+ };
+ }
+
[RCTImageLoader loadImageWithTag:_src
size:self.bounds.size
scale:RCTScreenScale()
- resizeMode:self.contentMode callback:^(NSError *error, id image) {
- if (error) {
- RCTLogWarn(@"%@", error.localizedDescription);
- }
+ resizeMode:self.contentMode
+ bridge:_bridge
+ progressBlock:progressHandler
+ completionBlock:^(NSError *error, id image) {
+
if ([image isKindOfClass:[CAAnimation class]]) {
[self.layer addAnimation:image forKey:@"contents"];
} else {
[self.layer removeAnimationForKey:@"contents"];
self.image = image;
}
+ if (error) {
+ if (_onError) {
+ NSDictionary *event = @{
+ @"target": self.reactTag,
+ @"error": error.localizedDescription,
+ };
+ [_bridge.eventDispatcher sendInputEventWithName:@"error" body:event];
+ }
+ } else {
+ if (_onLoad) {
+ NSDictionary *event = @{ @"target": self.reactTag };
+ [_bridge.eventDispatcher sendInputEventWithName:@"load" body:event];
+ }
+ }
+ if (_onLoadEnd) {
+ NSDictionary *event = @{ @"target": self.reactTag };
+ [_bridge.eventDispatcher sendInputEventWithName:@"loadEnd" body:event];
+ }
}];
} else {
[self.layer removeAnimationForKey:@"contents"];
@@ -102,7 +164,7 @@
[super reactSetFrame:frame];
if (self.image == nil) {
[self reloadImage];
- } else if ([RCTImageLoader isAssetLibraryImage:_src]) {
+ } else if ([RCTImageLoader isAssetLibraryImage:_src] || [RCTImageLoader isRemoteImage:_src]) {
CGSize imageSize = {
self.image.size.width / RCTScreenScale(),
self.image.size.height / RCTScreenScale()
@@ -116,4 +178,23 @@
}
}
+- (void)willMoveToSuperview:(UIView *)newSuperview
+{
+ [super willMoveToSuperview:newSuperview];
+
+ if (!newSuperview) {
+ [self.layer removeAnimationForKey:@"contents"];
+ self.image = nil;
+ }
+}
+
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
+
+ if (self.superview && self.src) {
+ [self reloadImage];
+ }
+}
+
@end
diff --git a/Libraries/Image/RCTStaticImageManager.h b/Libraries/Image/RCTImageViewManager.h
similarity index 87%
rename from Libraries/Image/RCTStaticImageManager.h
rename to Libraries/Image/RCTImageViewManager.h
index b02f9fe11..4e8d3fac4 100644
--- a/Libraries/Image/RCTStaticImageManager.h
+++ b/Libraries/Image/RCTImageViewManager.h
@@ -9,6 +9,6 @@
#import "RCTViewManager.h"
-@interface RCTStaticImageManager : RCTViewManager
+@interface RCTImageViewManager : RCTViewManager
@end
diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m
new file mode 100644
index 000000000..28f93466a
--- /dev/null
+++ b/Libraries/Image/RCTImageViewManager.m
@@ -0,0 +1,57 @@
+/**
+ * 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 "RCTImageViewManager.h"
+
+#import
+
+#import "RCTConvert.h"
+#import "RCTImageView.h"
+
+@implementation RCTImageViewManager
+
+RCT_EXPORT_MODULE()
+
+- (UIView *)view
+{
+ return [[RCTImageView alloc] initWithBridge:self.bridge];
+}
+
+RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets)
+RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage)
+RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
+RCT_EXPORT_VIEW_PROPERTY(src, NSString)
+RCT_EXPORT_VIEW_PROPERTY(onLoadStart, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(onProgress, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(onError, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(onLoad, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, BOOL)
+RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
+{
+ if (json) {
+ view.renderingMode = UIImageRenderingModeAlwaysTemplate;
+ view.tintColor = [RCTConvert UIColor:json];
+ } else {
+ view.renderingMode = defaultView.renderingMode;
+ view.tintColor = defaultView.tintColor;
+ }
+}
+
+- (NSDictionary *)customDirectEventTypes
+{
+ return @{
+ @"loadStart": @{ @"registrationName": @"onLoadStart" },
+ @"progress": @{ @"registrationName": @"onProgress" },
+ @"error": @{ @"registrationName": @"onError" },
+ @"load": @{ @"registrationName": @"onLoad" },
+ @"loadEnd": @{ @"registrationName": @"onLoadEnd" },
+ };
+}
+
+@end
diff --git a/Libraries/Image/RCTNetworkImageView.h b/Libraries/Image/RCTNetworkImageView.h
deleted file mode 100644
index 6dd73e9aa..000000000
--- a/Libraries/Image/RCTNetworkImageView.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * 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
-
-@class RCTEventDispatcher;
-@class RCTImageDownloader;
-
-@interface RCTNetworkImageView : UIView
-
-- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
- imageDownloader:(RCTImageDownloader *)imageDownloader NS_DESIGNATED_INITIALIZER;
-
-/**
- * An image that will appear while the view is loading the image from the network,
- * or when imageURL is nil. Defaults to nil.
- */
-@property (nonatomic, strong) UIImage *defaultImage;
-
-/**
- * Specify a URL for an image. The image will be asynchronously loaded and displayed.
- */
-@property (nonatomic, strong) NSURL *imageURL;
-
-/**
- * Whether the image should be masked with this view's tint color.
- */
-@property (nonatomic, assign) BOOL tinted;
-
-/**
- * By default, changing imageURL will reset whatever existing image was present
- * and revert to defaultImage while the new image loads. In certain obscure cases you
- * may want to disable this behavior and instead keep displaying the previous image
- * while the new one loads. In this case, pass NO for resetToDefaultImageWhileLoading.
- * (If you set imageURL to nil, however, resetToDefaultImageWhileLoading is ignored;
- * that will always reset to the default image.)
- */
-- (void)setImageURL:(NSURL *)imageURL resetToDefaultImageWhileLoading:(BOOL)reset;
-
-@end
diff --git a/Libraries/Image/RCTNetworkImageView.m b/Libraries/Image/RCTNetworkImageView.m
deleted file mode 100644
index 20d297b46..000000000
--- a/Libraries/Image/RCTNetworkImageView.m
+++ /dev/null
@@ -1,220 +0,0 @@
-/**
- * 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 "RCTNetworkImageView.h"
-
-#import "RCTAssert.h"
-#import "RCTConvert.h"
-#import "RCTGIFImage.h"
-#import "RCTImageDownloader.h"
-#import "RCTUtils.h"
-#import "RCTBridgeModule.h"
-#import "RCTEventDispatcher.h"
-#import "UIView+React.h"
-
-@implementation RCTNetworkImageView
-{
- BOOL _deferred;
- BOOL _progressHandlerRegistered;
- NSURL *_imageURL;
- NSURL *_deferredImageURL;
- NSUInteger _deferSentinel;
- RCTImageDownloader *_imageDownloader;
- id _downloadToken;
- RCTEventDispatcher *_eventDispatcher;
-}
-
-- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher imageDownloader:(RCTImageDownloader *)imageDownloader
-{
- if ((self = [super initWithFrame:CGRectZero])) {
- _eventDispatcher = eventDispatcher;
- _progressHandlerRegistered = NO;
- _deferSentinel = 0;
- _imageDownloader = imageDownloader;
- self.userInteractionEnabled = NO;
- self.contentMode = UIViewContentModeScaleAspectFill;
- }
- return self;
-}
-
-RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame)
-RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
-
-- (NSURL *)imageURL
-{
- // We clear our imageURL when we are not in a window for a while,
- // to make sure we don't consume network resources while offscreen.
- // However we don't want to expose this hackery externally.
- return _deferred ? _deferredImageURL : _imageURL;
-}
-
-- (void)setBackgroundColor:(UIColor *)backgroundColor
-{
- super.backgroundColor = backgroundColor;
- [self _updateImage];
-}
-
-- (void)setTintColor:(UIColor *)tintColor
-{
- super.tintColor = tintColor;
- [self _updateImage];
-}
-
-- (void)setProgressHandlerRegistered:(BOOL)progressHandlerRegistered
-{
- _progressHandlerRegistered = progressHandlerRegistered;
-}
-
-- (void)reactSetFrame:(CGRect)frame
-{
- [super reactSetFrame:frame];
- [self _updateImage];
-}
-
-- (void)_updateImage
-{
- [self setImageURL:_imageURL resetToDefaultImageWhileLoading:NO];
-}
-
-- (void)setImageURL:(NSURL *)imageURL resetToDefaultImageWhileLoading:(BOOL)reset
-{
- if (![_imageURL isEqual:imageURL] && _downloadToken) {
- [_imageDownloader cancelDownload:_downloadToken];
- NSDictionary *event = @{ @"target": self.reactTag };
- [_eventDispatcher sendInputEventWithName:@"loadAbort" body:event];
- _downloadToken = nil;
- }
-
- _imageURL = imageURL;
-
- if (_deferred) {
- _deferredImageURL = imageURL;
- } else {
- if (!imageURL) {
- self.layer.contents = nil;
- return;
- }
- if (reset) {
- self.layer.contentsScale = _defaultImage.scale;
- self.layer.contents = (__bridge id)_defaultImage.CGImage;
- self.layer.minificationFilter = kCAFilterTrilinear;
- self.layer.magnificationFilter = kCAFilterTrilinear;
- }
- [_eventDispatcher sendInputEventWithName:@"loadStart" body:@{ @"target": self.reactTag }];
-
- RCTDataProgressBlock progressHandler = ^(int64_t written, int64_t total) {
- if (_progressHandlerRegistered) {
- NSDictionary *event = @{
- @"target": self.reactTag,
- @"written": @(written),
- @"total": @(total),
- };
- [_eventDispatcher sendInputEventWithName:@"loadProgress" body:event];
- }
- };
-
- void (^errorHandler)(NSString *errorDescription) = ^(NSString *errorDescription) {
- NSDictionary *event = @{
- @"target": self.reactTag,
- @"error": errorDescription,
- };
- [_eventDispatcher sendInputEventWithName:@"loadError" body:event];
- };
-
- void (^loadEndHandler)(void) = ^(void) {
- NSDictionary *event = @{ @"target": self.reactTag };
- [_eventDispatcher sendInputEventWithName:@"loaded" body:event];
- };
-
- if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) {
- _downloadToken = [_imageDownloader downloadDataForURL:imageURL progressBlock:progressHandler block:^(NSData *data, NSError *error) {
- if (data) {
- dispatch_async(dispatch_get_main_queue(), ^{
- if (imageURL != self.imageURL) {
- // Image has changed
- return;
- }
- CAKeyframeAnimation *animation = RCTGIFImageWithData(data);
- self.layer.contentsScale = 1.0;
- self.layer.minificationFilter = kCAFilterLinear;
- self.layer.magnificationFilter = kCAFilterLinear;
- [self.layer addAnimation:animation forKey:@"contents"];
- loadEndHandler();
- });
- } else if (error) {
- errorHandler([error localizedDescription]);
- }
- }];
- } else {
- _downloadToken = [_imageDownloader downloadImageForURL:imageURL
- size:self.bounds.size
- scale:RCTScreenScale()
- resizeMode:self.contentMode
- tintColor:_tinted ? self.tintColor : nil
- backgroundColor:self.backgroundColor
- progressBlock:progressHandler
- block:^(UIImage *image, NSError *error) {
- if (image) {
- dispatch_async(dispatch_get_main_queue(), ^{
- if (imageURL != self.imageURL) {
- // Image has changed
- return;
- }
- [self.layer removeAnimationForKey:@"contents"];
- self.layer.contentsScale = image.scale;
- self.layer.contents = (__bridge id)image.CGImage;
- loadEndHandler();
- });
- } else if (error) {
- errorHandler([error localizedDescription]);
- }
- }];
- }
- }
-}
-
-- (void)setImageURL:(NSURL *)imageURL
-{
- [self setImageURL:imageURL resetToDefaultImageWhileLoading:YES];
-}
-
-- (void)willMoveToWindow:(UIWindow *)newWindow
-{
- [super willMoveToWindow:newWindow];
- if (newWindow != nil && _deferredImageURL) {
- // Immediately exit deferred mode and restore the imageURL that we saved when we went offscreen.
- [self setImageURL:_deferredImageURL resetToDefaultImageWhileLoading:YES];
- _deferredImageURL = nil;
- }
-}
-
-- (void)_enterDeferredModeIfNeededForSentinel:(NSUInteger)sentinel
-{
- if (self.window == nil && _deferSentinel == sentinel) {
- _deferred = YES;
- [_imageDownloader cancelDownload:_downloadToken];
- _downloadToken = nil;
- _deferredImageURL = _imageURL;
- _imageURL = nil;
- }
-}
-
-- (void)didMoveToWindow
-{
- [super didMoveToWindow];
- if (self.window == nil) {
- __weak RCTNetworkImageView *weakSelf = self;
- NSUInteger sentinelAtDispatchTime = ++_deferSentinel;
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^(void){
- [weakSelf _enterDeferredModeIfNeededForSentinel:sentinelAtDispatchTime];
- });
- }
-}
-
-@end
diff --git a/Libraries/Image/RCTNetworkImageViewManager.h b/Libraries/Image/RCTNetworkImageViewManager.h
deleted file mode 100644
index 3176ce896..000000000
--- a/Libraries/Image/RCTNetworkImageViewManager.h
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * 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 "RCTViewManager.h"
-
-@interface RCTNetworkImageViewManager : RCTViewManager
-
-@end
-
diff --git a/Libraries/Image/RCTNetworkImageViewManager.m b/Libraries/Image/RCTNetworkImageViewManager.m
deleted file mode 100644
index f42ef48f1..000000000
--- a/Libraries/Image/RCTNetworkImageViewManager.m
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * 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 "RCTNetworkImageViewManager.h"
-
-#import "RCTBridge.h"
-#import "RCTConvert.h"
-#import "RCTImageDownloader.h"
-#import "RCTNetworkImageView.h"
-#import "RCTUtils.h"
-
-@implementation RCTNetworkImageViewManager
-
-RCT_EXPORT_MODULE()
-
-@synthesize bridge = _bridge;
-@synthesize methodQueue = _methodQueue;
-
-- (UIView *)view
-{
- return [[RCTNetworkImageView alloc] initWithEventDispatcher:self.bridge.eventDispatcher imageDownloader:[RCTImageDownloader sharedInstance]];
-}
-
-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(progressHandlerRegistered, BOOL)
-RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTNetworkImageView)
-{
- if (json) {
- view.tinted = YES;
- view.tintColor = [RCTConvert UIColor:json];
- } else {
- view.tinted = defaultView.tinted;
- view.tintColor = defaultView.tintColor;
- }
-}
-
-- (NSDictionary *)customDirectEventTypes
-{
- return @{
- @"loadStart": @{ @"registrationName": @"onLoadStart" },
- @"loadProgress": @{ @"registrationName": @"onLoadProgress" },
- @"loaded": @{ @"registrationName": @"onLoadEnd" },
- @"loadError": @{ @"registrationName": @"onLoadError" },
- @"loadAbort": @{ @"registrationName": @"onLoadAbort" },
- };
-}
-
-@end
diff --git a/Libraries/Image/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m
deleted file mode 100644
index 7b3fb16db..000000000
--- a/Libraries/Image/RCTStaticImageManager.m
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * 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 "RCTStaticImageManager.h"
-
-#import
-
-#import "RCTConvert.h"
-#import "RCTStaticImage.h"
-
-@implementation RCTStaticImageManager
-
-RCT_EXPORT_MODULE()
-
-- (UIView *)view
-{
- return [[RCTStaticImage alloc] init];
-}
-
-RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets)
-RCT_REMAP_VIEW_PROPERTY(imageTag, src, NSString)
-RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
-RCT_EXPORT_VIEW_PROPERTY(src, NSString)
-RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTStaticImage)
-{
- if (json) {
- view.renderingMode = UIImageRenderingModeAlwaysTemplate;
- view.tintColor = [RCTConvert UIColor:json];
- } else {
- view.renderingMode = defaultView.renderingMode;
- view.tintColor = defaultView.tintColor;
- }
-}
-
-@end
diff --git a/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m
index 6a6b385af..c89a4fbc9 100644
--- a/Libraries/Network/RCTHTTPRequestHandler.m
+++ b/Libraries/Network/RCTHTTPRequestHandler.m
@@ -75,6 +75,16 @@ RCT_EXPORT_MODULE()
#pragma mark - NSURLSession delegate
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didSendBodyData:(int64_t)bytesSent
+ totalBytesSent:(int64_t)totalBytesSent
+totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
+{
+ [[_delegates objectForKey:task] URLRequest:task didUploadProgress:(double)totalBytesSent total:(double)totalBytesExpectedToSend];
+}
+
+
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)task
didReceiveResponse:(NSURLResponse *)response
diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m
index d86b00950..d73f1fb4d 100644
--- a/Libraries/Network/RCTNetworking.m
+++ b/Libraries/Network/RCTNetworking.m
@@ -184,6 +184,11 @@ typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError
return [self initWithRequest:nil handler:nil callback:nil];
}
+- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
+{
+ RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
+}
+
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
{
RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
@@ -230,15 +235,13 @@ RCT_EXPORT_MODULE()
}
- (void)buildRequest:(NSDictionary *)query
- responseSender:(RCTResponseSenderBlock)responseSender
+ completionBlock:(void (^)(NSURLRequest *request))block
{
NSURL *URL = [RCTConvert NSURL:query[@"url"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
request.HTTPMethod = [[RCTConvert NSString:query[@"method"]] uppercaseString] ?: @"GET";
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
- BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]];
-
NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]];
[self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) {
if (error) {
@@ -258,9 +261,7 @@ RCT_EXPORT_MODULE()
[request setValue:[@(request.HTTPBody.length) description] forHTTPHeaderField:@"Content-Length"];
}
- [self sendRequest:request
- incrementalUpdates:incrementalUpdates
- responseSender:responseSender];
+ block(request);
}];
}
@@ -399,6 +400,17 @@ RCT_EXPORT_MODULE()
#pragma mark - RCTURLRequestDelegate
+- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
+{
+ dispatch_async(_methodQueue, ^{
+ RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
+ RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
+
+ NSArray *responseJSON = @[request.requestID, @(progress), @(total)];
+ [_bridge.eventDispatcher sendDeviceEventWithName:@"didUploadProgress" body:responseJSON];
+ });
+}
+
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
{
dispatch_async(_methodQueue, ^{
@@ -464,7 +476,12 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query
responseSender:(RCTResponseSenderBlock)responseSender)
{
- [self buildRequest:query responseSender:responseSender];
+ [self buildRequest:query completionBlock:^(NSURLRequest *request) {
+
+ BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]];
+ [self sendRequest:request incrementalUpdates:incrementalUpdates
+ responseSender:responseSender];
+ }];
}
RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID)
diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js
index 6eb586c26..da020c808 100644
--- a/Libraries/Network/XMLHttpRequest.ios.js
+++ b/Libraries/Network/XMLHttpRequest.ios.js
@@ -21,15 +21,23 @@ class XMLHttpRequest extends XMLHttpRequestBase {
_requestId: ?number;
_subscriptions: [any];
+ upload: {
+ onprogress?: (event: Object) => void;
+ };
constructor() {
super();
this._requestId = null;
this._subscriptions = [];
+ this.upload = {};
}
_didCreateRequest(requestId: number): void {
this._requestId = requestId;
+ this._subscriptions.push(RCTDeviceEventEmitter.addListener(
+ 'didUploadProgress',
+ (args) => this._didUploadProgress.call(this, args[0], args[1], args[2])
+ ));
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
'didReceiveNetworkResponse',
(args) => this._didReceiveResponse.call(this, args[0], args[1], args[2])
@@ -44,6 +52,17 @@ class XMLHttpRequest extends XMLHttpRequestBase {
));
}
+ _didUploadProgress(requestId: number, progress: number, total: number): void {
+ if (requestId === this._requestId && this.upload.onprogress) {
+ var event = {
+ lengthComputable: true,
+ loaded: progress,
+ total,
+ };
+ this.upload.onprogress(event);
+ }
+ }
+
_didReceiveResponse(requestId: number, status: number, responseHeaders: ?Object): void {
if (requestId === this._requestId) {
this.status = status;
diff --git a/Libraries/Portal/Portal.js b/Libraries/Portal/Portal.js
new file mode 100644
index 000000000..a029bb99f
--- /dev/null
+++ b/Libraries/Portal/Portal.js
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @providesModule Portal
+ * @flow
+ */
+'use strict';
+
+var React = require('React');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+
+var _portalRef: any;
+
+// Unique identifiers for modals.
+var lastUsedTag = 0;
+
+/*
+ * A container that renders all the modals on top of everything else in the application.
+ *
+ * Portal makes it possible for application code to pass modal views all the way up to
+ * the root element created in `renderApplication`.
+ *
+ * Never use `` in your code. There is only one Portal instance rendered
+ * by the top-level `renderApplication`.
+ */
+var Portal = React.createClass({
+ statics: {
+ /**
+ * Use this to create a new unique tag for your component that renders
+ * modals. A good place to allocate a tag is in `componentWillMount`
+ * of your component.
+ * See `showModal` and `closeModal`.
+ */
+ allocateTag: function(): string {
+ return '__modal_' + (++lastUsedTag);
+ },
+
+ /**
+ * Render a new modal.
+ * @param tag A unique tag identifying the React component to render.
+ * This tag can be later used in `closeModal`.
+ * @param component A React component to be rendered.
+ */
+ showModal: function(tag: string, component: any) {
+ if (!_portalRef) {
+ console.error('Calling showModal but no Portal has been rendered.');
+ return;
+ }
+ _portalRef._showModal(tag, component);
+ },
+
+ /**
+ * Remove a modal from the collection of modals to be rendered.
+ * @param tag A unique tag identifying the React component to remove.
+ * Must exactly match the tag previously passed to `showModal`.
+ */
+ closeModal: function(tag: string) {
+ if (!_portalRef) {
+ console.error('Calling closeModal but no Portal has been rendered.');
+ return;
+ }
+ _portalRef._closeModal(tag);
+ },
+
+ /**
+ * Get an array of all the open modals, as identified by their tag string.
+ */
+ getOpenModals: function(): Array {
+ if (!_portalRef) {
+ console.error('Calling getOpenModals but no Portal has been rendered.');
+ return [];
+ }
+ return _portalRef._getOpenModals();
+ }
+ },
+
+ getInitialState: function() {
+ return {modals: {}};
+ },
+
+ _showModal: function(tag: string, component: any) {
+ // This way state is chained through multiple calls to
+ // _showModal, _closeModal correctly.
+ this.setState((state) => {
+ var modals = state.modals;
+ modals[tag] = component;
+ return {modals};
+ });
+ },
+
+ _closeModal: function(tag: string) {
+ if (!this.state.modals.hasOwnProperty(tag)) {
+ return;
+ }
+ // This way state is chained through multiple calls to
+ // _showModal, _closeModal correctly.
+ this.setState((state) => {
+ var modals = state.modals;
+ delete modals[tag];
+ return {modals};
+ });
+ },
+
+ _getOpenModals: function(): Array {
+ return Object.keys(this.state.modals);
+ },
+
+ render: function() {
+ _portalRef = this;
+ if (!this.state.modals) {
+ return null;
+ }
+ var modals = [];
+ for (var tag in this.state.modals) {
+ modals.push(this.state.modals[tag]);
+ }
+ if (modals.length === 0) {
+ return null;
+ }
+ return (
+
+ {modals}
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ modalsContainer: {
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ },
+});
+
+module.exports = Portal;
diff --git a/Libraries/ReactIOS/WarningBox.js b/Libraries/ReactIOS/WarningBox.js
index 37076ef5c..8caaa5e02 100644
--- a/Libraries/ReactIOS/WarningBox.js
+++ b/Libraries/ReactIOS/WarningBox.js
@@ -174,15 +174,13 @@ var WarningRow = React.createClass({
{...this.panGesture.panHandlers}>
-
- { this.text = text; }}>
- {countText}
- {this.props.warning}
-
-
+ { this.text = text; }}>
+ {countText}
+ {this.props.warning}
+
{ this.closeButton = closeButton; }}
@@ -212,30 +210,27 @@ var WarningBoxOpened = React.createClass({
return (
-
-
- {countText}
- {this.props.warning}
-
-
-
-
-
- Dismiss
-
-
-
-
-
-
- Ignore
-
-
-
-
+ onPress={this.props.onClose}
+ style={styles.yellowBox}>
+
+ {countText}
+ {this.props.warning}
+
+
+
+
+ Dismiss
+
+
+
+
+ Ignore
+
+
);
diff --git a/Libraries/Text/RCTTextField.h b/Libraries/Text/RCTTextField.h
index ef0a07887..0c8266d7e 100644
--- a/Libraries/Text/RCTTextField.h
+++ b/Libraries/Text/RCTTextField.h
@@ -11,13 +11,15 @@
@class RCTEventDispatcher;
-@interface RCTTextField : UITextField
+@interface RCTTextField : UITextField
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, assign) BOOL autoCorrect;
@property (nonatomic, assign) BOOL selectTextOnFocus;
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, strong) UIColor *placeholderTextColor;
+@property (nonatomic, assign) NSInteger mostRecentEventCount;
+@property (nonatomic, strong) NSNumber *maxLength;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m
index 46e9cc7a4..57e0499bd 100644
--- a/Libraries/Text/RCTTextField.m
+++ b/Libraries/Text/RCTTextField.m
@@ -19,6 +19,7 @@
RCTEventDispatcher *_eventDispatcher;
NSMutableArray *_reactSubviews;
BOOL _jsRequestingFirstResponder;
+ NSInteger _nativeEventCount;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
@@ -31,6 +32,7 @@
[self addTarget:self action:@selector(_textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd];
[self addTarget:self action:@selector(_textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
_reactSubviews = [[NSMutableArray alloc] init];
+ self.delegate = self;
}
return self;
}
@@ -38,10 +40,40 @@
RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
+- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
+{
+ if (_maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return
+ return YES;
+ }
+ NSUInteger allowedLength = _maxLength.integerValue - textField.text.length + range.length;
+ if (string.length > allowedLength) {
+ if (string.length > 1) {
+ // Truncate the input string so the result is exactly maxLength
+ NSString *limitedString = [string substringToIndex:allowedLength];
+ NSMutableString *newString = textField.text.mutableCopy;
+ [newString replaceCharactersInRange:range withString:limitedString];
+ textField.text = newString;
+ // Collapse selection at end of insert to match normal paste behavior
+ UITextPosition *insertEnd = [textField positionFromPosition:textField.beginningOfDocument
+ offset:(range.location + allowedLength)];
+ textField.selectedTextRange = [textField textRangeFromPosition:insertEnd toPosition:insertEnd];
+ [self _textFieldDidChange];
+ }
+ return NO;
+ } else {
+ return YES;
+ }
+}
+
- (void)setText:(NSString *)text
{
- if (![text isEqualToString:self.text]) {
+ NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
+ if (eventLag == 0 && ![text isEqualToString:self.text]) {
+ UITextRange *selection = self.selectedTextRange;
[super setText:text];
+ self.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds
+ } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
+ RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag);
}
}
@@ -122,17 +154,29 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
return self.autocorrectionType == UITextAutocorrectionTypeYes;
}
-#define RCT_TEXT_EVENT_HANDLER(delegateMethod, eventName) \
-- (void)delegateMethod \
-{ \
- [_eventDispatcher sendTextEventWithType:eventName \
- reactTag:self.reactTag \
- text:self.text]; \
+- (void)_textFieldDidChange
+{
+ _nativeEventCount++;
+ [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
+ reactTag:self.reactTag
+ text:self.text
+ eventCount:_nativeEventCount];
}
-RCT_TEXT_EVENT_HANDLER(_textFieldDidChange, RCTTextEventTypeChange)
-RCT_TEXT_EVENT_HANDLER(_textFieldEndEditing, RCTTextEventTypeEnd)
-RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit)
+- (void)_textFieldEndEditing
+{
+ [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
+ reactTag:self.reactTag
+ text:self.text
+ eventCount:_nativeEventCount];
+}
+- (void)_textFieldSubmitEditing
+{
+ [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit
+ reactTag:self.reactTag
+ text:self.text
+ eventCount:_nativeEventCount];
+}
- (void)_textFieldBeginEditing
{
@@ -143,11 +187,10 @@ RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit)
}
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
reactTag:self.reactTag
- text:self.text];
+ text:self.text
+ eventCount:_nativeEventCount];
}
-// TODO: we should support shouldChangeTextInRect (see UITextFieldDelegate)
-
- (BOOL)becomeFirstResponder
{
_jsRequestingFirstResponder = YES;
@@ -163,7 +206,8 @@ RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit)
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
reactTag:self.reactTag
- text:self.text];
+ text:self.text
+ eventCount:_nativeEventCount];
}
return result;
}
diff --git a/Libraries/Text/RCTTextFieldManager.m b/Libraries/Text/RCTTextFieldManager.m
index cc71b39fa..723ec10f9 100644
--- a/Libraries/Text/RCTTextFieldManager.m
+++ b/Libraries/Text/RCTTextFieldManager.m
@@ -29,6 +29,7 @@ RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
+RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode)
RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
@@ -56,6 +57,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextField)
{
view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName];
}
+RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
{
diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h
index 014e35315..c5012ec09 100644
--- a/Libraries/Text/RCTTextView.h
+++ b/Libraries/Text/RCTTextView.h
@@ -25,6 +25,8 @@
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *placeholderTextColor;
@property (nonatomic, strong) UIFont *font;
+@property (nonatomic, assign) NSInteger mostRecentEventCount;
+@property (nonatomic, strong) NSNumber *maxLength;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m
index f32debd47..bbb9a6927 100644
--- a/Libraries/Text/RCTTextView.m
+++ b/Libraries/Text/RCTTextView.m
@@ -21,6 +21,7 @@
NSString *_placeholder;
UITextView *_placeholderView;
UITextView *_textView;
+ NSInteger _nativeEventCount;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
@@ -124,11 +125,41 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
return _textView.text;
}
+- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
+{
+ if (_maxLength == nil) {
+ return YES;
+ }
+ NSUInteger allowedLength = _maxLength.integerValue - textView.text.length + range.length;
+ if (text.length > allowedLength) {
+ if (text.length > 1) {
+ // Truncate the input string so the result is exactly maxLength
+ NSString *limitedString = [text substringToIndex:allowedLength];
+ NSMutableString *newString = textView.text.mutableCopy;
+ [newString replaceCharactersInRange:range withString:limitedString];
+ textView.text = newString;
+ // Collapse selection at end of insert to match normal paste behavior
+ UITextPosition *insertEnd = [textView positionFromPosition:textView.beginningOfDocument
+ offset:(range.location + allowedLength)];
+ textView.selectedTextRange = [textView textRangeFromPosition:insertEnd toPosition:insertEnd];
+ [self textViewDidChange:textView];
+ }
+ return NO;
+ } else {
+ return YES;
+ }
+}
+
- (void)setText:(NSString *)text
{
- if (![text isEqualToString:_textView.text]) {
+ NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
+ if (eventLag == 0 && ![text isEqualToString:_textView.text]) {
+ UITextRange *selection = _textView.selectedTextRange;
[_textView setText:text];
[self _setPlaceholderVisibility];
+ _textView.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds
+ } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
+ RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag);
}
}
@@ -170,15 +201,18 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
reactTag:self.reactTag
- text:textView.text];
+ text:textView.text
+ eventCount:_nativeEventCount];
}
- (void)textViewDidChange:(UITextView *)textView
{
[self _setPlaceholderVisibility];
+ _nativeEventCount++;
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
reactTag:self.reactTag
- text:textView.text];
+ text:textView.text
+ eventCount:_nativeEventCount];
}
@@ -186,7 +220,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
reactTag:self.reactTag
- text:textView.text];
+ text:textView.text
+ eventCount:_nativeEventCount];
}
- (BOOL)becomeFirstResponder
@@ -204,7 +239,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
if (result) {
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
reactTag:self.reactTag
- text:_textView.text];
+ text:_textView.text
+ eventCount:_nativeEventCount];
}
return result;
}
diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m
index 570a51115..f47a106bd 100644
--- a/Libraries/Text/RCTTextViewManager.m
+++ b/Libraries/Text/RCTTextViewManager.m
@@ -29,6 +29,7 @@ RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
+RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType)
@@ -52,6 +53,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView)
{
view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName];
}
+RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
{
diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m
index 138e11695..a38476f4d 100644
--- a/React/Base/RCTBatchedBridge.m
+++ b/React/Base/RCTBatchedBridge.m
@@ -106,6 +106,13 @@ id RCTGetLatestExecutor(void)
*/
[self registerModules];
+ /**
+ * If currently profiling, hook into the current instance
+ */
+ if (RCTProfileIsProfiling()) {
+ RCTProfileHookModules(self);
+ }
+
/**
* Start the application script
*/
@@ -235,18 +242,13 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL
NSString *configJSON = RCTJSONStringify(@{
@"remoteModuleConfig": config,
}, NULL);
- dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[_javaScriptExecutor injectJSONText:configJSON
- asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:
- ^(NSError *error) {
- if (error) {
- [[RCTRedBox sharedInstance] showError:error];
- }
-
- dispatch_semaphore_signal(semaphore);
- }];
-
- dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW);
+ asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
+ callback:^(NSError *error) {
+ if (error) {
+ [[RCTRedBox sharedInstance] showError:error];
+ }
+ }];
NSURL *bundleURL = _parentBridge.bundleURL;
if (_javaScriptExecutor == nil) {
@@ -349,53 +351,41 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL
RCTLatestExecutor = nil;
}
- void (^mainThreadInvalidate)(void) = ^{
- RCTAssertMainThread();
+ [_mainDisplayLink invalidate];
+ _mainDisplayLink = nil;
- [_mainDisplayLink invalidate];
- _mainDisplayLink = nil;
-
- // Invalidate modules
- dispatch_group_t group = dispatch_group_create();
- for (RCTModuleData *moduleData in _modules) {
- if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
- [moduleData dispatchBlock:^{
- [(id)moduleData.instance invalidate];
- } dispatchGroup:group];
- }
- moduleData.queue = nil;
+ // Invalidate modules
+ dispatch_group_t group = dispatch_group_create();
+ for (RCTModuleData *moduleData in _modules) {
+ if (moduleData.instance == _javaScriptExecutor) {
+ continue;
}
- dispatch_group_notify(group, dispatch_get_main_queue(), ^{
+
+ if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
+ [moduleData dispatchBlock:^{
+ [(id)moduleData.instance invalidate];
+ } dispatchGroup:group];
+ }
+ moduleData.queue = nil;
+ }
+
+ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
+ [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
+ [_jsDisplayLink invalidate];
+ _jsDisplayLink = nil;
+
+ [_javaScriptExecutor invalidate];
+ _javaScriptExecutor = nil;
+
+ if (RCTProfileIsProfiling()) {
+ RCTProfileUnhookModules(self);
+ }
_modules = nil;
_modulesByName = nil;
_frameUpdateObservers = nil;
- });
- };
- if (!_javaScriptExecutor) {
-
- // No JS thread running
- mainThreadInvalidate();
- return;
- }
-
- [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
-
- /**
- * JS Thread deallocations
- */
- [_javaScriptExecutor invalidate];
- _javaScriptExecutor = nil;
-
- [_jsDisplayLink invalidate];
- _jsDisplayLink = nil;
-
- /**
- * Main Thread deallocations
- */
- dispatch_async(dispatch_get_main_queue(), mainThreadInvalidate);
-
- }];
+ }];
+ });
}
#pragma mark - RCTBridge methods
diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h
index 5576df64f..ebd58e75e 100644
--- a/React/Base/RCTEventDispatcher.h
+++ b/React/Base/RCTEventDispatcher.h
@@ -28,6 +28,8 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
RCTScrollEventTypeEndAnimation,
};
+extern const NSInteger RCTTextUpdateLagWarningThreshold;
+
@protocol RCTEvent
@required
@@ -76,12 +78,14 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
*/
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body;
+
/**
* Send a text input/focus event.
*/
- (void)sendTextEventWithType:(RCTTextEventType)type
reactTag:(NSNumber *)reactTag
- text:(NSString *)text;
+ text:(NSString *)text
+ eventCount:(NSInteger)eventCount;
- (void)sendEvent:(id)event;
diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m
index ac0d1097b..7638ce99d 100644
--- a/React/Base/RCTEventDispatcher.m
+++ b/React/Base/RCTEventDispatcher.m
@@ -12,6 +12,8 @@
#import "RCTAssert.h"
#import "RCTBridge.h"
+const NSInteger RCTTextUpdateLagWarningThreshold = 3;
+
static NSNumber *RCTGetEventID(id event)
{
return @(
@@ -113,6 +115,7 @@ RCT_EXPORT_MODULE()
- (void)sendTextEventWithType:(RCTTextEventType)type
reactTag:(NSNumber *)reactTag
text:(NSString *)text
+ eventCount:(NSInteger)eventCount
{
static NSString *events[] = {
@"topFocus",
@@ -124,8 +127,10 @@ RCT_EXPORT_MODULE()
[self sendInputEventWithName:events[type] body:text ? @{
@"text": text,
+ @"eventCount": @(eventCount),
@"target": reactTag
} : @{
+ @"eventCount": @(eventCount),
@"target": reactTag
}];
}
diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h
index 469a81552..66cf40bf4 100644
--- a/React/Base/RCTProfile.h
+++ b/React/Base/RCTProfile.h
@@ -103,6 +103,16 @@ RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString *
RCTProfileEndEvent([NSString stringWithFormat:@"[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd)], category, arguments); \
}
+/**
+ * Hook into a bridge instance to log all bridge module's method calls
+ */
+RCT_EXTERN void RCTProfileHookModules(RCTBridge *);
+
+/**
+ * Unhook from a given bridge instance's modules
+ */
+RCT_EXTERN void RCTProfileUnhookModules(RCTBridge *);
+
#else
#define RCTProfileBeginFlowEvent()
@@ -125,4 +135,7 @@ RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString *
#define RCTProfileBlock(block, ...) block
+#define RCTProfileHookModules(...)
+#define RCTProfileUnhookModules(...)
+
#endif
diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m
index 62a17fe7b..5174af4f1 100644
--- a/React/Base/RCTProfile.m
+++ b/React/Base/RCTProfile.m
@@ -124,6 +124,8 @@ static void RCTProfileForwardInvocation(NSObject *self, __unused SEL cmd, NSInvo
RCTProfileBeginEvent();
[invocation invoke];
RCTProfileEndEvent(name, @"objc_call,modules,auto", nil);
+ } else if ([self respondsToSelector:invocation.selector]) {
+ [invocation invoke];
} else {
// Use original selector to don't change error message
[self doesNotRecognizeSelector:invocation.selector];
@@ -144,14 +146,17 @@ static IMP RCTProfileMsgForward(NSObject *self, SEL selector)
return imp;
}
-static void RCTProfileHookModules(RCTBridge *);
-static void RCTProfileHookModules(RCTBridge *bridge)
+void RCTProfileHookModules(RCTBridge *bridge)
{
for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) {
[moduleData dispatchBlock:^{
Class moduleClass = moduleData.cls;
Class proxyClass = objc_allocateClassPair(moduleClass, RCTProfileProxyClassName(moduleClass), 0);
+ if (!proxyClass) {
+ return;
+ }
+
unsigned int methodCount;
Method *methods = class_copyMethodList(moduleClass, &methodCount);
for (NSUInteger i = 0; i < methodCount; i++) {
@@ -185,20 +190,17 @@ static void RCTProfileHookModules(RCTBridge *bridge)
}
}
-void RCTProfileUnhookModules(RCTBridge *);
void RCTProfileUnhookModules(RCTBridge *bridge)
{
- for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) {
- [moduleData dispatchBlock:^{
- RCTProfileLock(
- Class proxyClass = object_getClass(moduleData.instance);
- if (moduleData.cls != proxyClass) {
- object_setClass(moduleData.instance, moduleData.cls);
- objc_disposeClassPair(proxyClass);
- }
- );
- }];
- };
+ RCTProfileLock(
+ for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) {
+ Class proxyClass = object_getClass(moduleData.instance);
+ if (moduleData.cls != proxyClass) {
+ object_setClass(moduleData.instance, moduleData.cls);
+ objc_disposeClassPair(proxyClass);
+ }
+ };
+ );
}
diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m
index a707c6532..12a36deec 100644
--- a/React/Base/RCTRedBox.m
+++ b/React/Base/RCTRedBox.m
@@ -36,7 +36,7 @@
{
if ((self = [super initWithFrame:frame])) {
_redColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1];
- self.windowLevel = UIWindowLevelStatusBar + 5;
+ self.windowLevel = CGFLOAT_MAX;
self.backgroundColor = _redColor;
self.hidden = YES;
diff --git a/React/Base/RCTURLRequestDelegate.h b/React/Base/RCTURLRequestDelegate.h
index 3ca5b0e01..48473b84b 100644
--- a/React/Base/RCTURLRequestDelegate.h
+++ b/React/Base/RCTURLRequestDelegate.h
@@ -15,6 +15,12 @@
*/
@protocol RCTURLRequestDelegate
+/**
+ * Call this when you first receives a response from the server. This should
+ * include response headers, etc.
+ */
+- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total;
+
/**
* Call this when you first receives a response from the server. This should
* include response headers, etc.
diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h
index 23926aa71..65eb99f8c 100644
--- a/React/Base/RCTUtils.h
+++ b/React/Base/RCTUtils.h
@@ -19,7 +19,6 @@
RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error);
RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error);
RCT_EXTERN id RCTJSONParseMutable(NSString *jsonString, NSError **error);
-RCT_EXTERN id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options);
// Strip non JSON-safe values from an object graph
RCT_EXTERN id RCTJSONClean(id object);
diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m
index d342c3be4..c577bf00c 100644
--- a/React/Base/RCTUtils.m
+++ b/React/Base/RCTUtils.m
@@ -23,34 +23,99 @@
NSString *RCTJSONStringify(id jsonObject, NSError **error)
{
- NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:(NSJSONWritingOptions)NSJSONReadingAllowFragments error:error];
+ static SEL JSONKitSelector = NULL;
+ static NSSet *collectionTypes;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ SEL selector = NSSelectorFromString(@"JSONStringWithOptions:error:");
+ if ([NSDictionary instancesRespondToSelector:selector]) {
+ JSONKitSelector = selector;
+ collectionTypes = [NSSet setWithObjects:
+ [NSArray class], [NSMutableArray class],
+ [NSDictionary class], [NSMutableDictionary class], nil];
+ }
+ });
+
+ // Use JSONKit if available and object is not a fragment
+ if (JSONKitSelector && [collectionTypes containsObject:[jsonObject classForCoder]]) {
+ return ((NSString *(*)(id, SEL, int, NSError **))objc_msgSend)(jsonObject, JSONKitSelector, 0, error);
+ }
+
+ // Use Foundation JSON method
+ NSData *jsonData = [NSJSONSerialization
+ dataWithJSONObject:jsonObject
+ options:(NSJSONWritingOptions)NSJSONReadingAllowFragments
+ error:error];
return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil;
}
-id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options)
+static id _RCTJSONParse(NSString *jsonString, BOOL mutable, NSError **error)
{
- if (!jsonString) {
- return nil;
- }
- NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
- if (!jsonData) {
- jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
- if (jsonData) {
- RCTLogWarn(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString);
- } else {
- RCTLogError(@"RCTJSONParse received invalid UTF8 data");
- return nil;
+ static SEL JSONKitSelector = NULL;
+ static SEL JSONKitMutableSelector = NULL;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ SEL selector = NSSelectorFromString(@"objectFromJSONStringWithParseOptions:error:");
+ if ([NSString instancesRespondToSelector:selector]) {
+ JSONKitSelector = selector;
+ JSONKitMutableSelector = NSSelectorFromString(@"mutableObjectFromJSONStringWithParseOptions:error:");
}
+ });
+
+ if (jsonString) {
+
+ // Use JSONKit if available and string is not a fragment
+ if (JSONKitSelector) {
+ NSInteger length = jsonString.length;
+ for (NSInteger i = 0; i < length; i++) {
+ unichar c = [jsonString characterAtIndex:i];
+ if (strchr("{[", c)) {
+ static const int options = (1 << 2); // loose unicode
+ SEL selector = mutable ? JSONKitMutableSelector : JSONKitSelector;
+ return ((id (*)(id, SEL, int, NSError **))objc_msgSend)(jsonString, selector, options, error);
+ }
+ if (!strchr(" \r\n\t", c)) {
+ break;
+ }
+ }
+ }
+
+ // Use Foundation JSON method
+ NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
+ if (!jsonData) {
+ jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
+ if (jsonData) {
+ RCTLogWarn(@"RCTJSONParse received the following string, which could "
+ "not be losslessly converted to UTF8 data: '%@'", jsonString);
+ } else {
+ NSString *errorMessage = @"RCTJSONParse received invalid UTF8 data";
+ if (error) {
+ *error = RCTErrorWithMessage(errorMessage);
+ } else {
+ RCTLogError(@"%@", errorMessage);
+ }
+ return nil;
+ }
+ }
+ NSJSONReadingOptions options = NSJSONReadingAllowFragments;
+ if (mutable) {
+ options |= NSJSONReadingMutableContainers;
+ }
+ return [NSJSONSerialization JSONObjectWithData:jsonData
+ options:options
+ error:error];
}
- return [NSJSONSerialization JSONObjectWithData:jsonData options:options error:error];
+ return nil;
}
-id RCTJSONParse(NSString *jsonString, NSError **error) {
- return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingAllowFragments);
+id RCTJSONParse(NSString *jsonString, NSError **error)
+{
+ return _RCTJSONParse(jsonString, NO, error);
}
-id RCTJSONParseMutable(NSString *jsonString, NSError **error) {
- return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingMutableContainers|NSJSONReadingMutableLeaves);
+id RCTJSONParseMutable(NSString *jsonString, NSError **error)
+{
+ return _RCTJSONParse(jsonString, YES, error);
}
id RCTJSONClean(id object)
@@ -308,7 +373,8 @@ NSURL *RCTDataURL(NSString *mimeType, NSData *data)
[data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]]];
}
-static BOOL RCTIsGzippedData(NSData *data)
+BOOL RCTIsGzippedData(NSData *); // exposed for unit testing purposes
+BOOL RCTIsGzippedData(NSData *data)
{
UInt8 *bytes = (UInt8 *)data.bytes;
return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b);
diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m
index 5fc529645..daa0fa26d 100644
--- a/React/Executors/RCTContextExecutor.m
+++ b/React/Executors/RCTContextExecutor.m
@@ -12,6 +12,7 @@
#import
#import
+#import
#import "RCTAssert.h"
#import "RCTDefines.h"
@@ -20,6 +21,22 @@
#import "RCTPerformanceLogger.h"
#import "RCTUtils.h"
+#ifndef RCT_JSC_PROFILER
+#if RCT_DEV && DEBUG
+#define RCT_JSC_PROFILER 1
+#else
+#define RCT_JSC_PROFILER 0
+#endif
+#endif
+
+#if RCT_JSC_PROFILER
+#include
+
+#ifndef RCT_JSC_PROFILER_DYLIB
+#define RCT_JSC_PROFILER_DYLIB [[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"RCTJSCProfiler.ios%zd", [[[UIDevice currentDevice] systemVersion] integerValue]] ofType:@"dylib" inDirectory:@"Frameworks"] UTF8String]
+#endif
+#endif
+
@interface RCTJavaScriptContext : NSObject
@property (nonatomic, assign, readonly) JSGlobalContextRef ctx;
@@ -269,6 +286,18 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
[strongSelf _addNativeHook:RCTConsoleProfile withName:"consoleProfile"];
[strongSelf _addNativeHook:RCTConsoleProfileEnd withName:"consoleProfileEnd"];
+#if RCT_JSC_PROFILER
+ void *JSCProfiler = dlopen(RCT_JSC_PROFILER_DYLIB, RTLD_NOW);
+ if (JSCProfiler != NULL) {
+ JSObjectCallAsFunctionCallback nativeProfilerStart = dlsym(JSCProfiler, "nativeProfilerStart");
+ JSObjectCallAsFunctionCallback nativeProfilerEnd = dlsym(JSCProfiler, "nativeProfilerEnd");
+ if (nativeProfilerStart != NULL && nativeProfilerEnd != NULL) {
+ [strongSelf _addNativeHook:nativeProfilerStart withName:"nativeProfilerStart"];
+ [strongSelf _addNativeHook:nativeProfilerEnd withName:"nativeProfilerStop"];
+ }
+ }
+#endif
+
for (NSString *event in @[RCTProfileDidStartProfiling, RCTProfileDidEndProfiling]) {
[[NSNotificationCenter defaultCenter] addObserver:strongSelf
selector:@selector(toggleProfilingFlag:)
@@ -320,6 +349,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
onThread:_javaScriptThread
withObject:nil
waitUntilDone:NO];
+ _context = nil;
}
- (void)dealloc
diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m
index 724ace6e6..d1af57705 100644
--- a/React/Modules/RCTUIManager.m
+++ b/React/Modules/RCTUIManager.m
@@ -1408,8 +1408,13 @@ RCT_EXPORT_METHOD(clearJSResponder)
for (RCTViewManager *manager in _viewManagers.allValues) {
if (RCTClassOverridesInstanceMethod([manager class], @selector(customDirectEventTypes))) {
NSDictionary *eventTypes = [manager customDirectEventTypes];
- for (NSString *eventName in eventTypes) {
- RCTAssert(!customDirectEventTypes[eventName], @"Event '%@' registered multiple times.", eventName);
+ if (RCT_DEV) {
+ for (NSString *eventName in eventTypes) {
+ id eventType = customDirectEventTypes[eventName];
+ RCTAssert(!eventType || [eventType isEqual:eventTypes[eventName]],
+ @"Event '%@' registered multiple times with different "
+ "properties.", eventName);
+ }
}
[customDirectEventTypes addEntriesFromDictionary:eventTypes];
}
diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj
index 1e193d9b2..2c551600b 100644
--- a/React/React.xcodeproj/project.pbxproj
+++ b/React/React.xcodeproj/project.pbxproj
@@ -472,6 +472,7 @@
83CBBA2A1A601D0E00E9B192 /* Sources */,
83CBBA2B1A601D0E00E9B192 /* Frameworks */,
83CBBA2C1A601D0E00E9B192 /* Copy Files */,
+ 142C4F7F1B582EA6001F0B58 /* ShellScript */,
);
buildRules = (
);
@@ -528,6 +529,20 @@
shellPath = /bin/sh;
shellScript = "if nc -w 5 -z localhost 8081 ; then\n if ! curl -s \"http://localhost:8081/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port 8081 already in use, packager is either not running or not running correctly\"\n exit 2\n fi\nelse\n open $SRCROOT/../packager/launchPackager.command || echo \"Can't start packager automatically\"\nfi";
};
+ 142C4F7F1B582EA6001F0B58 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "if [[ \"$CONFIGURATION\" == \"Debug\" ]] && [[ -d \"/tmp/RCTJSCProfiler\" ]]; then\n find \"${CONFIGURATION_BUILD_DIR}\" -name '*.app' | xargs -I{} sh -c 'mkdir -p \"$1/Frameworks\" && cp -r /tmp/RCTJSCProfiler/* \"$1/Frameworks\"' -- {}\nfi";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m
index 63e3d8023..ff91b0b54 100644
--- a/React/Views/RCTNavigator.m
+++ b/React/Views/RCTNavigator.m
@@ -436,7 +436,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
*/
- (UIView *)reactSuperview
{
- RCTAssert(self.superview != nil, @"put reactNavSuperviewLink back");
+ RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back");
return self.superview ? self.superview : self.reactNavSuperviewLink;
}
diff --git a/React/Views/RCTSliderManager.m b/React/Views/RCTSliderManager.m
index f57e1f362..3dbae6916 100644
--- a/React/Views/RCTSliderManager.m
+++ b/React/Views/RCTSliderManager.m
@@ -23,6 +23,7 @@ RCT_EXPORT_MODULE()
RCTSlider *slider = [[RCTSlider alloc] init];
[slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
[slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpInside];
+ [slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpOutside];
return slider;
}