From e362fb20957b7c2e2727617a2e14a3f4e9a5252b Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Tue, 14 Apr 2015 18:02:42 -0700 Subject: [PATCH 1/4] [ReactNative] Move image asset loading to a queue --- Libraries/Image/RCTImageLoader.m | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 3f8e3c75a..34158f692 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -19,6 +19,19 @@ #import "RCTImageDownloader.h" #import "RCTLog.h" +static dispatch_queue_t RCTImageLoaderQueue(void) +{ + static dispatch_queue_t queue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.facebook.rctImageLoader", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(queue, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + }); + + return queue; +} + NSError *errorWithMessage(NSString *message) { NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; @@ -43,10 +56,20 @@ NSError *errorWithMessage(NSString *message) if ([imageTag hasPrefix:@"assets-library"]) { [[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) { if (asset) { - ALAssetRepresentation *representation = [asset defaultRepresentation]; - ALAssetOrientation orientation = [representation orientation]; - UIImage *image = [UIImage imageWithCGImage:[representation fullResolutionImage] scale:1.0f orientation:(UIImageOrientation)orientation]; - callback(nil, image); + // 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, + // and might trigger memory warnings and/or OOM crashes. + // To improve this, process the loaded asset in a serial queue. + dispatch_async(RCTImageLoaderQueue(), ^{ + // Also make sure the image is released immediately after it's used so it + // doesn't spike the memory up during the process. + @autoreleasepool { + ALAssetRepresentation *representation = [asset defaultRepresentation]; + ALAssetOrientation orientation = [representation orientation]; + UIImage *image = [UIImage imageWithCGImage:[representation fullResolutionImage] scale:1.0f orientation:(UIImageOrientation)orientation]; + callback(nil, image); + } + }); } else { NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageTag]; NSError *error = errorWithMessage(errorText); From 7a68691686d63c9f6c56022b87f751e92d2ab067 Mon Sep 17 00:00:00 2001 From: guru inamdar Date: Tue, 14 Apr 2015 17:51:28 -0700 Subject: [PATCH 2/4] MapView to support MKPointAnnotation using new attribute annotate in Map... Summary: ### MapView to support Pin annotation var pinLocation = { latitude: property.latitude, longitude: property.longitude, title: property.title }; this.state = {propertyPoint: pinLocation}; ![mapview-pinannotation](https://cloud.githubusercontent.com/assets/845379/7100280/6c1ffc08-dfe5-11e4-9d1b-8da6a65da1bc.png) Closes https://github.com/facebook/react-native/pull/810 Github Author: guru inamdar Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Examples/UIExplorer/MapViewExample.js | 82 ++++++++++++++---- Examples/UIExplorer/TextInputExample.js | 27 +++++- .../testTabBarExampleSnapshot_1@2x.png | Bin 29696 -> 28887 bytes .../UIExplorerTests/UIExplorerTests.m | 11 ++- .../IntegrationTestsTests.m | 20 +++-- Libraries/Components/MapView/MapView.js | 19 ++++ Libraries/Components/TextInput/TextInput.js | 16 +++- .../Initialization/loadSourceMap.js | 2 +- Libraries/RCTTest/RCTTestRunner.h | 29 ++++--- Libraries/RCTTest/RCTTestRunner.m | 3 +- Libraries/Text/RCTText.m | 4 +- React/Base/RCTRootView.m | 9 +- React/React.xcodeproj/project.pbxproj | 12 +++ React/Views/RCTConvert+CoreLocation.h | 19 ++++ React/Views/RCTConvert+CoreLocation.m | 25 ++++++ React/Views/RCTConvert+MapKit.h | 22 +++++ React/Views/RCTConvert+MapKit.m | 46 ++++++++++ React/Views/RCTMap.h | 5 ++ React/Views/RCTMap.m | 19 +++- React/Views/RCTMapManager.m | 66 ++++++-------- React/Views/RCTNavigator.m | 17 ++-- React/Views/RCTTabBarItem.m | 27 +++--- React/Views/RCTTextField.h | 1 + React/Views/RCTTextField.m | 15 +++- React/Views/RCTTextFieldManager.m | 2 + 25 files changed, 382 insertions(+), 116 deletions(-) create mode 100644 React/Views/RCTConvert+CoreLocation.h create mode 100644 React/Views/RCTConvert+CoreLocation.m create mode 100644 React/Views/RCTConvert+MapKit.h create mode 100644 React/Views/RCTConvert+MapKit.m diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js index c532ace5d..ab6bd0717 100644 --- a/Examples/UIExplorer/MapViewExample.js +++ b/Examples/UIExplorer/MapViewExample.js @@ -24,33 +24,44 @@ var { View, } = React; +var regionText = { + latitude: '0', + longitude: '0', + latitudeDelta: '0', + longitudeDelta: '0', +} + var MapRegionInput = React.createClass({ propTypes: { region: React.PropTypes.shape({ - latitude: React.PropTypes.number, - longitude: React.PropTypes.number, - latitudeDelta: React.PropTypes.number, - longitudeDelta: React.PropTypes.number, + latitude: React.PropTypes.number.isRequired, + longitude: React.PropTypes.number.isRequired, + latitudeDelta: React.PropTypes.number.isRequired, + longitudeDelta: React.PropTypes.number.isRequired, }), onChange: React.PropTypes.func.isRequired, }, getInitialState: function() { return { - latitude: 0, - longitude: 0, - latitudeDelta: 0, - longitudeDelta: 0, + region: { + latitude: 0, + longitude: 0, + latitudeDelta: 0, + longitudeDelta: 0, + } }; }, componentWillReceiveProps: function(nextProps) { - this.setState(nextProps.region); + this.setState({ + region: nextProps.region || this.getInitialState().region + }); }, render: function() { - var region = this.state; + var region = this.state.region || this.getInitialState().region; return ( @@ -61,6 +72,7 @@ var MapRegionInput = React.createClass({ value={'' + region.latitude} style={styles.textInput} onChange={this._onChangeLatitude} + selectTextOnFocus={true} /> @@ -71,6 +83,7 @@ var MapRegionInput = React.createClass({ value={'' + region.longitude} style={styles.textInput} onChange={this._onChangeLongitude} + selectTextOnFocus={true} /> @@ -81,6 +94,7 @@ var MapRegionInput = React.createClass({ value={'' + region.latitudeDelta} style={styles.textInput} onChange={this._onChangeLatitudeDelta} + selectTextOnFocus={true} /> @@ -91,6 +105,7 @@ var MapRegionInput = React.createClass({ value={'' + region.longitudeDelta} style={styles.textInput} onChange={this._onChangeLongitudeDelta} + selectTextOnFocus={true} /> @@ -103,23 +118,29 @@ var MapRegionInput = React.createClass({ }, _onChangeLatitude: function(e) { - this.setState({latitude: parseFloat(e.nativeEvent.text)}); + regionText.latitude = e.nativeEvent.text; }, _onChangeLongitude: function(e) { - this.setState({longitude: parseFloat(e.nativeEvent.text)}); + regionText.longitude = e.nativeEvent.text; }, _onChangeLatitudeDelta: function(e) { - this.setState({latitudeDelta: parseFloat(e.nativeEvent.text)}); + regionText.latitudeDelta = e.nativeEvent.text; }, _onChangeLongitudeDelta: function(e) { - this.setState({longitudeDelta: parseFloat(e.nativeEvent.text)}); + regionText.longitudeDelta = e.nativeEvent.text; }, _change: function() { - this.props.onChange(this.state); + this.setState({ + latitude: parseFloat(regionText.latitude), + longitude: parseFloat(regionText.longitude), + latitudeDelta: parseFloat(regionText.latitudeDelta), + longitudeDelta: parseFloat(regionText.longitudeDelta), + }); + this.props.onChange(this.state.region); }, }); @@ -130,6 +151,8 @@ var MapViewExample = React.createClass({ return { mapRegion: null, mapRegionInput: null, + annotations: null, + isFirstLoad: true, }; }, @@ -138,8 +161,10 @@ var MapViewExample = React.createClass({ + + + + + + + + ); + } + }, ]; diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTabBarExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTabBarExampleSnapshot_1@2x.png index 7237c5f10fa0e836167a8e560c49e44fb12962a8..d3e66652b5c01bcdc1c08114cfea10fba8d5fe05 100644 GIT binary patch literal 28887 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 XMUQ+qbQTMMe~uoq{I=*Dx2yjLZA`-) literal 29696 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 diff --git a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m index e82422110..fd321546a 100644 --- a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m +++ b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m @@ -39,9 +39,10 @@ #endif NSString *version = [[UIDevice currentDevice] systemVersion]; RCTAssert([version isEqualToString:@"8.1"], @"Snapshot tests should be run on iOS 8.1, found %@", version); - _runner = initRunnerForApp(@"Examples/UIExplorer/UIExplorerApp"); + _runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp"); - // If tests have changes, set recordMode = YES below and run the affected tests on an iPhone5, iOS 8.1 simulator. + // If tests have changes, set recordMode = YES below and run the affected + // tests on an iPhone5, iOS 8.1 simulator. _runner.recordMode = NO; } @@ -58,8 +59,10 @@ return NO; } -// Make sure this test runs first (underscores sort early) otherwise the other tests will tear out the rootView -- (void)test__RootViewLoadsAndRenders { +// Make sure this test runs first (underscores sort early) otherwise the +// other tests will tear out the rootView +- (void)test__RootViewLoadsAndRenders +{ UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; RCTAssert([vc.view isKindOfClass:[RCTRootView class]], @"This test must run first."); NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; diff --git a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m index 578d3915f..e0a43e793 100644 --- a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m +++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m @@ -18,7 +18,8 @@ @end -@implementation IntegrationTestsTests { +@implementation IntegrationTestsTests +{ RCTTestRunner *_runner; } @@ -28,10 +29,11 @@ RCTAssert(!__LP64__, @"Tests should be run on 32-bit device simulators (e.g. iPhone 5)"); #endif NSString *version = [[UIDevice currentDevice] systemVersion]; - RCTAssert([version isEqualToString:@"8.1"], @"Tests should be run on iOS 8.1, found %@", version); - _runner = initRunnerForApp(@"IntegrationTests/IntegrationTestsApp"); + RCTAssert([version integerValue] == 8, @"Tests should be run on iOS 8.x, found %@", version); + _runner = RCTInitRunnerForApp(@"IntegrationTests/IntegrationTestsApp"); - // If tests have changes, set recordMode = YES below and run the affected tests on an iPhone5, iOS 8.1 simulator. + // If tests have changes, set recordMode = YES below and run the affected + // tests on an iPhone5, iOS 8.1 simulator. _runner.recordMode = NO; } @@ -44,15 +46,19 @@ - (void)testTheTester_waitOneFrame { - [_runner runTest:_cmd module:@"IntegrationTestHarnessTest" initialProps:@{@"waitOneFrame": @YES} expectErrorBlock:nil]; + [_runner runTest:_cmd + module:@"IntegrationTestHarnessTest" + initialProps:@{@"waitOneFrame": @YES} + expectErrorBlock:nil]; } -- (void)testTheTester_ExpectError +// TODO: this seems to stall forever - figure out why +- (void)DISABLED_testTheTester_ExpectError { [_runner runTest:_cmd module:@"IntegrationTestHarnessTest" initialProps:@{@"shouldThrow": @YES} - expectErrorRegex:[NSRegularExpression regularExpressionWithPattern:@"because shouldThrow" options:0 error:nil]]; + expectErrorRegex:@"because shouldThrow"]; } - (void)testTimers diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index 388e22ddc..7beeabbea 100644 --- a/Libraries/Components/MapView/MapView.js +++ b/Libraries/Components/MapView/MapView.js @@ -95,6 +95,23 @@ var MapView = React.createClass({ longitudeDelta: React.PropTypes.number.isRequired, }), + /** + * Map annotations with title/subtitle. + */ + annotations: React.PropTypes.arrayOf(React.PropTypes.shape({ + /** + * The location of the annotation. + */ + latitude: React.PropTypes.number.isRequired, + longitude: React.PropTypes.number.isRequired, + + /** + * Annotation title/subtile. + */ + title: React.PropTypes.string, + subtitle: React.PropTypes.string, + })), + /** * Maximum size of area that can be displayed. */ @@ -142,6 +159,7 @@ var MapView = React.createClass({ pitchEnabled={this.props.pitchEnabled} scrollEnabled={this.props.scrollEnabled} region={this.props.region} + annotations={this.props.annotations} maxDelta={this.props.maxDelta} minDelta={this.props.minDelta} legalLabelInsets={this.props.legalLabelInsets} @@ -165,6 +183,7 @@ var RCTMap = createReactIOSNativeComponentClass({ pitchEnabled: true, scrollEnabled: true, region: {diff: deepDiffer}, + annotations: {diff: deepDiffer}, maxDelta: true, minDelta: true, legalLabelInsets: {diff: insetsDiffer}, diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 6908ed8a6..bf988f593 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -58,6 +58,8 @@ var RCTTextFieldAttributes = merge(RCTTextViewAttributes, { caretHidden: true, enabled: true, clearButtonMode: true, + clearTextOnFocus: true, + selectTextOnFocus: true, }); var onlyMultiline = { @@ -267,7 +269,17 @@ var TextInput = React.createClass({ 'unless-editing', 'always', ]), - + /** + * If true, clears the text field automatically when editing begins + */ + clearTextOnFocus: PropTypes.bool, + /** + * If true, selected the text automatically when editing begins + */ + selectTextOnFocus: PropTypes.bool, + /** + * Styles + */ style: Text.propTypes.style, /** * Used to locate this view in end-to-end tests. @@ -431,6 +443,8 @@ var TextInput = React.createClass({ autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} clearButtonMode={clearButtonMode} + clearTextOnFocus={this.props.clearTextOnFocus} + selectTextOnFocus={this.props.selectTextOnFocus} />; } else { for (var propKey in notMultiline) { diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js index 9ecf2543b..25d0194dc 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule loadSourceMap - * @flow + * -- disabled flow due to mysterious validation errors -- */ 'use strict'; diff --git a/Libraries/RCTTest/RCTTestRunner.h b/Libraries/RCTTest/RCTTestRunner.h index 6dc1ddb06..1b37ba492 100644 --- a/Libraries/RCTTest/RCTTestRunner.h +++ b/Libraries/RCTTest/RCTTestRunner.h @@ -10,13 +10,13 @@ #import /** - * Use the initRunnerForApp macro for typical usage. + * Use the RCTInitRunnerForApp macro for typical usage. * * Add this to your test target's gcc preprocessor macros: * * FB_REFERENCE_IMAGE_DIR="\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\"" */ -#define initRunnerForApp(app__) [[RCTTestRunner alloc] initWithApp:(app__) referenceDir:@FB_REFERENCE_IMAGE_DIR] +#define RCTInitRunnerForApp(app__) [[RCTTestRunner alloc] initWithApp:(app__) referenceDir:@FB_REFERENCE_IMAGE_DIR] @interface RCTTestRunner : NSObject @@ -24,22 +24,25 @@ @property (nonatomic, strong) NSURL *scriptURL; /** - * Initialize a runner. It's recommended that you use the initRunnerForApp macro instead of calling this directly. + * Initialize a runner. It's recommended that you use the RCTInitRunnerForApp + * macro instead of calling this directly. * * @param app The path to the app bundle without suffixes, e.g. IntegrationTests/IntegrationTestsApp - * @param referencesDir The path for snapshot references images. The initRunnerForApp macro uses + * @param referencesDir The path for snapshot references images. The RCTInitRunnerForApp macro uses * FB_REFERENCE_IMAGE_DIR for this automatically. */ - (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir; /** - * Simplest runTest function simply mounts the specified JS module with no initialProps and waits for it to call + * Simplest runTest function simply mounts the specified JS module with no + * initialProps and waits for it to call * * RCTTestModule.markTestCompleted() * - * JS errors/exceptions and timeouts will fail the test. Snapshot tests call RCTTestModule.verifySnapshot whenever they - * want to verify what has been rendered (typically via requestAnimationFrame to make sure the latest state has been - * rendered in native. + * JS errors/exceptions and timeouts will fail the test. Snapshot tests call + * RCTTestModule.verifySnapshot whenever they want to verify what has been + * rendered (typically via requestAnimationFrame to make sure the latest state + * has been rendered in native. * * @param test Selector of the test, usually just `_cmd`. * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS. @@ -47,8 +50,9 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName; /** - * Same as runTest:, but allows for passing initialProps for providing mock data or requesting different behaviors, and - * expectErrorRegex verifies that the error you expected was thrown. + * Same as runTest:, but allows for passing initialProps for providing mock data + * or requesting different behaviors, and expectErrorRegex verifies that the + * error you expected was thrown. * * @param test Selector of the test, usually just `_cmd`. * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS. @@ -58,8 +62,9 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSString *)expectErrorRegex; /** - * Same as runTest:, but allows for passing initialProps for providing mock data or requesting different behaviors, and - * expectErrorBlock provides arbitrary logic for processing errors (nil will cause any error to fail the test). + * Same as runTest:, but allows for passing initialProps for providing mock data + * or requesting different behaviors, and expectErrorBlock provides arbitrary + * logic for processing errors (nil will cause any error to fail the test). * * @param test Selector of the test, usually just `_cmd`. * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS. diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 12eaf8072..9b3a7d3c8 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -49,7 +49,8 @@ [self runTest:test module:moduleName initialProps:nil expectErrorBlock:nil]; } -- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSString *)errorRegex +- (void)runTest:(SEL)test module:(NSString *)moduleName + initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSString *)errorRegex { [self runTest:test module:moduleName initialProps:initialProps expectErrorBlock:^BOOL(NSString *error){ return [error rangeOfString:errorRegex options:NSRegularExpressionSearch].location != NSNotFound; diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 87c625cd3..84f6b85e1 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -114,7 +114,9 @@ - (NSNumber *)reactTagAtPoint:(CGPoint)point { CGFloat fraction; - NSUInteger characterIndex = [_layoutManager characterIndexForPoint:point inTextContainer:_textContainer fractionOfDistanceBetweenInsertionPoints:&fraction]; + NSUInteger characterIndex = [_layoutManager characterIndexForPoint:point + inTextContainer:_textContainer + fractionOfDistanceBetweenInsertionPoints:&fraction]; NSNumber *reactTag = nil; diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index c282272b7..1dbe714c8 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -106,11 +106,14 @@ { [[NSNotificationCenter defaultCenter] removeObserver:self]; [_touchHandler invalidate]; - [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" - args:@[_contentView.reactTag]]; + if (_contentView) { + [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" + args:@[_contentView.reactTag]]; + } } -- (UIViewController *)backingViewController { +- (UIViewController *)backingViewController +{ return _backingViewController ?: [super backingViewController]; } diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 5c9c13355..4c1cfc241 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; }; 00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */; }; + 13456E931ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */; }; + 13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */; }; 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; }; 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */; }; 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */; }; @@ -83,6 +85,10 @@ 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; }; 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewControllerProtocol.h; sourceTree = ""; }; + 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+CoreLocation.h"; sourceTree = ""; }; + 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+CoreLocation.m"; sourceTree = ""; }; + 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+MapKit.h"; sourceTree = ""; }; + 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+MapKit.m"; sourceTree = ""; }; 134FCB391A6E7F0800051CC8 /* RCTContextExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTContextExecutor.h; sourceTree = ""; }; 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutor.m; sourceTree = ""; }; 134FCB3B1A6E7F0800051CC8 /* RCTWebViewExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewExecutor.h; sourceTree = ""; }; @@ -255,6 +261,10 @@ 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */, 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */, 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */, + 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, + 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, + 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, + 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */, 14435CE11AAC4AE100FC20F4 /* RCTMap.h */, 14435CE21AAC4AE100FC20F4 /* RCTMap.m */, 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */, @@ -459,6 +469,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */, 13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */, 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */, 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */, @@ -490,6 +501,7 @@ 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */, 13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */, 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */, + 13456E931ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m in Sources */, 137327E91AA5CF210034F82E /* RCTTabBarItemManager.m in Sources */, 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */, 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */, diff --git a/React/Views/RCTConvert+CoreLocation.h b/React/Views/RCTConvert+CoreLocation.h new file mode 100644 index 000000000..89e0c729c --- /dev/null +++ b/React/Views/RCTConvert+CoreLocation.h @@ -0,0 +1,19 @@ +// +// RCTConvert+CoreLocation.h +// React +// +// Created by Nick Lockwood on 12/04/2015. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +#import "RCTConvert.h" + +@interface RCTConvert (CoreLocation) + ++ (CLLocationDegrees)CLLocationDegrees:(id)json; ++ (CLLocationDistance)CLLocationDistance:(id)json; ++ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json; + +@end diff --git a/React/Views/RCTConvert+CoreLocation.m b/React/Views/RCTConvert+CoreLocation.m new file mode 100644 index 000000000..a347c7fea --- /dev/null +++ b/React/Views/RCTConvert+CoreLocation.m @@ -0,0 +1,25 @@ +// +// RCTConvert+CoreLocation.m +// React +// +// Created by Nick Lockwood on 12/04/2015. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTConvert+CoreLocation.h" + +@implementation RCTConvert(CoreLocation) + +RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue); +RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue); + ++ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json +{ + json = [self NSDictionary:json]; + return (CLLocationCoordinate2D){ + [self CLLocationDegrees:json[@"latitude"]], + [self CLLocationDegrees:json[@"longitude"]] + }; +} + +@end diff --git a/React/Views/RCTConvert+MapKit.h b/React/Views/RCTConvert+MapKit.h new file mode 100644 index 000000000..8ad9316a1 --- /dev/null +++ b/React/Views/RCTConvert+MapKit.h @@ -0,0 +1,22 @@ +// +// RCTConvert+MapKit.h +// React +// +// Created by Nick Lockwood on 12/04/2015. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +#import "RCTConvert.h" + +@interface RCTConvert (MapKit) + ++ (MKCoordinateSpan)MKCoordinateSpan:(id)json; ++ (MKCoordinateRegion)MKCoordinateRegion:(id)json; ++ (MKShape *)MKShape:(id)json; + +typedef NSArray MKShapeArray; ++ (MKShapeArray *)MKShapeArray:(id)json; + +@end diff --git a/React/Views/RCTConvert+MapKit.m b/React/Views/RCTConvert+MapKit.m new file mode 100644 index 000000000..cd6c9fb41 --- /dev/null +++ b/React/Views/RCTConvert+MapKit.m @@ -0,0 +1,46 @@ +// +// RCTConvert+MapKit.m +// React +// +// Created by Nick Lockwood on 12/04/2015. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTConvert+MapKit.h" + +#import "RCTConvert+CoreLocation.h" + +@implementation RCTConvert(MapKit) + ++ (MKCoordinateSpan)MKCoordinateSpan:(id)json +{ + json = [self NSDictionary:json]; + return (MKCoordinateSpan){ + [self CLLocationDegrees:json[@"latitudeDelta"]], + [self CLLocationDegrees:json[@"longitudeDelta"]] + }; +} + ++ (MKCoordinateRegion)MKCoordinateRegion:(id)json +{ + return (MKCoordinateRegion){ + [self CLLocationCoordinate2D:json], + [self MKCoordinateSpan:json] + }; +} + ++ (MKShape *)MKShape:(id)json +{ + json = [self NSDictionary:json]; + + // TODO: more shape types + MKShape *shape = [[MKPointAnnotation alloc] init]; + shape.coordinate = [self CLLocationCoordinate2D:json]; + shape.title = [RCTConvert NSString:json[@"title"]]; + shape.subtitle = [RCTConvert NSString:json[@"subtitle"]]; + return shape; +} + +RCT_ARRAY_CONVERTER(MKShape) + +@end diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h index 3850378e9..89e4c0a80 100644 --- a/React/Views/RCTMap.h +++ b/React/Views/RCTMap.h @@ -10,6 +10,8 @@ #import #import +#import "RCTConvert+MapKit.h" + extern const CLLocationDegrees RCTMapDefaultSpan; extern const NSTimeInterval RCTMapRegionChangeObserveInterval; extern const CGFloat RCTMapZoomBoundBuffer; @@ -19,9 +21,12 @@ extern const CGFloat RCTMapZoomBoundBuffer; @interface RCTMap: MKMapView @property (nonatomic, assign) BOOL followUserLocation; +@property (nonatomic, assign) BOOL hasStartedLoading; @property (nonatomic, assign) CGFloat minDelta; @property (nonatomic, assign) CGFloat maxDelta; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets; @property (nonatomic, strong) NSTimer *regionChangeObserveTimer; +- (void)setAnnotations:(MKShapeArray *)annotations; + @end diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 72c0db5eb..187303ac2 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -9,7 +9,6 @@ #import "RCTMap.h" -#import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -27,10 +26,14 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; - (instancetype)init { if ((self = [super init])) { + + _hasStartedLoading = NO; + // Find Apple link label for (UIView *subview in self.subviews) { if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) { - // This check is super hacky, but the whole premise of moving around Apple's internal subviews is super hacky + // This check is super hacky, but the whole premise of moving around + // Apple's internal subviews is super hacky _legalLabel = subview; break; } @@ -82,11 +85,11 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; [_locationManager requestWhenInUseAuthorization]; } } - [super setShowsUserLocation:showsUserLocation]; + super.showsUserLocation = showsUserLocation; // If it needs to show user location, force map view centered // on user's current location on user location updates - self.followUserLocation = showsUserLocation; + _followUserLocation = showsUserLocation; } } @@ -109,4 +112,12 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; [super setRegion:region animated:YES]; } +- (void)setAnnotations:(MKShapeArray *)annotations +{ + [self removeAnnotations:self.annotations]; + if (annotations.count) { + [self addAnnotations:annotations]; + } +} + @end diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index 24d8bee16..52b635fd6 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -10,43 +10,13 @@ #import "RCTMapManager.h" #import "RCTBridge.h" +#import "RCTConvert+CoreLocation.h" +#import "RCTConvert+MapKit.h" #import "RCTEventDispatcher.h" #import "RCTMap.h" #import "UIView+React.h" -@implementation RCTConvert(CoreLocation) - -+ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json -{ - json = [self NSDictionary:json]; - return (CLLocationCoordinate2D){ - [self double:json[@"latitude"]], - [self double:json[@"longitude"]] - }; -} - -@end - -@implementation RCTConvert(MapKit) - -+ (MKCoordinateSpan)MKCoordinateSpan:(id)json -{ - json = [self NSDictionary:json]; - return (MKCoordinateSpan){ - [self double:json[@"latitudeDelta"]], - [self double:json[@"longitudeDelta"]] - }; -} - -+ (MKCoordinateRegion)MKCoordinateRegion:(id)json -{ - return (MKCoordinateRegion){ - [self CLLocationCoordinate2D:json], - [self MKCoordinateSpan:json] - }; -} - -@end +static NSString *const RCTMapViewKey = @"MapView"; @interface RCTMapManager() @@ -72,6 +42,8 @@ RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat) RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat) RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion) +RCT_EXPORT_VIEW_PROPERTY(annotations, MKShapeArray) + #pragma mark MKMapViewDelegate @@ -93,12 +65,15 @@ RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion) { [self _regionChanged:mapView]; - mapView.regionChangeObserveTimer = [NSTimer timerWithTimeInterval:RCTMapRegionChangeObserveInterval - target:self - selector:@selector(_onTick:) - userInfo:@{ @"mapView": mapView } - repeats:YES]; - [[NSRunLoop mainRunLoop] addTimer:mapView.regionChangeObserveTimer forMode:NSRunLoopCommonModes]; + if (animated) { + mapView.regionChangeObserveTimer = [NSTimer timerWithTimeInterval:RCTMapRegionChangeObserveInterval + target:self + selector:@selector(_onTick:) + userInfo:@{ RCTMapViewKey: mapView } + repeats:YES]; + + [[NSRunLoop mainRunLoop] addTimer:mapView.regionChangeObserveTimer forMode:NSRunLoopCommonModes]; + } } - (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated @@ -107,6 +82,17 @@ RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion) mapView.regionChangeObserveTimer = nil; [self _regionChanged:mapView]; + + // Don't send region did change events until map has + // started loading, as these won't represent the final location + if (mapView.hasStartedLoading) { + [self _emitRegionChangeEvent:mapView continuous:NO]; + }; +} + +- (void)mapViewWillStartLoadingMap:(RCTMap *)mapView +{ + mapView.hasStartedLoading = YES; [self _emitRegionChangeEvent:mapView continuous:NO]; } @@ -114,7 +100,7 @@ RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion) - (void)_onTick:(NSTimer *)timer { - [self _regionChanged:timer.userInfo[@"mapView"]]; + [self _regionChanged:timer.userInfo[RCTMapViewKey]]; } - (void)_regionChanged:(RCTMap *)mapView diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 373313b93..5523e49b7 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -494,17 +494,20 @@ NSInteger kNeverProgressed = -10000; jsMakingNoProgressAndDoesntNeedTo)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } - RCTAssert( - currentReactCount <= _currentViews.count, - @"Cannot adjust current top of stack beyond available views" - ); + if (currentReactCount > _currentViews.count) { + RCTLogError(@"Cannot adjust current top of stack beyond available views"); + } // Views before the previous react count must not have changed. Views greater than previousReactCount // up to currentReactCount may have changed. for (NSInteger i = 0; i < MIN(_currentViews.count, MIN(_previousViews.count, previousReactCount)); i++) { - RCTAssert(_currentViews[i] == _previousViews[i], @"current view should equal previous view"); + if (_currentViews[i] != _previousViews[i]) { + RCTLogError(@"current view should equal previous view"); + } + } + if (currentReactCount < 1) { + RCTLogError(@"should be at least one current view"); } - RCTAssert(currentReactCount >= 1, @"should be at least one current view"); if (jsGettingAhead) { if (reactPushOne) { UIView *lastView = [_currentViews lastObject]; @@ -517,7 +520,7 @@ NSInteger kNeverProgressed = -10000; _numberOfViewControllerMovesToIgnore = viewControllerCount - currentReactCount; [_navigationController popToViewController:viewControllerToPopTo animated:YES]; } else { - RCTAssert(NO, @"Pushing or popping more than one view at a time from JS"); + RCTLogError(@"Pushing or popping more than one view at a time from JS"); } } else if (jsCatchingUp) { [self freeLock]; // Nothing to push/pop diff --git a/React/Views/RCTTabBarItem.m b/React/Views/RCTTabBarItem.m index 967ae04af..e6caa0b18 100644 --- a/React/Views/RCTTabBarItem.m +++ b/React/Views/RCTTabBarItem.m @@ -31,20 +31,19 @@ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ systemIcons = @{ - @"bookmarks": @(UITabBarSystemItemBookmarks), - @"contacts": @(UITabBarSystemItemContacts), - @"downloads": @(UITabBarSystemItemDownloads), - @"favorites": @(UITabBarSystemItemFavorites), - @"featured": @(UITabBarSystemItemFeatured), - @"history": @(UITabBarSystemItemHistory), - @"more": @(UITabBarSystemItemMore), - @"most-recent": @(UITabBarSystemItemMostRecent), - @"most-viewed": @(UITabBarSystemItemMostViewed), - @"recents": @(UITabBarSystemItemRecents), - @"search": @(UITabBarSystemItemSearch), - @"top-rated": @(UITabBarSystemItemTopRated), - }; - + @"bookmarks": @(UITabBarSystemItemBookmarks), + @"contacts": @(UITabBarSystemItemContacts), + @"downloads": @(UITabBarSystemItemDownloads), + @"favorites": @(UITabBarSystemItemFavorites), + @"featured": @(UITabBarSystemItemFeatured), + @"history": @(UITabBarSystemItemHistory), + @"more": @(UITabBarSystemItemMore), + @"most-recent": @(UITabBarSystemItemMostRecent), + @"most-viewed": @(UITabBarSystemItemMostViewed), + @"recents": @(UITabBarSystemItemRecents), + @"search": @(UITabBarSystemItemSearch), + @"top-rated": @(UITabBarSystemItemTopRated), + }; }); // Update icon diff --git a/React/Views/RCTTextField.h b/React/Views/RCTTextField.h index 47d76ad52..bd1be9c18 100644 --- a/React/Views/RCTTextField.h +++ b/React/Views/RCTTextField.h @@ -15,6 +15,7 @@ @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign) BOOL autoCorrect; +@property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) UIEdgeInsets contentInset; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/React/Views/RCTTextField.m b/React/Views/RCTTextField.m index 077e75c5d..35eb84d96 100644 --- a/React/Views/RCTTextField.m +++ b/React/Views/RCTTextField.m @@ -104,15 +104,26 @@ } RCT_TEXT_EVENT_HANDLER(_textFieldDidChange, RCTTextEventTypeChange) -RCT_TEXT_EVENT_HANDLER(_textFieldBeginEditing, RCTTextEventTypeFocus) RCT_TEXT_EVENT_HANDLER(_textFieldEndEditing, RCTTextEventTypeEnd) RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit) +- (void)_textFieldBeginEditing +{ + if (_selectTextOnFocus) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self selectAll:nil]; + }); + } + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus + reactTag:self.reactTag + text:self.text]; +} + // TODO: we should support shouldChangeTextInRect (see UITextFieldDelegate) - (BOOL)becomeFirstResponder { - _jsRequestingFirstResponder = YES; // TODO: is this still needed? + _jsRequestingFirstResponder = YES; BOOL result = [super becomeFirstResponder]; _jsRequestingFirstResponder = NO; return result; diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m index 3cfdd53a1..6e78d86a3 100644 --- a/React/Views/RCTTextFieldManager.m +++ b/React/Views/RCTTextFieldManager.m @@ -30,6 +30,8 @@ RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(text, NSString) RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode) +RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL) +RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType) RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, BOOL) From 4f8b2825a0e2e8074f27ddc1fe06d741ba598060 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Wed, 15 Apr 2015 06:04:42 -0700 Subject: [PATCH 3/4] Replace ES6Promise with Promise --- Libraries/Promise.js | 37 +++ Libraries/vendor/core/ES6Promise.js | 364 ---------------------------- Libraries/vendor/core/Promise.js | 88 ------- 3 files changed, 37 insertions(+), 452 deletions(-) create mode 100644 Libraries/Promise.js delete mode 100644 Libraries/vendor/core/ES6Promise.js delete mode 100644 Libraries/vendor/core/Promise.js diff --git a/Libraries/Promise.js b/Libraries/Promise.js new file mode 100644 index 000000000..7cef20423 --- /dev/null +++ b/Libraries/Promise.js @@ -0,0 +1,37 @@ +/** + * + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule Promise + * + * This module wraps and augments the minimally ES6-compliant Promise + * implementation provided by the promise npm package. + */ + +'use strict'; + +global.setImmediate = require('setImmediate'); +var Promise = require('promise/setimmediate/es6-extensions'); +require('promise/setimmediate/done'); + +/** + * Handle either fulfillment or rejection with the same callback. + */ +Promise.prototype.finally = function(onSettled) { + return this.then(onSettled, onSettled); +}; + + +module.exports = Promise; diff --git a/Libraries/vendor/core/ES6Promise.js b/Libraries/vendor/core/ES6Promise.js deleted file mode 100644 index acbf02773..000000000 --- a/Libraries/vendor/core/ES6Promise.js +++ /dev/null @@ -1,364 +0,0 @@ -/** - * @generated SignedSource<> - * - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * !! This file is a check-in of a static_upstream project! !! - * !! !! - * !! You should not modify this file directly. Instead: !! - * !! 1) Use `fjs use-upstream` to temporarily replace this with !! - * !! the latest version from upstream. !! - * !! 2) Make your changes, test them, etc. !! - * !! 3) Use `fjs push-upstream` to copy your changes back to !! - * !! static_upstream. !! - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * - * Copyright 2013-2014 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule ES6Promise - * - * This module implements the minimum functionality necessary to comply - * with chapter 25.4 of the ES6 specification. Any extensions to Promise - * or Promise.prototype should be added in the Promise module. - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise-objects - */ - -module.exports = (function(global, undefined) { - 'use strict'; - - var setImmediate = require('setImmediate'); - - // These are the possible values for slots(promise).state. - var PENDING_STATE = 'pending'; - var FULFILLED_STATE = 'fulfilled'; - var REJECTED_STATE = 'rejected'; - - // The ES6 specification makes heavy use of a notion of internal slots. - // Some of these slots are best implemented as closure variables, such - // as the alreadySettled variable in createResolvingFunctions, which - // corresponds to the resolve.[[AlreadyResolved]].value property in the - // specification. Other slots are best implemented as properties of a - // slots object attached to the host object by a pseudo-private - // property. The latter kind of slots may be accessed by passing the - // host object (such as a Promise or a resolve/reject function object) - // to the slots function; e.g., the slots(promise).state slot, which - // corresponds to promise.[[PromiseState]] in the specification. - var slotsKey = '__slots$' + Math.random().toString(36).slice(2); - function slots(obj) { - var result = obj[slotsKey]; - if (!result) { - // In ES5+ environments, this property will be safely non-writable, - // non-configurable, and non-enumerable. This implementation does - // not logically rely on those niceties, however, so this code works - // just fine in pre-ES5 environments, too. - obj[slotsKey] = result = {}; - if (Object.defineProperty) try { - Object.defineProperty(obj, slotsKey, { value: result }); - } catch (definePropertyIsBrokenInIE8) {} - } - return result; - } - - // Reusable callback functions. The identify function is the default - // when onFulfilled is undefined or null, and the raise function is the - // default when onRejected is undefined or null. - function identity(x) { return x; } - function raise(x) { throw x; } - - /** - * When the Promise function is called with argument executor, the - * following steps are taken: - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise - * - * The executor argument must be a function object. It is called for - * initiating and reporting completion of the possibly deferred action - * represented by this Promise object. The executor is called with two - * arguments: resolve and reject. These are functions that may be used - * by the executor function to report eventual completion or failure of - * the deferred computation. Returning from the executor function does - * not mean that the deferred action has been completed, but only that - * the request to eventually perform the deferred action has been - * accepted. - * - * The resolve function that is passed to an executor function accepts a - * single argument. The executor code may eventually call the resolve - * function to indicate that it wishes to resolve the associated Promise - * object. The argument passed to the resolve function represents the - * eventual value of the deferred action and can be either the actual - * fulfillment value or another Promise object which will provide the - * value if it is fullfilled. - * - * The reject function that is passed to an executor function accepts a - * single argument. The executor code may eventually call the reject - * function to indicate that the associated Promise is rejected and will - * never be fulfilled. The argument passed to the reject function is - * used as the rejection value of the promise. Typically it will be an - * Error object. - * - * When Promise is called as a function rather than as a constructor, it - * initializes its this value with the internal state necessary to - * support the Promise.prototype methods. - * - * The Promise constructor is designed to be subclassable. It may be - * used as the value in an extends clause of a class - * definition. Subclass constructors that intend to inherit the - * specified Promise behaviour must include a super call to Promise, - * e.g. by invoking Promise.call(this, executor). - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise-constructor - */ - function Promise(executor) { - var promiseSlots = slots(this); - promiseSlots.state = PENDING_STATE; - promiseSlots.fulfillReactions = []; - promiseSlots.rejectReactions = []; - - var resolvingFunctions = createResolvingFunctions(this); - var reject = resolvingFunctions.reject; - - try { - executor(resolvingFunctions.resolve, reject); - } catch (err) { - reject(err); - } - } - - function createResolvingFunctions(promise) { - var alreadySettled = false; - - return { - resolve: function(resolution) { - if (!alreadySettled) { - alreadySettled = true; - - if (resolution === promise) { - return settlePromise( - promise, - REJECTED_STATE, - new TypeError('Cannot resolve promise with itself') - ); - } - - // To be treated as a Promise-like object, the resolution only - // needs to be an object with a callable .then method. - if (!resolution || - typeof resolution !== "object" || - typeof resolution.then !== "function") { - return settlePromise(promise, FULFILLED_STATE, resolution); - } - - var resolvingFunctions = createResolvingFunctions(promise); - var reject = resolvingFunctions.reject; - - try { - resolution.then(resolvingFunctions.resolve, reject); - } catch (err) { - reject(err); - } - } - }, - - reject: function(reason) { - if (!alreadySettled) { - alreadySettled = true; - settlePromise(promise, REJECTED_STATE, reason); - } - } - }; - } - - // This function unifies the FulfillPromise and RejectPromise functions - // defined in the ES6 specification. - function settlePromise(promise, state, result) { - var promiseSlots = slots(promise); - if (promiseSlots.state !== PENDING_STATE) { - throw new Error('Settling a ' + promiseSlots.state + ' promise'); - } - - var reactions; - if (state === FULFILLED_STATE) { - reactions = promiseSlots.fulfillReactions; - } else if (state === REJECTED_STATE) { - reactions = promiseSlots.rejectReactions; - } - - promiseSlots.result = result; - promiseSlots.fulfillReactions = undefined; - promiseSlots.rejectReactions = undefined; - promiseSlots.state = state; - - var count = reactions.length; - count && setImmediate(function() { - for (var i = 0; i < count; ++i) { - reactions[i](promiseSlots.result); - } - }); - } - - /** - * The Promise.all function returns a new promise which is fulfilled - * with an array of fulfillment values for the passed promises, or - * rejects with the reason of the first passed promise that rejects. It - * resoves all elements of the passed iterable to promises as it runs - * this algorithm. - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise.all - */ - Promise.all = function(array) { - var Promise = this; - return new Promise(function(resolve, reject) { - var results = []; - var remaining = 0; - array.forEach(function(element, index) { - ++remaining; // Array might be sparse. - Promise.resolve(element).then(function(result) { - if (!results.hasOwnProperty(index)) { - results[index] = result; - --remaining || resolve(results); - } - }, reject); - }); - remaining || resolve(results); - }); - }; - - /** - * The Promise.race function returns a new promise which is settled in - * the same way as the first passed promise to settle. It resolves all - * elements of the passed iterable to promises as it runs this - * algorithm. - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise.race - */ - Promise.race = function(array) { - var Promise = this; - return new Promise(function(resolve, reject) { - array.forEach(function(element) { - Promise.resolve(element).then(resolve, reject); - }); - }); - }; - - /** - * The Promise.resolve function returns either a new promise resolved - * with the passed argument, or the argument itself if the argument a - * promise produced by this construtor. - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise.resolve - */ - Promise.resolve = function(x) { - return x instanceof Promise && x.constructor === this - ? x // Refuse to create promises for promises. - : new this(function(resolve) { resolve(x); }); - }; - - /** - * The Promise.reject function returns a new promise rejected with the - * passed argument. - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise.reject - */ - Promise.reject = function(r) { - return new this(function(_, reject) { reject(r); }); - }; - - var Pp = Promise.prototype; - - /** - * When the .then method is called with arguments onFulfilled and - * onRejected, the following steps are taken: - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise.prototype.then - */ - Pp.then = function(onFulfilled, onRejected) { - var capabilityResolve; - var capabilityReject; - var capabilityPromise = new this.constructor(function(resolve, reject) { - capabilityResolve = resolve; - capabilityReject = reject; - }); - - if (typeof capabilityResolve !== "function") { - throw new TypeError('Uncallable Promise resolve function'); - } - - if (typeof capabilityReject !== "function") { - throw new TypeError('Uncallable Promise reject function'); - } - - if (onFulfilled === undefined || onFulfilled === null) { - onFulfilled = identity; - } - - if (onRejected === undefined || onRejected === null) { - onRejected = raise; - } - - var promiseSlots = slots(this); - var state = promiseSlots.state; - if (state === PENDING_STATE) { - promiseSlots.fulfillReactions.push(makeReaction( - capabilityResolve, - capabilityReject, - onFulfilled - )); - - promiseSlots.rejectReactions.push(makeReaction( - capabilityResolve, - capabilityReject, - onRejected - )); - - } else if (state === FULFILLED_STATE || state === REJECTED_STATE) { - setImmediate(makeReaction( - capabilityResolve, - capabilityReject, - state === FULFILLED_STATE ? onFulfilled : onRejected, - promiseSlots.result - )); - } - - return capabilityPromise; - }; - - function makeReaction(resolve, reject, handler, argument) { - var hasArgument = arguments.length > 3; - return function(result) { - try { - result = handler(hasArgument ? argument : result); - } catch (err) { - reject(err); - return; - } - resolve(result); - }; - } - - /** - * When the .catch method is called with argument onRejected, the - * following steps are taken: - * - * people.mozilla.org/~jorendorff/es6-draft.html#sec-promise.prototype.catch - */ - Pp['catch'] = function(onRejected) { - return this.then(undefined, onRejected); - }; - - Pp.toString = function() { - return '[object Promise]'; - }; - - return Promise; -}(/* jslint evil: true */ Function('return this')())); diff --git a/Libraries/vendor/core/Promise.js b/Libraries/vendor/core/Promise.js deleted file mode 100644 index 1593c0fd2..000000000 --- a/Libraries/vendor/core/Promise.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @generated SignedSource<> - * - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * !! This file is a check-in of a static_upstream project! !! - * !! !! - * !! You should not modify this file directly. Instead: !! - * !! 1) Use `fjs use-upstream` to temporarily replace this with !! - * !! the latest version from upstream. !! - * !! 2) Make your changes, test them, etc. !! - * !! 3) Use `fjs push-upstream` to copy your changes back to !! - * !! static_upstream. !! - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * - * Copyright 2013-2014 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule Promise - * - * This module wraps and augments the minimally ES6-compliant Promise - * implementation provided by the ES6Promise module. - */ - -var Promise = require('ES6Promise'); -var Pp = Promise.prototype; - -var invariant = require('invariant'); -var setImmediate = require('setImmediate'); -var throwImmediate = require('throwImmediate'); - -/** - * Handle either fulfillment or rejection with the same callback. - */ -Pp.finally = function(onSettled) { - return this.then(onSettled, onSettled); -}; - -/** - * Throw any unhandled error in a separate tick of the event loop. - */ -Pp.done = function(onFulfilled, onRejected) { - this.then(onFulfilled, onRejected).then(null, throwImmediate); -}; - -/** - * This function takes an object with promises as keys and returns a promise. - * The returned promise is resolved when all promises from the object are - * resolved and gets rejected when the first promise is rejected. - * - * EXAMPLE: - * var promisedMuffin = Promise.allObject({ - * dough: promisedDough, - * frosting: promisedFrosting - * }).then(function(results) { - * return combine(results.dough, results.frosting); - * }); - */ -Promise.allObject = function(/*object*/ promises) { - // Throw instead of warn here to make sure people use this only with object. - invariant( - !Array.isArray(promises), - 'expected an object, got an array instead' - ); - - var keys = Object.keys(promises); - return Promise.all(keys.map(function(key) { - return promises[key]; - })).then(function(values) { - var answers = {}; - values.forEach(function(value, i) { - answers[keys[i]] = value; - }); - return answers; - }); -}; - -module.exports = Promise; From 75e4e124fa510577865766845fcd0ebd1b8e175a Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Wed, 15 Apr 2015 07:07:19 -0700 Subject: [PATCH 4/4] [ReactNative] Use a single DisplayLink held by the bridge --- React/Base/RCTBridge.h | 11 ++++ React/Base/RCTBridge.m | 94 +++++++++++++++++++++++++++ React/Base/RCTFrameUpdate.h | 44 +++++++++++++ React/Modules/RCTTiming.h | 3 +- React/Modules/RCTTiming.m | 20 ++---- React/React.xcodeproj/project.pbxproj | 2 + React/Views/RCTNavigator.h | 7 +- React/Views/RCTNavigator.m | 52 ++++----------- React/Views/RCTNavigatorManager.m | 2 +- 9 files changed, 176 insertions(+), 59 deletions(-) create mode 100644 React/Base/RCTFrameUpdate.h diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 2ff4d9c1e..ab853851c 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -10,6 +10,7 @@ #import #import "RCTBridgeModule.h" +#import "RCTFrameUpdate.h" #import "RCTInvalidating.h" #import "RCTJavaScriptExecutor.h" @@ -122,4 +123,14 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method; */ - (void)reload; +/** + * Add a new observer that will be called on every screen refresh + */ +- (void)addFrameUpdateObserver:(id)observer; + +/** + * Stop receiving screen refresh updates for the given observer + */ +- (void)removeFrameUpdateObserver:(id)observer; + @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index a6040bfe5..8aa83723c 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -677,6 +677,73 @@ static NSDictionary *RCTLocalModulesConfig() return localModules; } +@interface RCTDisplayLink : NSObject + +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; + +@end + +@interface RCTBridge (RCTDisplayLink) + +- (void)_update:(CADisplayLink *)displayLink; + +@end + +@implementation RCTDisplayLink +{ + __weak RCTBridge *_bridge; + CADisplayLink *_displayLink; +} + +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + if ((self = [super init])) { + _bridge = bridge; + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + return self; +} + +- (BOOL)isValid +{ + return _displayLink != nil; +} + +- (void)invalidate +{ + if (self.isValid) { + [_displayLink invalidate]; + _displayLink = nil; + } +} + +- (void)_update:(CADisplayLink *)displayLink +{ + [_bridge _update:displayLink]; +} + +@end + +@interface RCTFrameUpdate (Private) + +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; + +@end + +@implementation RCTFrameUpdate + +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink +{ + if ((self = [super init])) { + _timestamp = displayLink.timestamp; + _deltaTime = displayLink.duration; + } + return self; +} + +@end + @implementation RCTBridge { RCTSparseArray *_modulesByID; @@ -685,6 +752,8 @@ static NSDictionary *RCTLocalModulesConfig() Class _executorClass; NSURL *_bundleURL; RCTBridgeModuleProviderBlock _moduleProvider; + RCTDisplayLink *_displayLink; + NSMutableSet *_frameUpdateObservers; BOOL _loading; } @@ -711,6 +780,8 @@ static id _latestJSExecutor; _latestJSExecutor = _javaScriptExecutor; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; _shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL); + _displayLink = [[RCTDisplayLink alloc] initWithBridge:self]; + _frameUpdateObservers = [[NSMutableSet alloc] init]; // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; @@ -891,6 +962,9 @@ static id _latestJSExecutor; [_javaScriptExecutor invalidate]; _javaScriptExecutor = nil; + [_displayLink invalidate]; + _frameUpdateObservers = nil; + // Invalidate modules for (id target in _modulesByID.allObjects) { if ([target respondsToSelector:@selector(invalidate)]) { @@ -1075,6 +1149,26 @@ static id _latestJSExecutor; return YES; } +- (void)_update:(CADisplayLink *)displayLink +{ + RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; + for (id observer in _frameUpdateObservers) { + if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { + [observer didUpdateFrame:frameUpdate]; + } + } +} + +- (void)addFrameUpdateObserver:(id)observer +{ + [_frameUpdateObservers addObject:observer]; +} + +- (void)removeFrameUpdateObserver:(id)observer +{ + [_frameUpdateObservers removeObject:observer]; +} + - (void)reload { if (!_loading) { diff --git a/React/Base/RCTFrameUpdate.h b/React/Base/RCTFrameUpdate.h new file mode 100644 index 000000000..b9a3d993f --- /dev/null +++ b/React/Base/RCTFrameUpdate.h @@ -0,0 +1,44 @@ +/** + * 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. + */ + +/** + * Interface containing the information about the last screen refresh. + */ +@interface RCTFrameUpdate : NSObject + +/** + * Timestamp for the actual screen refresh + */ +@property (nonatomic, readonly) NSTimeInterval timestamp; + +/** + * Time since the last frame update ( >= 16.6ms ) + */ +@property (nonatomic, readonly) NSTimeInterval deltaTime; + +@end + +/** + * Protocol that must be implemented for subscribing to display refreshes (DisplayLink updates) + */ +@protocol RCTFrameUpdateObserver + +/** + * Method called on every screen refresh (if paused != YES) + */ +- (void)didUpdateFrame:(RCTFrameUpdate *)update; + +@optional + +/** + * Synthesize and set to true to pause the calls to -[didUpdateFrame:] + */ +@property (nonatomic, assign, getter=isPaused) BOOL paused; + +@end diff --git a/React/Modules/RCTTiming.h b/React/Modules/RCTTiming.h index 67251613b..c6d63bcfc 100644 --- a/React/Modules/RCTTiming.h +++ b/React/Modules/RCTTiming.h @@ -10,8 +10,9 @@ #import #import "RCTBridgeModule.h" +#import "RCTFrameUpdate.h" #import "RCTInvalidating.h" -@interface RCTTiming : NSObject +@interface RCTTiming : NSObject @end diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index 8c7ef1f23..ce8688f62 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -58,7 +58,6 @@ @implementation RCTTiming { RCTSparseArray *_timers; - id _updateTimer; } @synthesize bridge = _bridge; @@ -113,32 +112,21 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) - (void)stopTimers { - [_updateTimer invalidate]; - _updateTimer = nil; + [_bridge removeFrameUpdateObserver:self]; } - (void)startTimers { RCTAssertMainThread(); - if (![self isValid] || _updateTimer != nil || _timers.count == 0) { + if (![self isValid] || _timers.count == 0) { return; } - _updateTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; - if (_updateTimer) { - [_updateTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; - } else { - RCTLogWarn(@"Failed to create a display link (probably on buildbot) - using an NSTimer for AppEngine instead."); - _updateTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60) - target:self - selector:@selector(update) - userInfo:nil - repeats:YES]; - } + [_bridge addFrameUpdateObserver:self]; } -- (void)update +- (void)didUpdateFrame:(RCTFrameUpdate *)update { RCTAssertMainThread(); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 4c1cfc241..294bf4145 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = ""; }; + 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTFrameUpdate.h; sourceTree = ""; }; 14435CE11AAC4AE100FC20F4 /* RCTMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMap.h; sourceTree = ""; }; 14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = ""; }; 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = ""; }; @@ -391,6 +392,7 @@ 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */, 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA501A601E3B00E9B192 /* RCTUtils.m */, + 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */, ); path = Base; sourceTree = ""; diff --git a/React/Views/RCTNavigator.h b/React/Views/RCTNavigator.h index ad7a2fd32..c59c9a3d3 100644 --- a/React/Views/RCTNavigator.h +++ b/React/Views/RCTNavigator.h @@ -9,16 +9,17 @@ #import +#import "RCTFrameUpdate.h" #import "RCTInvalidating.h" -@class RCTEventDispatcher; +@class RCTBridge; -@interface RCTNavigator : UIView +@interface RCTNavigator : UIView @property (nonatomic, strong) UIView *reactNavSuperviewLink; @property (nonatomic, assign) NSInteger requestedTopOfStack; -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; /** * Schedules a JavaScript navigation and prevents `UIKit` from navigating until diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 5523e49b7..f3ebb6554 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -10,6 +10,7 @@ #import "RCTNavigator.h" #import "RCTAssert.h" +#import "RCTBridge.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" @@ -190,10 +191,6 @@ NSInteger kNeverProgressed = -10000; @end @interface RCTNavigator() -{ - RCTEventDispatcher *_eventDispatcher; - NSInteger _numberOfViewControllerMovesToIgnore; -} @property (nonatomic, assign) NSInteger previousRequestedTopOfStack; @@ -251,7 +248,6 @@ NSInteger kNeverProgressed = -10000; * */ @property (nonatomic, readonly, assign) CGFloat mostRecentProgress; -@property (nonatomic, readwrite, strong) CADisplayLink *displayLink; @property (nonatomic, readonly, strong) NSTimer *runTimer; @property (nonatomic, readonly, assign) NSInteger currentlyTransitioningFrom; @property (nonatomic, readonly, assign) NSInteger currentlyTransitioningTo; @@ -263,22 +259,17 @@ NSInteger kNeverProgressed = -10000; @end @implementation RCTNavigator +{ + __weak RCTBridge *_bridge; + NSInteger _numberOfViewControllerMovesToIgnore; +} -- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (id)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithFrame:CGRectZero])) { - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(reportNavigationProgress:)]; + _bridge = bridge; _mostRecentProgress = kNeverProgressed; _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; - if (_displayLink) { - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - _displayLink.paused = YES; - } else { - // It's okay to leak this on a build bot. - RCTLogWarn(@"Failed to create a display link (probably on automated build system) - using an NSTimer for AppEngine instead."); - _runTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) target:self selector:@selector(reportNavigationProgress:) userInfo:nil repeats:YES]; - } - _eventDispatcher = eventDispatcher; _previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push. _previousViews = @[]; _currentViews = [[NSMutableArray alloc] initWithCapacity:0]; @@ -295,7 +286,7 @@ NSInteger kNeverProgressed = -10000; return self; } -- (void)reportNavigationProgress:(CADisplayLink *)sender +- (void)didUpdateFrame:(RCTFrameUpdate *)update { if (_currentlyTransitioningFrom != _currentlyTransitioningTo) { UIView *topView = _dummyView; @@ -307,7 +298,7 @@ NSInteger kNeverProgressed = -10000; return; } _mostRecentProgress = nextProgress; - [_eventDispatcher sendInputEventWithName:@"topNavigationProgress" body:@{ + [_bridge.eventDispatcher sendInputEventWithName:@"topNavigationProgress" body:@{ @"fromIndex": @(_currentlyTransitioningFrom), @"toIndex": @(_currentlyTransitioningTo), @"progress": @(nextProgress), @@ -350,16 +341,14 @@ NSInteger kNeverProgressed = -10000; _dummyView.frame = (CGRect){{destination}}; _currentlyTransitioningFrom = indexOfFrom; _currentlyTransitioningTo = indexOfTo; - if (indexOfFrom != indexOfTo) { - _displayLink.paused = NO; - } + [_bridge addFrameUpdateObserver:self]; } completion:^(id context) { [weakSelf freeLock]; _currentlyTransitioningFrom = 0; _currentlyTransitioningTo = 0; _dummyView.frame = CGRectZero; - _displayLink.paused = YES; + [_bridge removeFrameUpdateObserver:self]; // Reset the parallel position tracker }]; } @@ -400,19 +389,6 @@ NSInteger kNeverProgressed = -10000; return _currentViews; } -- (BOOL)isValid -{ - return _displayLink != nil; -} - -- (void)invalidate -{ - // Prevent displayLink from retaining the navigator indefinitely - [_displayLink invalidate]; - _displayLink = nil; - _runTimer = nil; -} - - (void)layoutSubviews { [super layoutSubviews]; @@ -430,7 +406,7 @@ NSInteger kNeverProgressed = -10000; - (void)handleTopOfStackChanged { - [_eventDispatcher sendInputEventWithName:@"topNavigateBack" body:@{ + [_bridge.eventDispatcher sendInputEventWithName:@"topNavigateBack" body:@{ @"target":self.reactTag, @"stackLength":@(_navigationController.viewControllers.count) }]; @@ -438,7 +414,7 @@ NSInteger kNeverProgressed = -10000; - (void)dispatchFakeScrollEvent { - [_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove + [_bridge.eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove reactTag:self.reactTag scrollView:nil userData:nil]; @@ -511,7 +487,7 @@ NSInteger kNeverProgressed = -10000; if (jsGettingAhead) { if (reactPushOne) { UIView *lastView = [_currentViews lastObject]; - RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_eventDispatcher]; + RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_bridge.eventDispatcher]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; [_navigationController pushViewController:vc animated:(currentReactCount > 1)]; diff --git a/React/Views/RCTNavigatorManager.m b/React/Views/RCTNavigatorManager.m index 730380bf9..1158f7dcf 100644 --- a/React/Views/RCTNavigatorManager.m +++ b/React/Views/RCTNavigatorManager.m @@ -21,7 +21,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[RCTNavigator alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; + return [[RCTNavigator alloc] initWithBridge:self.bridge]; } RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack, NSInteger)