Updates from Thurs July 23rd

This commit is contained in:
Spencer Ahrens 2015-07-23 01:16:05 +02:00
commit bb7aa500d7
78 changed files with 2438 additions and 1176 deletions

View File

@ -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 (
<ScrollView>
<View style={styles.row}>
<Image source={image} style={styles.imageWide}/>
</View>
<View style={styles.row}>
<Image source={image} style={styles.imageThumb}/>
<Image source={image} style={styles.imageThumb}/>
<Image source={image} style={styles.imageThumb}/>
</View>
<View style={styles.row}>
<Image source={image} style={styles.imageT1}/>
<Image source={image} style={styles.imageT2}/>
</View>
</ScrollView>
);
},
});
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 = '<AssetScaledImageExample>';
exports.description = 'Example component that displays the automatic scaling capabilities of the <Image /> tag';
module.exports = AssetScaledImageExample;

View File

@ -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({
<Text>{'Group Type: ' + this.state.groupTypes}</Text>
<CameraRollView
ref={CAMERA_ROLL_VIEW}
batchSize={5}
batchSize={20}
groupTypes={this.state.groupTypes}
renderImage={this._renderImage}
/>
@ -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 (
<View key={asset} style={styles.row}>
<Image
source={asset.node.image}
style={imageStyle}
/>
<View style={styles.info}>
<Text style={styles.url}>{asset.node.image.uri}</Text>
<Text>{location}</Text>
<Text>{asset.node.group_name}</Text>
<Text>{new Date(asset.node.timestamp).toString()}</Text>
<TouchableOpacity onPress={ this.loadAsset.bind( this, asset ) }>
<View key={asset} style={styles.row}>
<Image
source={asset.node.image}
style={imageStyle}
/>
<View style={styles.info}>
<Text style={styles.url}>{asset.node.image.uri}</Text>
<Text>{location}</Text>
<Text>{asset.node.group_name}</Text>
<Text>{new Date(asset.node.timestamp).toString()}</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
},
@ -115,7 +128,7 @@ var styles = StyleSheet.create({
},
});
exports.title = '<CameraRollView>';
exports.title = 'Camera Roll';
exports.description = 'Example component that uses CameraRoll to list user\'s photos';
exports.examples = [
{

View File

@ -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({
<Image
source={this.props.source}
style={[styles.base, {overflow: 'visible'}]}
onLoadError={(e) => 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}
</Image>;
}

View File

@ -48,19 +48,19 @@ var Thumb = React.createClass({
},
render: function() {
return (
<TouchableOpacity onPress={this._onPressThumb}>
<View style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
{this.state.dir === 'column' ?
<Text>
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.
</Text> :
<Text />
}
</View>
<TouchableOpacity
onPress={this._onPressThumb}
style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
{this.state.dir === 'column' ?
<Text>
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.
</Text> :
<Text />
}
</TouchableOpacity>
);
}
@ -127,14 +127,12 @@ var ListViewPagingExample = React.createClass({
<View><Text style={styles.text}>1 Like</Text></View> :
null;
return (
<TouchableOpacity onPress={this._onPressHeader}>
<View style={styles.header}>
{headerLikeText}
<View>
<Text style={styles.text}>
Table Header (click me)
</Text>
</View>
<TouchableOpacity onPress={this._onPressHeader} style={styles.header}>
{headerLikeText}
<View>
<Text style={styles.text}>
Table Header (click me)
</Text>
</View>
</TouchableOpacity>
);

View File

@ -55,26 +55,24 @@ var BreadcrumbNavSample = React.createClass({
return (
<TouchableOpacity
onPress={() => navigator.push(_getRandomRoute())}>
<View>
<Text style={styles.titleText}>{route.title}</Text>
</View>
<Text style={styles.titleText}>{route.title}</Text>
</TouchableOpacity>
);
},
iconForRoute: function(route, navigator) {
return (
<TouchableOpacity onPress={() => {
navigator.popToRoute(route);
}}>
<View style={styles.crumbIconPlaceholder} />
</TouchableOpacity>
<TouchableOpacity
onPress={() => { navigator.popToRoute(route); }}
style={styles.crumbIconPlaceholder}
/>
);
},
separatorForRoute: function(route, navigator) {
return (
<TouchableOpacity onPress={navigator.pop}>
<View style={styles.crumbSeparatorPlaceholder} />
</TouchableOpacity>
<TouchableOpacity
onPress={navigator.pop}
style={styles.crumbSeparatorPlaceholder}
/>
);
}
};

View File

@ -51,12 +51,11 @@ var NavigationBarRouteMapper = {
var previousRoute = navState.routeStack[index - 1];
return (
<TouchableOpacity
onPress={() => navigator.pop()}>
<View style={styles.navBarLeftButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
{previousRoute.title}
</Text>
</View>
onPress={() => navigator.pop()}
style={styles.navBarLeftButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
{previousRoute.title}
</Text>
</TouchableOpacity>
);
},
@ -64,12 +63,11 @@ var NavigationBarRouteMapper = {
RightButton: function(route, navigator, index, navState) {
return (
<TouchableOpacity
onPress={() => navigator.push(newRandomRoute())}>
<View style={styles.navBarRightButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
Next
</Text>
</View>
onPress={() => navigator.push(newRandomRoute())}
style={styles.navBarRightButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
Next
</Text>
</TouchableOpacity>
);
},

View File

@ -33,7 +33,7 @@ var WithLabel = React.createClass({
{this.props.children}
</View>
);
}
},
});
var TextEventsExample = React.createClass({
@ -41,13 +41,17 @@ var TextEventsExample = React.createClass({
return {
curText: '<No Event>',
prevText: '<No Event>',
prev2Text: '<No Event>',
};
},
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({
/>
<Text style={styles.eventLabel}>
{this.state.curText}{'\n'}
(prev: {this.state.prevText})
(prev: {this.state.prevText}){'\n'}
(prev2: {this.state.prev2Text})
</Text>
</View>
);
}
});
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 (
<View style={styles.rewriteContainer}>
<TextInput
multiline={false}
maxLength={limit}
onChangeText={(text) => {
text = text.replace(/ /g, '_');
this.setState({text});
}}
style={styles.default}
value={this.state.text}
/>
<Text style={[styles.remainder, {color: remainderColor}]}>
{remainder}
</Text>
</View>
);
}
}
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 <TextInput autoFocus={true} style={styles.default} />;
}
},
{
title: "Live Re-Write (<sp> -> '_') + maxLength",
render: function() {
return <RewriteExample />;
}
},
{
title: 'Auto-capitalize',
render: function() {
@ -268,7 +315,7 @@ exports.examples = [
return (
<View>
<WithLabel label="true">
<TextInput password={true} style={styles.default} value="abc" />
<TextInput password={true} style={styles.default} defaultValue="abc" />
</WithLabel>
</View>
);
@ -276,7 +323,7 @@ exports.examples = [
},
{
title: 'Event handling',
render: function(): ReactElement { return <TextEventsExample /> },
render: function(): ReactElement { return <TextEventsExample />; },
},
{
title: 'Colored input text',
@ -285,11 +332,11 @@ exports.examples = [
<View>
<TextInput
style={[styles.default, {color: 'blue'}]}
value="Blue"
defaultValue="Blue"
/>
<TextInput
style={[styles.default, {color: 'green'}]}
value="Green"
defaultValue="Green"
/>
</View>
);
@ -336,7 +383,7 @@ exports.examples = [
<WithLabel label="clearTextOnFocus">
<TextInput
placeholder="text is cleared on focus"
value="text is cleared on focus"
defaultValue="text is cleared on focus"
style={styles.default}
clearTextOnFocus={true}
/>
@ -344,7 +391,7 @@ exports.examples = [
<WithLabel label="selectTextOnFocus">
<TextInput
placeholder="text is selected on focus"
value="text is selected on focus"
defaultValue="text is selected on focus"
style={styles.default}
selectTextOnFocus={true}
/>

View File

@ -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 = "<group>"; };
13417FE31AA91428003F314A /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = "<group>"; };
13417FEA1AA914B8003F314A /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = "<group>"; };
134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = "<group>"; };
@ -172,6 +175,7 @@
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = UIExplorer/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = UIExplorer/main.m; sourceTree = "<group>"; };
13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = "<group>"; };
13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = "<group>"; };
141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = "<group>"; };
143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
@ -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 = "<group>"; };
143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerSnapshotTests.m; sourceTree = "<group>"; };
144D21231B2204C5006DB32B /* RCTClippingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClippingTests.m; sourceTree = "<group>"; };
144D21231B2204C5006DB32B /* RCTClipRectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClipRectTests.m; sourceTree = "<group>"; };
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAllocationTests.m; sourceTree = "<group>"; };
1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeTests.m; sourceTree = "<group>"; };
1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutorTests.m; sourceTree = "<group>"; };
@ -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;

View File

@ -59,12 +59,12 @@ var IntegrationTestsApp = React.createClass({
<View style={styles.separator} />
<ScrollView>
{TESTS.map((test) => [
<TouchableOpacity onPress={() => this.setState({test})}>
<View style={styles.row}>
<Text style={styles.testName}>
{test.displayName}
</Text>
</View>
<TouchableOpacity
onPress={() => this.setState({test})}
style={styles.row}>
<Text style={styles.testName}>
{test.displayName}
</Text>
</TouchableOpacity>,
<View style={styles.separator} />
])}

View File

@ -16,10 +16,7 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIView.h>
#import <XCTest/XCTest.h>
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);
}

View File

@ -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 <XCTest/XCTest.h>
#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

View File

@ -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 <XCTest/XCTest.h>
#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

View File

@ -58,19 +58,19 @@ var WebViewExample = React.createClass({
return (
<View style={[styles.container]}>
<View style={[styles.addressBarRow]}>
<TouchableOpacity onPress={this.goBack}>
<View style={this.state.backButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'<'}
</Text>
</View>
<TouchableOpacity
onPress={this.goBack}
style={this.state.backButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'<'}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.goForward}>
<View style={this.state.forwardButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'>'}
</Text>
</View>
<TouchableOpacity
onPress={this.goForward}
style={this.state.forwardButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'>'}
</Text>
</TouchableOpacity>
<TextInput
ref={TEXT_INPUT_REF}

View File

@ -128,6 +128,7 @@ class FormUploader extends React.Component {
super(props);
this.state = {
isUploading: false,
uploadProgress: null,
randomPhoto: null,
textParams: [],
};
@ -217,6 +218,14 @@ class FormUploader extends React.Component {
this.state.textParams.forEach(
(param) => 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 {
</View>
));
var uploadButtonLabel = this.state.isUploading ? 'Uploading...' : 'Upload';
var uploadProgress = this.state.uploadProgress;
if (uploadProgress !== null) {
uploadButtonLabel += ' ' + Math.round(uploadProgress * 100) + '%';
}
var uploadButton = (
<View style={styles.uploadButtonBox}>
<Text style={styles.uploadButtonLabel}>{uploadButtonLabel}</Text>

View File

@ -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;

View File

@ -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);
}

View File

@ -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 <yajl/yajl_gen.h>
#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<JSC::ProfileNode> 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<JSC::Profile> 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);
}

108
JSCLegacyProfiler/Makefile Normal file
View File

@ -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

View File

@ -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);
}
});
},

View File

@ -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()};

View File

@ -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:
*
* ```
* <View>
* <TextInput
* style={{height: 40, borderColor: 'gray', borderWidth: 1}}
* onChangeText={(text) => this.setState({input: text})}
* onChangeText={(text) => this.setState({text})}
* value={this.state.text}
* />
* <Text>{'user input: ' + this.state.input}</Text>
* </View>
* ```
*
* The `value` prop can be used to set the value of the input in order to make
* the state of the component clear, but <TextInput> 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});
}
},
});

View File

@ -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();

View File

@ -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 (
<Animated.View
accessible={true}
style={[this.props.style, {opacity: this.state.anim}]}
testID={this.props.testID}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant}
onResponderMove={this.touchableHandleResponderMove}
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}>
{this.props.children}
</Animated.View>
);
},
});
@ -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;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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',
]);
});
});

View File

@ -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']);

View File

@ -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 (
<View
key={this.state.idStack[i]}
key={'scene_' + getRouteID(route)}
ref={'scene_' + i}
onStartShouldSetResponderCapture={() => {
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
)}
</View>
);
},

View File

@ -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 (
<View style={[styles.breadCrumbContainer, this.props.style]}>
{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 (
<StaticContainer
ref={'crumbContainer' + uid}
key={'crumbContainer' + uid}
shouldUpdate={false}
/>
);
_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 (
<StaticContainer
ref={'crumbContainer' + uid}
key={'crumbContainer' + uid}
shouldUpdate={false}>
<View ref={'crumb_' + index} style={firstStyles.Crumb}>
<View ref={'icon_' + index} style={firstStyles.Icon}>
{navBarRouteMapper.iconForRoute(route, navOps)}
</View>
<View ref={'separator_' + index} style={firstStyles.Separator}>
{navBarRouteMapper.separatorForRoute(route, navOps)}
</View>
var breadcrumbDescriptor = (
<View ref={'crumb_' + index} style={firstStyles.Crumb}>
<View ref={'icon_' + index} style={firstStyles.Icon}>
{navBarRouteMapper.iconForRoute(route, this.props.navigator)}
</View>
</StaticContainer>
<View ref={'separator_' + index} style={firstStyles.Separator}>
{navBarRouteMapper.separatorForRoute(route, this.props.navigator)}
</View>
</View>
);
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 (
<StaticContainer
ref={'titleContainer' + uid}
key={'titleContainer' + uid}
shouldUpdate={false}
/>
);
_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 (
<StaticContainer
ref={'titleContainer' + uid}
key={'titleContainer' + uid}
shouldUpdate={false}>
<View ref={'title_' + index} style={firstStyles.Title}>
{titleContent}
</View>
</StaticContainer>
var titleDescriptor = (
<View ref={'title_' + index} style={firstStyles.Title}>
{titleContent}
</View>
);
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 (
<StaticContainer
ref={'rightContainer' + uid}
key={'rightContainer' + uid}
shouldUpdate={false}
/>
);
_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 (
<StaticContainer
ref={'rightContainer' + uid}
key={'rightContainer' + uid}
shouldUpdate={false}>
<View ref={'right_' + index} style={firstStyles.RightItem}>
{rightContent}
</View>
</StaticContainer>
var rightButtonDescriptor = (
<View ref={'right_' + index} style={firstStyles.RightItem}>
{rightContent}
</View>
);
this._descriptors.right = this._descriptors.right.set(route, rightButtonDescriptor);
return rightButtonDescriptor;
},
});

View File

@ -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 (
<StaticContainer
ref={containerRef}
key={containerRef}
shouldUpdate={false}
/>
);
) /*?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 (
<StaticContainer
ref={containerRef}
key={containerRef}
shouldUpdate={false}>
<View ref={componentName + index} style={initialStage[componentName]}>
{content}
</View>
</StaticContainer>
rendered = (
<View
ref={(ref) => {
this._components[componentName] = this._components[componentName].set(route, ref);
}}
style={initialStage[componentName]}>
{content}
</View>
);
this._descriptors[componentName] = this._descriptors[componentName].set(route, rendered);
return rendered;
},
});

View File

@ -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;

View File

@ -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 (
<RawImage
{...this.props}
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
src={source.uri}
defaultSrc={defaultSource.uri}
/>
);
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 <RawImage {...nativeProps} />;
}
});
@ -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;

View File

@ -14,6 +14,7 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#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;

View File

@ -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 = "<group>"; };
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTaskWrapper.m; sourceTree = "<group>"; };
1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImage.h; sourceTree = "<group>"; };
1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImage.m; sourceTree = "<group>"; };
1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImageManager.h; sourceTree = "<group>"; };
1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImageManager.m; sourceTree = "<group>"; };
1304D5A71AA8C4A30002E2BE /* RCTImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageView.h; sourceTree = "<group>"; };
1304D5A81AA8C4A30002E2BE /* RCTImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageView.m; sourceTree = "<group>"; };
1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageViewManager.h; sourceTree = "<group>"; };
1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageViewManager.m; sourceTree = "<group>"; };
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImage.h; sourceTree = "<group>"; };
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = "<group>"; };
1345A8371B26592900583190 /* RCTImageRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageRequestHandler.h; sourceTree = "<group>"; };
@ -52,13 +51,11 @@
143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = "<group>"; };
143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = "<group>"; };
143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = "<group>"; };
35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = "<group>"; };
35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageStoreManager.m; sourceTree = "<group>"; };
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 = "<group>"; };
58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = "<group>"; };
58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageView.h; sourceTree = "<group>"; };
58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageView.m; sourceTree = "<group>"; };
58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageViewManager.h; sourceTree = "<group>"; };
58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageViewManager.m; sourceTree = "<group>"; };
/* 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;

View File

@ -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

View File

@ -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

View File

@ -10,6 +10,11 @@
#import <UIKit/UIKit.h>
@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

View File

@ -15,10 +15,12 @@
#import <Photos/PHImageManager.h>
#import <UIKit/UIKit.h>
#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

View File

@ -10,6 +10,7 @@
#import <UIKit/UIKit.h>
#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;

View File

@ -0,0 +1,29 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <UIKit/UIKit.h>
#import "RCTBridge.h"
#import "RCTURLRequestHandler.h"
@interface RCTImageStoreManager : NSObject<RCTURLRequestHandler>
/**
* 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

View File

@ -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<RCTURLRequestDelegate>)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

View File

@ -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)};
}
}

View File

@ -9,9 +9,14 @@
#import <UIKit/UIKit.h>
@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;

View File

@ -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

View File

@ -9,6 +9,6 @@
#import "RCTViewManager.h"
@interface RCTStaticImageManager : RCTViewManager
@interface RCTImageViewManager : RCTViewManager
@end

View File

@ -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 <UIKit/UIKit.h>
#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

View File

@ -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 <UIKit/UIKit.h>
@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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <UIKit/UIKit.h>
#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

View File

@ -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

View File

@ -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)

View File

@ -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;

139
Libraries/Portal/Portal.js Normal file
View File

@ -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 `<Portal>` 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<string> {
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<string> {
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 (
<View style={styles.modalsContainer}>
{modals}
</View>
);
}
});
var styles = StyleSheet.create({
modalsContainer: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
},
});
module.exports = Portal;

View File

@ -174,15 +174,13 @@ var WarningRow = React.createClass({
{...this.panGesture.panHandlers}>
<TouchableOpacity
onPress={this.props.onOpened}>
<View>
<Text
style={styles.warningText}
numberOfLines={2}
ref={text => { this.text = text; }}>
{countText}
{this.props.warning}
</Text>
</View>
<Text
style={styles.warningText}
numberOfLines={2}
ref={text => { this.text = text; }}>
{countText}
{this.props.warning}
</Text>
</TouchableOpacity>
<View
ref={closeButton => { this.closeButton = closeButton; }}
@ -212,30 +210,27 @@ var WarningBoxOpened = React.createClass({
return (
<TouchableOpacity
activeOpacity={0.9}
onPress={this.props.onClose}>
<View style={styles.yellowBox}>
<Text style={styles.yellowBoxText}>
{countText}
{this.props.warning}
</Text>
<View style={styles.yellowBoxButtons}>
<View style={styles.yellowBoxButton}>
<TouchableOpacity
onPress={this.props.onDismissed}>
<Text style={styles.yellowBoxButtonText}>
Dismiss
</Text>
</TouchableOpacity>
</View>
<View style={styles.yellowBoxButton}>
<TouchableOpacity
onPress={this.props.onIgnored}>
<Text style={styles.yellowBoxButtonText}>
Ignore
</Text>
</TouchableOpacity>
</View>
</View>
onPress={this.props.onClose}
style={styles.yellowBox}>
<Text style={styles.yellowBoxText}>
{countText}
{this.props.warning}
</Text>
<View style={styles.yellowBoxButtons}>
<TouchableOpacity
onPress={this.props.onDismissed}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Dismiss
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.props.onIgnored}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Ignore
</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
);

View File

@ -11,13 +11,15 @@
@class RCTEventDispatcher;
@interface RCTTextField : UITextField
@interface RCTTextField : UITextField<UITextFieldDelegate>
@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;

View File

@ -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;
}

View File

@ -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
{

View File

@ -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;

View File

@ -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;
}

View File

@ -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
{

View File

@ -106,6 +106,13 @@ id<RCTJavaScriptExecutor> 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<RCTInvalidating>)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<RCTInvalidating>)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

View File

@ -28,6 +28,8 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
RCTScrollEventTypeEndAnimation,
};
extern const NSInteger RCTTextUpdateLagWarningThreshold;
@protocol RCTEvent <NSObject>
@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<RCTEvent>)event;

View File

@ -12,6 +12,8 @@
#import "RCTAssert.h"
#import "RCTBridge.h"
const NSInteger RCTTextUpdateLagWarningThreshold = 3;
static NSNumber *RCTGetEventID(id<RCTEvent> 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
}];
}

View File

@ -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

View File

@ -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);
}
};
);
}

View File

@ -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;

View File

@ -15,6 +15,12 @@
*/
@protocol RCTURLRequestDelegate <NSObject>
/**
* 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.

View File

@ -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);

View File

@ -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);

View File

@ -12,6 +12,7 @@
#import <pthread.h>
#import <JavaScriptCore/JavaScriptCore.h>
#import <UIKit/UIDevice.h>
#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 <dlfcn.h>
#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 <RCTInvalidating>
@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

View File

@ -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];
}

View File

@ -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 */

View File

@ -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;
}

View File

@ -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;
}