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