From 82a774e92bf19b9a135004cca53b6e1e98dde052 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 17 Jul 2015 04:50:42 -0700 Subject: [PATCH 01/33] Added unit tests for gzip functionality --- .../UIExplorer.xcodeproj/project.pbxproj | 6 ++ .../UIExplorerUnitTests/RCTGzipTests.m | 81 +++++++++++++++++++ Libraries/Network/RCTNetworking.m | 15 ++-- React/Base/RCTUtils.m | 3 +- 4 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index db89c32b8d5..e6c9afc35c5 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 */; }; @@ -156,6 +157,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 = ""; }; @@ -357,6 +359,7 @@ 1497CFAA1B21F5E400C1F8F2 /* RCTSparseArrayTests.m */, 1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */, 138D6A151B53CD440074A87E /* RCTCacheTests.m */, + 1300627E1B59179B0043FE5A /* RCTGzipTests.m */, 143BC57E1B21E18100462512 /* Info.plist */, 14D6D7101B220EB3001FB087 /* libOCMock.a */, 14D6D7011B220AE3001FB087 /* OCMock */, @@ -783,6 +786,7 @@ 1497CFB01B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m in Sources */, 144D21241B2204C5006DB32B /* RCTClippingTests.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 */, @@ -1040,6 +1044,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 +1099,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/UIExplorerUnitTests/RCTGzipTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m new file mode 100644 index 00000000000..5e74bf7c4da --- /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/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index d86b0095097..df102e594d9 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -230,15 +230,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 +256,7 @@ RCT_EXPORT_MODULE() [request setValue:[@(request.HTTPBody.length) description] forHTTPHeaderField:@"Content-Length"]; } - [self sendRequest:request - incrementalUpdates:incrementalUpdates - responseSender:responseSender]; + block(request); }]; } @@ -464,7 +460,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/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index d342c3be4ce..724a9c36805 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -308,7 +308,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); From 61c648d5641da595366801d4a9ef31628cd0eee7 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 15 Jul 2015 19:17:13 -0100 Subject: [PATCH 02/33] Merged RCTNetworkImageView functionality into RCTStaticImage Summary: RCTNetworkImageView and RCTStaticImage had significant overlap in functionality, but each had a different subset of features and bugs. This diff merges most of the functionality of RCTNetworkImageView into RCTStaticImage, eliminating some bugs in the former, such as constant redrawing when properties were changed. I've also removed the onLoadAbort event for now (as it wasn't implemented), and renamed the other events to match the web specs for `` and XHMLHttpRequest. The API is essentially what Adobe proposed here: http://blogs.adobe.com/webplatform/2012/01/13/html5-image-progress-events/ The following features have not yet been ported from RCTNetworkImageView: - Background color compositing. It's not clear that this adds much value and it increases memory consumption, etc. - Image request cancelling when images are removed from view. Again, it's not clear if this is a huge benefit, but if it is it should be combined with other optimisations, such as unloading offscreen images. (Note that this only affects the open source fork. For now, internal apps will still use FBNetworkImageView for remote images.) --- Examples/UIExplorer/ImageExample.js | 10 +- Libraries/Image/Image.ios.js | 91 +++----- .../Image/RCTImage.xcodeproj/project.pbxproj | 36 +-- Libraries/Image/RCTImageDownloader.h | 7 - Libraries/Image/RCTImageDownloader.m | 30 +-- Libraries/Image/RCTImageLoader.h | 24 +- Libraries/Image/RCTImageLoader.m | 67 +++--- .../{RCTStaticImage.h => RCTImageView.h} | 7 +- .../{RCTStaticImage.m => RCTImageView.m} | 77 +++++- ...icImageManager.h => RCTImageViewManager.h} | 2 +- Libraries/Image/RCTImageViewManager.m | 57 +++++ Libraries/Image/RCTNetworkImageView.h | 46 ---- Libraries/Image/RCTNetworkImageView.m | 220 ------------------ Libraries/Image/RCTNetworkImageViewManager.h | 15 -- Libraries/Image/RCTNetworkImageViewManager.m | 56 ----- Libraries/Image/RCTStaticImageManager.m | 41 ---- React/Modules/RCTUIManager.m | 9 +- 17 files changed, 253 insertions(+), 542 deletions(-) rename Libraries/Image/{RCTStaticImage.h => RCTImageView.h} (72%) rename Libraries/Image/{RCTStaticImage.m => RCTImageView.m} (59%) rename Libraries/Image/{RCTStaticImageManager.h => RCTImageViewManager.h} (87%) create mode 100644 Libraries/Image/RCTImageViewManager.m delete mode 100644 Libraries/Image/RCTNetworkImageView.h delete mode 100644 Libraries/Image/RCTNetworkImageView.m delete mode 100644 Libraries/Image/RCTNetworkImageViewManager.h delete mode 100644 Libraries/Image/RCTNetworkImageViewManager.m delete mode 100644 Libraries/Image/RCTStaticImageManager.m diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index b886bf861cc..82721993f9e 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/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 63534af3e6e..e1fc6df2f16 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/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 8ecabbafd07..3eabd148ecf 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -8,8 +8,8 @@ /* 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 */; }; @@ -17,8 +17,6 @@ 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.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 +34,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 = ""; }; @@ -55,10 +53,6 @@ 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 +83,10 @@ 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 */, 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */, 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */, 58B5115E1A9E6B3D00147676 /* Products */, @@ -171,15 +161,13 @@ files = ( 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 43bb9a69d93..44ad1cde39b 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 f32d895cb0e..6cec0f47828 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 4337836fd31..25fdb1b3096 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -11,6 +11,10 @@ @class ALAssetsLibrary; +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 +26,28 @@ * 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 + 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 + 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 69d98a60a33..405b4907b83 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -57,21 +57,23 @@ 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 + callback:(RCTImageLoaderCompletionBlock)callback { return [self loadImageWithTag:imageTag size:CGSizeZero scale:0 resizeMode:UIViewContentModeScaleToFill - callback:callback]; + progressBlock:nil + completionBlock:callback]; } -+ (void)loadImageWithTag:(NSString *)imageTag - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode - callback:(void (^)(NSError *error, id image))callback ++ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(UIViewContentMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progress + completionBlock:(RCTImageLoaderCompletionBlock)completion { if ([imageTag hasPrefix:@"assets-library://"]) { [[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) { @@ -109,19 +111,20 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:(UIImageOrientation)orientation]; - RCTDispatchCallbackOnMainQueue(callback, nil, image); + RCTDispatchCallbackOnMainQueue(completion, nil, 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,8 +135,8 @@ 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]; @@ -144,59 +147,67 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:nil 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.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/RCTStaticImage.h b/Libraries/Image/RCTImageView.h similarity index 72% rename from Libraries/Image/RCTStaticImage.h rename to Libraries/Image/RCTImageView.h index c8f46a30282..fff7c96a0a7 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 59% rename from Libraries/Image/RCTStaticImage.m rename to Libraries/Image/RCTImageView.m index 0e9d4b60849..8773aebb72b 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,55 @@ - (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 + 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 +163,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() 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 b02f9fe11e9..4e8d3fac45b 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 00000000000..28f93466a62 --- /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 6dd73e9aa2d..00000000000 --- 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 20d297b46b0..00000000000 --- 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 3176ce89617..00000000000 --- 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 f42ef48f1b2..00000000000 --- 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 7b3fb16db84..00000000000 --- 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/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 724ace6e694..d1af57705d0 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]; } From 3c78aa6d256f3e603241ff5f2a80fffecebb00c7 Mon Sep 17 00:00:00 2001 From: DengYun Date: Fri, 17 Jul 2015 09:05:26 -0700 Subject: [PATCH 03/33] [Bridge] remove unused semaphore Summary: dispatch_semaphore_wait with DISPATCH_TIME_NOW don't wait for the semaphore. It just test whether there's a signal and return the result. So the line "dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW);" does nothing here. It seems like a bug, but each executing in js thread will be queued, so it's safe to remove the unused semaphore instead of wait for it. Closes https://github.com/facebook/react-native/pull/1915 Github Author: DengYun --- React/Base/RCTBatchedBridge.m | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 138e1169557..98fdb5115e8 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -235,18 +235,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) { From b56717ede9da9cf073152c146529c0803f604b1f Mon Sep 17 00:00:00 2001 From: Bill Fisher Date: Fri, 17 Jul 2015 12:58:39 -0700 Subject: [PATCH 04/33] [Storyline] SliderIOS responds to UIControlEventTouchUpOutside --- React/Views/RCTSliderManager.m | 1 + 1 file changed, 1 insertion(+) diff --git a/React/Views/RCTSliderManager.m b/React/Views/RCTSliderManager.m index f57e1f36209..3dbae69166a 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; } From c4389c006f77728e0642e5804fd8bae430f65caa Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Fri, 17 Jul 2015 13:21:25 -0700 Subject: [PATCH 05/33] [ReactNative] Max RedBox windowLevel to always appear on top --- React/Base/RCTRedBox.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index a707c653223..12a36deec70 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; From c28d33f3a23202c436cf794de9397050ec50923b Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Fri, 17 Jul 2015 15:20:04 -0700 Subject: [PATCH 06/33] [ReactNative] Remove dependencies on Navigator idStack Summary: idStack is going away soon. This removes all references to it. Looking at the internal state of navigator will make you have a bad time. The biggest change is switching to the new component-freezing techinique in the navigation bars. This way we avoid dependence on the idStack to provide a scalar ID for each route. --- .../NavigatorBreadcrumbNavigationBar.js | 136 +++++++----------- .../Navigator/NavigatorNavigationBar.js | 60 ++++---- 2 files changed, 87 insertions(+), 109 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js index 4b8724cbf7c..aa9eb64cebc 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 172819de2fa..7b69e2635c8 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; }, }); From 4a262e0f2b0f5e428d0599dc3c6525ebe14a877c Mon Sep 17 00:00:00 2001 From: Elliot Lynde Date: Fri, 17 Jul 2015 16:47:36 -0700 Subject: [PATCH 07/33] [React Native][Pokes Dashboard] Fix crash --- Libraries/Components/Touchable/TouchableHighlight.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index dcbfbeee19d..47d24b8117d 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(); From f2d65ea85be5aa0d7bdf87a639d869afc18245d1 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Mon, 20 Jul 2015 02:34:11 -0700 Subject: [PATCH 08/33] [ReactNative] Fix RCTJavaScriptContext deallocation (attempt #2) Summary: The context wasn't being explicitly released before, since it'd be immediately released. Now that the executors are bridge modules, it was only being deallocated when the modules were released, what caused the threads to not be released at all. --- React/Base/RCTBatchedBridge.m | 65 ++++++++++------------------ React/Executors/RCTContextExecutor.m | 1 + 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 98fdb5115e8..7975b5e7cbb 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -344,53 +344,36 @@ 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(), ^{ - _modules = nil; - _modulesByName = nil; - _frameUpdateObservers = nil; - }); - }; - if (!_javaScriptExecutor) { - - // No JS thread running - mainThreadInvalidate(); - return; + 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 executeBlockOnJavaScriptQueue:^{ + [_javaScriptExecutor invalidate]; + _javaScriptExecutor = nil; + }]; - /** - * JS Thread deallocations - */ - [_javaScriptExecutor invalidate]; - _javaScriptExecutor = nil; - - [_jsDisplayLink invalidate]; - _jsDisplayLink = nil; - - /** - * Main Thread deallocations - */ - dispatch_async(dispatch_get_main_queue(), mainThreadInvalidate); - - }]; + _modules = nil; + _modulesByName = nil; + _frameUpdateObservers = nil; + }); } #pragma mark - RCTBridge methods diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 5fc5296459e..173179c5ac2 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -320,6 +320,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) onThread:_javaScriptThread withObject:nil waitUntilDone:NO]; + _context = nil; } - (void)dealloc From 5db42643cf9d41fe5c73c47d9e04c502b1b509cb Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 20 Jul 2015 09:20:06 -0700 Subject: [PATCH 09/33] Added JSONKit support Summary: React Native will now use JSONKit if it's already available in the project, otherwise it will fall back to using NSJSONSerialization as before. This provides a small performance boost to JSON parsing in some cases. --- .../UIExplorer.xcodeproj/project.pbxproj | 8 +- .../UIExplorerUnitTests/RCTJSONTests.m | 81 ++++++++++++++ React/Base/RCTUtils.h | 1 - React/Base/RCTUtils.m | 101 ++++++++++++++---- 4 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index e6c9afc35c5..988b3f433f1 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 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 */; }; @@ -174,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 = ""; }; @@ -350,16 +352,17 @@ children = ( 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */, 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */, + 138D6A151B53CD440074A87E /* RCTCacheTests.m */, 144D21231B2204C5006DB32B /* RCTClippingTests.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 */, - 1300627E1B59179B0043FE5A /* RCTGzipTests.m */, 143BC57E1B21E18100462512 /* Info.plist */, 14D6D7101B220EB3001FB087 /* libOCMock.a */, 14D6D7011B220AE3001FB087 /* OCMock */, @@ -793,6 +796,7 @@ 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 */, ); diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m new file mode 100644 index 00000000000..b8951ca02aa --- /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/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 23926aa71b1..65eb99f8c2e 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 724a9c36805..c577bf00c25 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) From 72f7a1be6fc8de43cf364bf10c76db20a3a64e53 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Mon, 20 Jul 2015 09:14:53 -0700 Subject: [PATCH 10/33] [ReactNative] Add JavaScriptCore legacy profiler --- JSCLegacyProfiler/JSCLegacyProfiler.h | 25 ++++ JSCLegacyProfiler/JSCLegacyProfiler.mm | 161 +++++++++++++++++++++++++ JSCLegacyProfiler/Makefile | 108 +++++++++++++++++ React/Executors/RCTContextExecutor.m | 28 +++++ React/React.xcodeproj/project.pbxproj | 15 +++ 5 files changed, 337 insertions(+) create mode 100644 JSCLegacyProfiler/JSCLegacyProfiler.h create mode 100644 JSCLegacyProfiler/JSCLegacyProfiler.mm create mode 100644 JSCLegacyProfiler/Makefile diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.h b/JSCLegacyProfiler/JSCLegacyProfiler.h new file mode 100644 index 00000000000..826e39f2103 --- /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 00000000000..218c5e55d05 --- /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 00000000000..b825f776477 --- /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/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 173179c5ac2..67232489cdc 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -20,6 +20,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", [[NSProcessInfo processInfo] operatingSystemVersion].majorVersion] ofType:@"dylib" inDirectory:@"Frameworks"] UTF8String] +#endif +#endif + @interface RCTJavaScriptContext : NSObject @property (nonatomic, assign, readonly) JSGlobalContextRef ctx; @@ -269,6 +285,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:) diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 1e193d9b2c9..2c551600b4d 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 */ From 0d91b93abab4b42b1722ef199796587904b23400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Mon, 20 Jul 2015 12:32:08 -0700 Subject: [PATCH 11/33] [LAUNCH-BLOCKER] Revert JS callsite of D2201593 --- Libraries/CustomComponents/ListView/ListView.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 93ba01f3450..8aaffa16bb5 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -29,7 +29,6 @@ var ListViewDataSource = require('ListViewDataSource'); var React = require('React'); var RCTUIManager = require('NativeModules').UIManager; -var RCTScrollViewManager = require('NativeModules').ScrollViewManager; var ScrollView = require('ScrollView'); var ScrollResponder = require('ScrollResponder'); var StaticRenderer = require('StaticRenderer'); @@ -416,14 +415,6 @@ var ListView = React.createClass({ logError, this._setScrollVisibleLength ); - - // RCTScrollViewManager.calculateChildFrames is not available on - // every platform - RCTScrollViewManager && RCTScrollViewManager.calculateChildFrames && - RCTScrollViewManager.calculateChildFrames( - React.findNodeHandle(scrollComponent), - this._updateChildFrames, - ); }, _setScrollContentLength: function(left, top, width, height) { From 95dc38929c0c1cda3c9b07721f823c983f2359fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Mon, 20 Jul 2015 12:32:14 -0700 Subject: [PATCH 12/33] [rn] revert D2201593 --- Libraries/CustomComponents/ListView/ListView.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 8aaffa16bb5..93ba01f3450 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -29,6 +29,7 @@ var ListViewDataSource = require('ListViewDataSource'); var React = require('React'); var RCTUIManager = require('NativeModules').UIManager; +var RCTScrollViewManager = require('NativeModules').ScrollViewManager; var ScrollView = require('ScrollView'); var ScrollResponder = require('ScrollResponder'); var StaticRenderer = require('StaticRenderer'); @@ -415,6 +416,14 @@ var ListView = React.createClass({ logError, this._setScrollVisibleLength ); + + // RCTScrollViewManager.calculateChildFrames is not available on + // every platform + RCTScrollViewManager && RCTScrollViewManager.calculateChildFrames && + RCTScrollViewManager.calculateChildFrames( + React.findNodeHandle(scrollComponent), + this._updateChildFrames, + ); }, _setScrollContentLength: function(left, top, width, height) { From 6213950334b0e61d3cb4324337cf90a5ddd8c978 Mon Sep 17 00:00:00 2001 From: Matheus Santos Date: Mon, 20 Jul 2015 12:39:58 -0700 Subject: [PATCH 13/33] =?UTF-8?q?Added=20transitions=20'VerticalUpSwipeJum?= =?UTF-8?q?p'=20and=20'VerticalDownSwipeJump'=20in=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: … NavigatorSceneConfigs Closes https://github.com/facebook/react-native/pull/1822 Github Author: Matheus Santos --- .../Navigator/NavigatorSceneConfigs.js | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js index 439534ddda3..37715e67896 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; From 725053acfeba4c7f2a21ac47ae8100588a710476 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Mon, 20 Jul 2015 16:29:40 -0700 Subject: [PATCH 14/33] [Animated][BREAKING_CHANGE] Convert to Animated Summary: Because we don't want to integrate Animated inside of the core of React, we can only pass Animated.Value to styles of . TouchableOpacity unfortunately used cloneElement. This means that we should have asked every single call site to replace their children to Animated.View. This isn't great. The other solution is to stop using cloneElement and instead wrap the children inside of an . This has many advantages: - We no longer use cloneElement so we're no longer messing up with elements that are not our own. - Refs are now working correctly for children elements - No longer need to enforce that there's only one child and that this child is a native element The downside is that we're introducing a into the hierarchy. Sadly with CSS there is no way to have a View that doesn't affect layout. What we need to do is to remove the inner and transfer all the styles to the TouchableOpacity. It is annoying but fortunately a pretty mechanical process. I think that having a wrapper is the best solution. I will investigate to see if we can make wrappers on TouchableHighliht and TouchableWithoutFeedback as well. **Upgrade Path:** If the child is a View, move the style of the View to TouchableOpacity and remove the View itself. ``` ... --> ... ``` If the child is an Image or Text, on all the examples at Facebook it worked without any change. But it is a great idea to double check them anyway. --- Examples/UIExplorer/ListViewPagingExample.js | 40 ++++++------ .../Navigator/BreadcrumbNavSample.js | 20 +++--- .../Navigator/NavigationBarSample.js | 22 +++---- .../js/IntegrationTestsApp.js | 12 ++-- Examples/UIExplorer/WebViewExample.js | 24 +++---- .../Components/Touchable/TouchableOpacity.js | 63 ++++++++----------- Libraries/ReactIOS/WarningBox.js | 61 +++++++++--------- 7 files changed, 109 insertions(+), 133 deletions(-) diff --git a/Examples/UIExplorer/ListViewPagingExample.js b/Examples/UIExplorer/ListViewPagingExample.js index ab67c9160f3..7167f3eea81 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 5abdcd11fa1..08b8571ae58 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 545f76b828a..3148ad9c11a 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/UIExplorerIntegrationTests/js/IntegrationTestsApp.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp.js index 519e333a2e8..339766b54d2 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/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index b9137e87cec..478c2a99550 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -58,19 +58,19 @@ var WebViewExample = React.createClass({ return ( - - - - {'<'} - - + + + {'<'} + - - - - {'>'} - - + + + {'>'} + + {this.props.children} + + ); }, }); @@ -188,6 +176,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/ReactIOS/WarningBox.js b/Libraries/ReactIOS/WarningBox.js index 37076ef5c29..8caaa5e0245 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 + + ); From 270e09d46edd732764a2f165fcb44dad10928756 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Mon, 20 Jul 2015 19:30:59 -0700 Subject: [PATCH 15/33] [ReactNative][Profiler] Fix NSProcessInfo instacrash on iOS7 Summary: `NSProcessInfo operatingSystemVersion` was being used to check the system version for the Legacy Profiler on `RCTContextExecutor` but it's only available on iOS8+ Change it to `[UIDevice systemVersion]` --- React/Executors/RCTContextExecutor.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 67232489cdc..daa0fa26d52 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -12,6 +12,7 @@ #import #import +#import #import "RCTAssert.h" #import "RCTDefines.h" @@ -32,7 +33,7 @@ #include #ifndef RCT_JSC_PROFILER_DYLIB -#define RCT_JSC_PROFILER_DYLIB [[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"RCTJSCProfiler.ios%zd", [[NSProcessInfo processInfo] operatingSystemVersion].majorVersion] ofType:@"dylib" inDirectory:@"Frameworks"] UTF8String] +#define RCT_JSC_PROFILER_DYLIB [[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"RCTJSCProfiler.ios%zd", [[[UIDevice currentDevice] systemVersion] integerValue]] ofType:@"dylib" inDirectory:@"Frameworks"] UTF8String] #endif #endif From bf7497a4185e0bc1e3ed9ebfb60333578a2c40ba Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Mon, 20 Jul 2015 19:34:27 -0700 Subject: [PATCH 16/33] [RN] Move ws to react-native-github From e54e4520267c0e0e75c4ce462260b29c46b9d720 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Mon, 20 Jul 2015 19:34:46 -0700 Subject: [PATCH 17/33] [RN] Remove extra copy of stacktrace-parser From 9105b229250d9e426d93593baba82794de6dcc92 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Mon, 20 Jul 2015 19:36:09 -0700 Subject: [PATCH 18/33] [RN] Remove 14 more unused node_modules From 6e2f07fb8101274b3a4a52f3df09f7fdc61f43c9 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Mon, 20 Jul 2015 22:49:53 -0700 Subject: [PATCH 19/33] [Navigator] Add a callback that is called after emitting an event. Summary: While adeveloper requests the emitter to emit an event, the emitter may not emit the event immediately instead of putting the request into a queue and process it later. This diff allows the developer to provide a callback which will be called when the event has been emitted. For instance: ``` class NavigationContext { push(nextRoute) { var nextStack = this._stack.push(nextRoute); this.emit( 'change', { reason: 'push', nextStack: nextStack, nextRoute: nextRoute, }, this._onPush ); } _onPush(event){ if (event.defaultPrevented) { return; } this._stack = event.nextStack; this.emit('change'); } } ``` --- .../Navigator/Navigation/NavigationEvent.js | 8 +- .../Navigation/NavigationEventEmitter.js | 25 +++- .../__tests__/NavigationEventEmitter-test.js | 110 +++++++++++++++--- 3 files changed, 119 insertions(+), 24 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js index 343e1f3e68e..3a7a3d6defd 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 db9e785540f..ef63bd39d62 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/__tests__/NavigationEventEmitter-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js index 2a8d7d82a73..cc2875c814c 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', + ]); + }); }); From 151ddd9e426a3283cdb0f6882803653fdde7689b Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Mon, 20 Jul 2015 22:44:42 -0700 Subject: [PATCH 20/33] [React Native] open source ImageStoreManager native module and plug into RCTImageLoader --- Libraries/Image/RCTCameraRollManager.m | 5 +- .../Image/RCTImage.xcodeproj/project.pbxproj | 6 + Libraries/Image/RCTImageLoader.h | 3 + Libraries/Image/RCTImageLoader.m | 16 ++ Libraries/Image/RCTImageRequestHandler.m | 5 +- Libraries/Image/RCTImageStoreManager.h | 29 ++++ Libraries/Image/RCTImageStoreManager.m | 149 ++++++++++++++++++ Libraries/Image/RCTImageView.m | 1 + 8 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 Libraries/Image/RCTImageStoreManager.h create mode 100644 Libraries/Image/RCTImageStoreManager.m diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m index 6fac5d49be7..3d331a8f15e 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 3eabd148ecf..6f18a284fec 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 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 */; }; /* End PBXBuildFile section */ @@ -50,6 +51,8 @@ 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 = ""; }; @@ -87,6 +90,8 @@ 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */, 1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */, 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */, + 35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */, + 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */, 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */, 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */, 58B5115E1A9E6B3D00147676 /* Products */, @@ -159,6 +164,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */, 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */, 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */, diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index 25fdb1b3096..5498f23b710 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -10,6 +10,7 @@ #import @class ALAssetsLibrary; +@class RCTBridge; typedef void (^RCTImageLoaderProgressBlock)(int64_t written, int64_t total); typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id /* UIImage or CAAnimation */); @@ -27,6 +28,7 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); * Will always call callback on main thread. */ + (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag + bridge:(RCTBridge *)bridge callback:(RCTImageLoaderCompletionBlock)callback; /** @@ -37,6 +39,7 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode + bridge:(RCTBridge *)bridge progressBlock:(RCTImageLoaderProgressBlock)progress completionBlock:(RCTImageLoaderCompletionBlock)completion; diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 405b4907b83..4300f3b9458 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" @@ -58,12 +60,14 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } + (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag + bridge:(RCTBridge *)bridge callback:(RCTImageLoaderCompletionBlock)callback { return [self loadImageWithTag:imageTag size:CGSizeZero scale:0 resizeMode:UIViewContentModeScaleToFill + bridge:bridge progressBlock:nil completionBlock:callback]; } @@ -72,6 +76,7 @@ static dispatch_queue_t RCTImageLoaderQueue(void) size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode + bridge:(RCTBridge *)bridge progressBlock:(RCTImageLoaderProgressBlock)progress completionBlock:(RCTImageLoaderCompletionBlock)completion { @@ -177,6 +182,17 @@ static dispatch_queue_t RCTImageLoaderQueue(void) RCTDispatchCallbackOnMainQueue(completion, error, image); }]; } + } 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) { diff --git a/Libraries/Image/RCTImageRequestHandler.m b/Libraries/Image/RCTImageRequestHandler.m index e5eb3bfd4f1..c1f485b3382 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 00000000000..afb4f24cb67 --- /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 00000000000..e751466f998 --- /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/RCTImageView.m b/Libraries/Image/RCTImageView.m index 8773aebb72b..5604ec4b8cf 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -124,6 +124,7 @@ RCT_NOT_IMPLEMENTED(-init) size:self.bounds.size scale:RCTScreenScale() resizeMode:self.contentMode + bridge:_bridge progressBlock:progressHandler completionBlock:^(NSError *error, id image) { From 9c5fe3612d6aac0021120ed506c59c56a9218a10 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Mon, 20 Jul 2015 22:44:51 -0700 Subject: [PATCH 21/33] [React Native][iOS] XHR upload progress events --- Examples/UIExplorer/XHRExample.js | 13 +++++++++++++ Libraries/Network/RCTHTTPRequestHandler.m | 10 ++++++++++ Libraries/Network/RCTNetworking.m | 16 ++++++++++++++++ Libraries/Network/XMLHttpRequest.ios.js | 19 +++++++++++++++++++ React/Base/RCTURLRequestDelegate.h | 6 ++++++ 5 files changed, 64 insertions(+) diff --git a/Examples/UIExplorer/XHRExample.js b/Examples/UIExplorer/XHRExample.js index 2a3b1ebbb5c..d142222c945 100644 --- a/Examples/UIExplorer/XHRExample.js +++ b/Examples/UIExplorer/XHRExample.js @@ -128,6 +128,7 @@ class FormUploader extends React.Component { super(props); this.state = { isUploading: false, + uploadProgress: null, randomPhoto: null, textParams: [], }; @@ -217,6 +218,14 @@ class FormUploader extends React.Component { this.state.textParams.forEach( (param) => formdata.append(param.name, param.value) ); + if (xhr.upload) { + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; + } xhr.send(formdata); this.setState({isUploading: true}); } @@ -251,6 +260,10 @@ class FormUploader extends React.Component { )); 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/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m index 6a6b385af6f..c89a4fbc9c4 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 df102e594d9..d73f1fb4dad 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"); @@ -395,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, ^{ diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 6eb586c2692..da020c808f5 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/React/Base/RCTURLRequestDelegate.h b/React/Base/RCTURLRequestDelegate.h index 3ca5b0e01d4..48473b84ba3 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. From 9c73e2ff7aeac8c65f25472135e4c0410f797eea Mon Sep 17 00:00:00 2001 From: Adam Roth Date: Tue, 21 Jul 2015 05:39:57 -0700 Subject: [PATCH 22/33] [Image] Improved loading of Assets Library and Photos Framework images. Summary: Update to https://github.com/facebook/react-native/pull/1969 -- Recent improvements allow RCTImageLoader to select a more appropriate sized image based on the layout dimensions. Sizes: - asset.thumbnail - asset.aspectRatioThumbnail - asset.defaultRepresentation.fullScreenImage - asset.defaultRepresentation.fullResolutionImage Prior, only the fullResolutionImage was used. This was memory intensive and resulted in crashes when loading several large images at once. The updated implementation works well, but can be made more efficient: Consider loading 10 8MP (3264x2448) images in 150x150 pixel containers. The target size (150x150) is larger than asset.thumbnail (approx 100x100), therefore the fullScreenImage representation is used instead (approx 1334x1000). This commit will scale the asset to the minimum size required while taking into account original aspect ratio and device scale. Memory usage is considerably lower and many more images can be loaded in sequence without having to worry Closes https://github.com/facebook/react-native/pull/2008 Github Author: Adam Roth --- .../UIExplorer/AssetScaledImageExample.js | 98 +++++++++++++++++ Examples/UIExplorer/CameraRollExample.ios.js | 39 ++++--- Examples/UIExplorer/createExamplePage.js | 1 + Libraries/Image/RCTImageLoader.m | 102 ++++++++++++++---- 4 files changed, 206 insertions(+), 34 deletions(-) create mode 100644 Examples/UIExplorer/AssetScaledImageExample.js diff --git a/Examples/UIExplorer/AssetScaledImageExample.js b/Examples/UIExplorer/AssetScaledImageExample.js new file mode 100644 index 00000000000..dbfe7afb7b6 --- /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 73678407213..d783d9d8e24 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/createExamplePage.js b/Examples/UIExplorer/createExamplePage.js index 3d5a1ac88c4..352f84a4c4a 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/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 4300f3b9458..f9bfd1bf79a 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -72,6 +72,64 @@ static dispatch_queue_t RCTImageLoaderQueue(void) completionBlock:callback]; } +// +// Why use a custom scaling method: +// http://www.mindsea.com/2012/12/downscaling-huge-alassets-without-fear-of-sigkill/ +// Greater efficiency, reduced memory overhead. ++ (UIImage *)scaledImageForAssetRepresentation:(ALAssetRepresentation *)representation + size:(CGSize)size + scale:(CGFloat)scale + orientation:(UIImageOrientation)orientation +{ + UIImage *image = nil; + NSData *data = nil; + + uint8_t *buffer = (uint8_t *)malloc(sizeof(uint8_t)*(NSUInteger)[representation size]); + if (buffer != NULL) { + NSError *error = nil; + NSUInteger bytesRead = [representation getBytes:buffer fromOffset:0 length:(NSUInteger)[representation size] error:&error]; + data = [NSData dataWithBytes:buffer length:bytesRead]; + + free(buffer); + } + + if ([data length]) { + CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil); + + NSMutableDictionary *options = [NSMutableDictionary dictionary]; + + CGSize source = representation.dimensions; + CGFloat mW = size.width / source.width; + CGFloat mH = size.height / source.height; + + if (mH > mW) { + size.width = size.height / source.height * source.width; + } else if (mW > mH) { + size.height = size.width / source.width * source.height; + } + + CGFloat maxPixelSize = MAX(size.width, size.height) * scale; + + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceShouldAllowFloat]; + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailWithTransform]; + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailFromImageAlways]; + [options setObject:(id)@(maxPixelSize) forKey:(id)kCGImageSourceThumbnailMaxPixelSize]; + + CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options); + + if (imageRef) { + image = [UIImage imageWithCGImage:imageRef scale:[representation scale] orientation:orientation]; + CGImageRelease(imageRef); + } + + if (sourceRef) { + CFRelease(sourceRef); + } + } + + return image; +} + + (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag size:(CGSize)size scale:(CGFloat)scale @@ -94,28 +152,17 @@ static dispatch_queue_t RCTImageLoaderQueue(void) 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; + + if (useMaximumSize) { + image = [UIImage imageWithCGImage:representation.fullResolutionImage scale:scale orientation:(UIImageOrientation)orientation]; + + } else { + image = [self scaledImageForAssetRepresentation:representation size:size scale:scale orientation:(UIImageOrientation)orientation]; } - UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:(UIImageOrientation)orientation]; RCTDispatchCallbackOnMainQueue(completion, nil, image); } }); @@ -145,12 +192,25 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } 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(completion, nil, result); } else { From 85cb35c51439a17c0961cc80518d2e979d7279c0 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 21 Jul 2015 05:40:06 -0700 Subject: [PATCH 23/33] Fixed rotation and scaling issues when loading ALAssets using RCTImageLoader --- .../UIExplorer.xcodeproj/project.pbxproj | 8 +- ...{RCTClippingTests.m => RCTClipRectTests.m} | 21 ++-- Libraries/Image/RCTImageLoader.m | 98 ++++++++----------- Libraries/Image/RCTImageUtils.m | 34 ++++++- 4 files changed, 90 insertions(+), 71 deletions(-) rename Examples/UIExplorer/UIExplorerUnitTests/{RCTClippingTests.m => RCTClipRectTests.m} (90%) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 988b3f433f1..dd7200d21bc 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -23,7 +23,7 @@ 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 */; }; @@ -187,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 = ""; }; @@ -353,7 +353,7 @@ 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */, 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */, 138D6A151B53CD440074A87E /* RCTCacheTests.m */, - 144D21231B2204C5006DB32B /* RCTClippingTests.m */, + 144D21231B2204C5006DB32B /* RCTClipRectTests.m */, 1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */, 1497CFA71B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m */, 1497CFA81B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m */, @@ -787,7 +787,7 @@ 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 */, 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 1f94a80c1ad..0041a1b46c5 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/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index f9bfd1bf79a..c9aeff5fed9 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -72,62 +72,48 @@ static dispatch_queue_t RCTImageLoaderQueue(void) completionBlock:callback]; } -// -// Why use a custom scaling method: -// http://www.mindsea.com/2012/12/downscaling-huge-alassets-without-fear-of-sigkill/ -// Greater efficiency, reduced memory overhead. -+ (UIImage *)scaledImageForAssetRepresentation:(ALAssetRepresentation *)representation - size:(CGSize)size - scale:(CGFloat)scale - orientation:(UIImageOrientation)orientation +// 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) { - UIImage *image = nil; - NSData *data = nil; - - uint8_t *buffer = (uint8_t *)malloc(sizeof(uint8_t)*(NSUInteger)[representation size]); - if (buffer != NULL) { - NSError *error = nil; - NSUInteger bytesRead = [representation getBytes:buffer fromOffset:0 length:(NSUInteger)[representation size] error:&error]; - data = [NSData dataWithBytes:buffer length:bytesRead]; - - free(buffer); + NSUInteger length = (NSUInteger)representation.size; + NSMutableData *data = [NSMutableData dataWithLength:length]; + if (![representation getBytes:data.mutableBytes + fromOffset:0 + length:length + error:error]) { + return nil; } - if ([data length]) { - CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil); + CGSize sourceSize = representation.dimensions; + CGRect targetRect = RCTClipRect(sourceSize, representation.scale, size, scale, resizeMode); + CGSize targetSize = targetRect.size; - NSMutableDictionary *options = [NSMutableDictionary dictionary]; + NSDictionary *options = @{ + (id)kCGImageSourceShouldAllowFloat: @YES, + (id)kCGImageSourceCreateThumbnailWithTransform: @YES, + (id)kCGImageSourceCreateThumbnailFromImageAlways: @YES, + (id)kCGImageSourceThumbnailMaxPixelSize: @(MAX(targetSize.width, targetSize.height) * scale) + }; - CGSize source = representation.dimensions; - CGFloat mW = size.width / source.width; - CGFloat mH = size.height / source.height; - - if (mH > mW) { - size.width = size.height / source.height * source.width; - } else if (mW > mH) { - size.height = size.width / source.width * source.height; - } - - CGFloat maxPixelSize = MAX(size.width, size.height) * scale; - - [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceShouldAllowFloat]; - [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailWithTransform]; - [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailFromImageAlways]; - [options setObject:(id)@(maxPixelSize) forKey:(id)kCGImageSourceThumbnailMaxPixelSize]; - - CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options); - - if (imageRef) { - image = [UIImage imageWithCGImage:imageRef scale:[representation scale] orientation:orientation]; - CGImageRelease(imageRef); - } - - if (sourceRef) { - CFRelease(sourceRef); - } + CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil); + CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options); + if (sourceRef) { + CFRelease(sourceRef); } - return image; + if (imageRef) { + UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale + orientation:(UIImageOrientation)representation.orientation]; + CGImageRelease(imageRef); + return image; + } + + return nil; } + (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag @@ -139,7 +125,7 @@ static dispatch_queue_t RCTImageLoaderQueue(void) 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, @@ -151,19 +137,19 @@ static dispatch_queue_t RCTImageLoaderQueue(void) @autoreleasepool { BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); - ALAssetOrientation orientation = ALAssetOrientationUp; ALAssetRepresentation *representation = [asset defaultRepresentation]; UIImage *image; - + NSError *error = nil; if (useMaximumSize) { - image = [UIImage imageWithCGImage:representation.fullResolutionImage scale:scale orientation:(UIImageOrientation)orientation]; - + image = [UIImage imageWithCGImage:representation.fullResolutionImage + scale:scale + orientation:(UIImageOrientation)representation.orientation]; } else { - image = [self scaledImageForAssetRepresentation:representation size:size scale:scale orientation:(UIImageOrientation)orientation]; + image = RCTScaledImageForAsset(representation, size, scale, resizeMode, &error); } - RCTDispatchCallbackOnMainQueue(completion, nil, image); + RCTDispatchCallbackOnMainQueue(completion, error, image); } }); } else { diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index 89d269532fd..7b2d88ebccd 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)}; } } From 98ab7581a560c8db4f9637bc5de14f273f599d27 Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Tue, 21 Jul 2015 09:10:52 -0700 Subject: [PATCH 24/33] Remove images from offscreen image views --- Libraries/Image/RCTImageView.m | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 5604ec4b8cf..ea7b6047eab 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -178,4 +178,23 @@ RCT_NOT_IMPLEMENTED(-init) } } +- (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 From 4f904b5d68d1176cabf51b3a476d11b61fb07bd1 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Tue, 21 Jul 2015 09:17:44 -0700 Subject: [PATCH 25/33] [ReactNative] Remove Navigator onItemRef Summary: Re-landing D2229686 after fixing bugs mentioned in D2250586 onItemRef is old and no longer needed now that the parent renders the scenes. This removes it from Navigator and all of our clients. This is a breaking change to users of Navigator, but it is easy to transition to a ref in renderScene instead --- .../CustomComponents/Navigator/Navigator.js | 62 +++++++------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index bc5d6579135..a8916ec125a 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 @@ -318,7 +331,6 @@ var Navigator = React.createClass({ onPanResponderMove: this._handlePanResponderMove, onPanResponderTerminate: this._handlePanResponderTerminate, }); - this._itemRefs = {}; this._interactionHandle = null; this._emitWillFocus(this.state.routeStack[this.state.presentedIndex]); }, @@ -1006,22 +1018,10 @@ 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), @@ -1031,38 +1031,22 @@ var Navigator = React.createClass({ }, _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 + )} ); }, From 961c1eb42904a4d5516fd7939ba14bc0625309d3 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Tue, 21 Jul 2015 12:37:24 -0700 Subject: [PATCH 26/33] [ReactNative] TextInput bug fixes and features Summary: This introduces event counts to make sure JS doesn't set out of date values on native text inputs, which can cause dropped characters and can mess with autocomplete, and obviates the need for the input buffering which added lag and complexity to the component. Made sure to test simulated super-slow JS text event processing to make sure characters aren't dropped, as well as typing obviously correctable words and making sure autocomplete works as expected. TextInput is now a controlled input by default without causing any issues for most cases, so I removed the `controlled` prop. Fixes selection state jumping by restoring it after setting new text values, so highlighting the middle of some text in the new ReWrite example and hitting space will replace that selection with an underscore and keep the cursor at a sensible position as expected, instead of jumping to the end. Ads `maxLength` prop to support the most commonly needed syncronous behavior: preventing the user from typing too many characters. It can also be used to prevent users from continuing to type after entering special characters by changing it to the current length after a regex match. Made sure to verify it works well with pasted input (including in the middle of existing text), truncating it and collapsing the selection the same way it does on the web. Fixes bug in TextEventsExample where it wouldn't show the submit and end events, even though there were firing correctly. --- Examples/UIExplorer/TextInputExample.js | 65 +++++++-- Libraries/Components/TextInput/TextInput.js | 150 +++++++------------- Libraries/Text/RCTTextField.h | 4 +- Libraries/Text/RCTTextField.m | 72 ++++++++-- Libraries/Text/RCTTextFieldManager.m | 2 + Libraries/Text/RCTTextView.h | 2 + Libraries/Text/RCTTextView.m | 46 +++++- Libraries/Text/RCTTextViewManager.m | 2 + React/Base/RCTEventDispatcher.h | 6 +- React/Base/RCTEventDispatcher.m | 5 + 10 files changed, 222 insertions(+), 132 deletions(-) diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 06cc12ee383..3369b41fb2b 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() { @@ -276,7 +323,7 @@ exports.examples = [ }, { title: 'Event handling', - render: function(): ReactElement { return }, + render: function(): ReactElement { return ; }, }, { title: 'Colored input text', diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index cc1b00b413f..d89291c3772 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,15 @@ 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. - */ - 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, /** * When the clear button should appear on the right side of the text view */ @@ -297,16 +290,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 +332,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}; }, @@ -411,7 +351,7 @@ var TextInput = React.createClass({ _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) { @@ -430,7 +370,8 @@ var TextInput = React.createClass({ onBlur={this._onBlur} onChange={this._onChange} onSelectionChangeShouldSetResponder={() => true} - text={this.state.bufferedValue} + text={this.props.value} + mostRecentEventCount={this.state.mostRecentEventCount} />; } else { for (var propKey in notMultiline) { @@ -459,14 +400,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.props.value} />; } @@ -516,7 +457,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.props.value} underlineColorAndroid={this.props.underlineColorAndroid} children={children} />; @@ -543,11 +484,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 +517,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/Text/RCTTextField.h b/Libraries/Text/RCTTextField.h index ef0a07887fa..0c8266d7ed4 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 46e9cc7a4ec..57e0499bd09 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 cc71b39fa61..723ec10f94f 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 014e3531594..c5012ec0917 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 f32debd47be..bbb9a6927af 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 570a511157b..f47a106bdf4 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/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 5576df64f50..ebd58e75e1b 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 ac0d1097b82..7638ce99d47 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 }]; } From 35aa9f3b9797681461b925af64f274a62bce3e44 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Tue, 21 Jul 2015 13:08:39 -0700 Subject: [PATCH 27/33] [ReactNative] Remove idStack from Navigator Summary: We want to transition to NavigationRouteStack, so we need to simplify and remove all uses of idStack --- Libraries/CustomComponents/Navigator/Navigator.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index a8916ec125a..f55f44e35d2 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -290,7 +290,6 @@ var Navigator = React.createClass({ sceneConfigStack: routeStack.map( (route) => this.props.configureScene(route) ), - idStack: routeStack.map(() => getuid()), routeStack, presentedIndex: initialRouteIndex, transitionFromIndex: null, @@ -357,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 @@ -882,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, }, () => { @@ -942,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); @@ -955,7 +946,6 @@ var Navigator = React.createClass({ this._emitWillFocus(route); } this.setState({ - idStack: nextIDStack, routeStack: nextRouteStack, sceneConfigStack: nextAnimationModeStack, }, () => { @@ -1024,7 +1014,6 @@ var Navigator = React.createClass({ if (newStackLength < this.state.routeStack.length) { this.setState({ sceneConfigStack: this.state.sceneConfigStack.slice(0, newStackLength), - idStack: this.state.idStack.slice(0, newStackLength), routeStack: this.state.routeStack.slice(0, newStackLength), }); } From 4e33b801ca1e4cbc906125fd5082e4b66f9798ef Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Tue, 21 Jul 2015 15:04:02 -0700 Subject: [PATCH 28/33] Allow Animated.parallel to accept empty element in the array --- Libraries/Animation/Animated/Animated.js | 10 ++++++++-- .../Animation/Animated/__tests__/Animated-test.js | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Libraries/Animation/Animated/Animated.js b/Libraries/Animation/Animated/Animated.js index 81223edcfbe..f99913f505b 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 8f9e522113b..cad752ff016 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()}; From 214c47ed76ed058a5f2fcaa332325488a3bab2d2 Mon Sep 17 00:00:00 2001 From: Felix Oghina Date: Wed, 22 Jul 2015 08:28:58 -0700 Subject: [PATCH 29/33] [reactnative] move .android.js files to oss --- Libraries/Portal/Portal.js | 139 +++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 Libraries/Portal/Portal.js diff --git a/Libraries/Portal/Portal.js b/Libraries/Portal/Portal.js new file mode 100644 index 00000000000..a029bb99f82 --- /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; From 280347d66afa7837a6c8c6e87b77049655b494a6 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Wed, 22 Jul 2015 11:46:43 -0700 Subject: [PATCH 30/33] [Navigator]: Add method `indexOf` and `slice` to NavigationRouteStack --- .../Navigation/NavigationRouteStack.js | 29 +++++-- .../__tests__/NavigationRouteStack-test.js | 83 ++++++++++++++----- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js index 91344e170ec..78fbfd1576b 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__/NavigationRouteStack-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationRouteStack-test.js index 3b750e05439..a80bf82670e 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']); From 8dd1256c25784c51c0d83453152559e3abc9b297 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Wed, 22 Jul 2015 11:50:46 -0700 Subject: [PATCH 31/33] [Navigation] Implements NavigationContextFuture --- .../Navigator/Navigation/NavigationContext.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js index 35ed24e3ad7..7f6153a33b9 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); } } From 49b55804b1e842d5154266085f8b6312758ab0f0 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Wed, 22 Jul 2015 10:54:45 -0700 Subject: [PATCH 32/33] [ReactNative] Fix crash when reload during profile (attempt #2) Summary: Fixes #1642 When reloading during profiling, the profile wouldn't unhook from the instance being deallocated. --- React/Base/RCTBatchedBridge.m | 20 ++++++++++++++++---- React/Base/RCTProfile.h | 13 +++++++++++++ React/Base/RCTProfile.m | 30 ++++++++++++++++-------------- React/Views/RCTNavigator.m | 2 +- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 7975b5e7cbb..a38476f4db5 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 */ @@ -361,6 +368,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL } moduleData.queue = nil; } + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ [_jsDisplayLink invalidate]; @@ -368,11 +376,15 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL [_javaScriptExecutor invalidate]; _javaScriptExecutor = nil; - }]; - _modules = nil; - _modulesByName = nil; - _frameUpdateObservers = nil; + if (RCTProfileIsProfiling()) { + RCTProfileUnhookModules(self); + } + _modules = nil; + _modulesByName = nil; + _frameUpdateObservers = nil; + + }]; }); } diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h index 469a8155291..66cf40bf442 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 62a17fe7b00..5174af4f153 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/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 63e3d802346..ff91b0b5474 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; } From cec5360f1bb37ef1cec626ae8ec1ff09174426e6 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 22 Jul 2015 14:03:32 -0700 Subject: [PATCH 33/33] [RN] Introduce initialValue prop to fix TextInputExamples Summary: Some of the examples relied on the fact that TextInput wasn't a controlled component before. This introduces a new `initialValue` prop which behaves the way the `value` prop used to - that is, you could type without updating it and it wouldn't get reset, thus acting as just an initial value - and switches the examples to use it where appropriate. --- Examples/UIExplorer/TextInputExample.js | 10 +++++----- Libraries/Components/TextInput/TextInput.js | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 3369b41fb2b..b2a42ffeb02 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -315,7 +315,7 @@ exports.examples = [ return ( - + ); @@ -332,11 +332,11 @@ exports.examples = [ ); @@ -383,7 +383,7 @@ exports.examples = [ @@ -391,7 +391,7 @@ exports.examples = [ diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index d89291c3772..01d6dbbfb5f 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -245,6 +245,12 @@ var TextInput = React.createClass({ * unwanted edits without flicker. */ value: PropTypes.string, + /** + * 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. + */ + defaultValue: PropTypes.string, /** * When the clear button should appear on the right side of the text view */ @@ -348,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); props.style = [styles.input, this.props.style]; - if (!props.multiline) { for (var propKey in onlyMultiline) { if (props[propKey]) { @@ -370,7 +381,7 @@ var TextInput = React.createClass({ onBlur={this._onBlur} onChange={this._onChange} onSelectionChangeShouldSetResponder={() => true} - text={this.props.value} + text={this._getText()} mostRecentEventCount={this.state.mostRecentEventCount} />; } else { @@ -407,7 +418,7 @@ var TextInput = React.createClass({ onSelectionChange={this._onSelectionChange} onTextInput={this._onTextInput} onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue} - text={this.props.value} + text={this._getText()} />; } @@ -457,7 +468,7 @@ var TextInput = React.createClass({ password={this.props.password || this.props.secureTextEntry} placeholder={this.props.placeholder} placeholderTextColor={this.props.placeholderTextColor} - text={this.props.value} + text={this._getText()} underlineColorAndroid={this.props.underlineColorAndroid} children={children} />;