diff --git a/.flowconfig b/.flowconfig index 4f9e8498e5c341..aa31b812a515a6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -15,6 +15,12 @@ .*/node_modules/react-tools/src/core/ReactInstanceHandles.js .*/node_modules/react-tools/src/event/EventPropagators.js +# Ignore commoner tests +.*/node_modules/react-tools/node_modules/commoner/test/.* + +# See https://github.com/facebook/flow/issues/442 +.*/react-tools/node_modules/commoner/lib/reader.js + # Ignore jest .*/react-native/node_modules/jest-cli/.* @@ -29,3 +35,6 @@ Examples/UIExplorer/ImageMocks.js [options] module.system=haste + +[version] +0.10.0 diff --git a/.travis.yml b/.travis.yml index 35a0f0ef105db3..603e924bb5b5cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ cache: before_install: - brew update install: - - brew reinstall flow watchman xctool + - brew reinstall flow xctool - npm config set spin=false - npm install diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32b4d2f93290b1..6bb19e007ba308 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,14 +52,27 @@ Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe ### Code -* Use semicolons; +#### General + * Add trailing commas, * 2 spaces for indentation (no tabs) -* Prefer `'` over `"` -* `'use strict';` -* 80 character line length * "Attractive" + +#### JavaScript + +* Use semicolons; +* `'use strict';` +* Prefer `'` over `"` * Do not use the optional parameters of `setTimeout` and `setInterval` +* 80 character line length + +#### Objective-C + +* Space after `@property` declarations +* Brackets on *every* `if`, on the *same* line +* `- method`, `@interface`, and `@implementation` brackets on the following line +* *Try* to keep it around 80 characters line length (sometimes it's just not possible...) +* `*` operator goes with the variable name (e.g. `NSObject *variableName;`) ### Documentation diff --git a/Examples/2048/2048/AppDelegate.m b/Examples/2048/2048/AppDelegate.m index bc089038c21d34..004e854a7558ae 100644 --- a/Examples/2048/2048/AppDelegate.m +++ b/Examples/2048/2048/AppDelegate.m @@ -22,24 +22,33 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( { NSURL *jsCodeLocation; - // Loading JavaScript code - uncomment the one you want. - - // OPTION 1 - // Load from development server. Start the server from the repository root: - // - // $ npm start - // - // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and - // iOS device are on the same Wi-Fi network. + /** + * Loading JavaScript code - uncomment the one you want. + * + * OPTION 1 + * Load from development server. Start the server from the repository root: + * + * $ npm start + * + * To run on device, change `localhost` to the IP address of your computer + * (you can get this by typing `ifconfig` into the terminal and selecting the + * `inet` value under `en0:`) and make sure your computer and iOS device are + * on the same Wi-Fi network. + */ + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle"]; - // OPTION 2 - // Load from pre-bundled file on disk. To re-generate the static bundle, run - // - // $ curl http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle -o main.jsbundle - // - // and uncomment the next following line - // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + /** + * OPTION 2 + * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` + * to your Xcode project folder in the terminal, and run + * + * $ curl 'http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle' -o main.jsbundle + * + * then add the `main.jsbundle` file to your project and uncomment this line: + */ + +// jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"Game2048" diff --git a/Examples/Movies/Movies/AppDelegate.m b/Examples/Movies/Movies/AppDelegate.m index b5672271252c87..74aed2cc4724d9 100644 --- a/Examples/Movies/Movies/AppDelegate.m +++ b/Examples/Movies/Movies/AppDelegate.m @@ -23,24 +23,33 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( { NSURL *jsCodeLocation; - // Loading JavaScript code - uncomment the one you want. + /** + * Loading JavaScript code - uncomment the one you want. + * + * OPTION 1 + * Load from development server. Start the server from the repository root: + * + * $ npm start + * + * To run on device, change `localhost` to the IP address of your computer + * (you can get this by typing `ifconfig` into the terminal and selecting the + * `inet` value under `en0:`) and make sure your computer and iOS device are + * on the same Wi-Fi network. + */ - // OPTION 1 - // Load from development server. Start the server from the repository root: - // - // $ npm start - // - // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and - // iOS device are on the same Wi-Fi network. jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle"]; - // OPTION 2 - // Load from pre-bundled file on disk. To re-generate the static bundle, run - // - // $ curl http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle -o main.jsbundle - // - // and uncomment the next following line - // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + /** + * OPTION 2 + * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` + * to your Xcode project folder in the terminal, and run + * + * $ curl 'http://localhost:8081/Examples/Movies/MoviesApp.includeRequire.runModule.bundle' -o main.jsbundle + * + * then add the `main.jsbundle` file to your project and uncomment this line: + */ + +// jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"MoviesApp" diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index fc31a90dc4d6b7..e484d0f6591ce0 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -29,8 +29,6 @@ var TimerMixin = require('react-timer-mixin'); var MovieCell = require('./MovieCell'); var MovieScreen = require('./MovieScreen'); -var fetch = require('fetch'); - /** * This is for demo purposes only, and rate limited. * In case you want to use the Rotten Tomatoes' API on a real app you should diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index d2fc3eadedee94..3928adc332ef9c 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; 00E356F31AD99517003FC87E /* SampleAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* SampleAppTests.m */; }; 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -82,6 +83,13 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = SampleApp; }; + 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; @@ -117,6 +125,7 @@ 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* SampleAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleAppTests.m; sourceTree = ""; }; + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = iOS/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = iOS/AppDelegate.m; sourceTree = ""; }; @@ -142,6 +151,7 @@ buildActionMask = 2147483647; files = ( 146834051AC3E58100842450 /* libReact.a in Frameworks */, + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, 00481BE81AC0C86700671115 /* libRCTWebSocketDebugger.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, 00C302E61ABCBA2D00DB3ED1 /* libRCTAdSupport.a in Frameworks */, @@ -230,6 +240,14 @@ name = "Supporting Files"; sourceTree = ""; }; + 139105B71AF99BAD00B5F7CC /* Products */ = { + isa = PBXGroup; + children = ( + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* SampleApp */ = { isa = PBXGroup; children = ( @@ -263,14 +281,15 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( - 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, - 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, 00C302AF1ABCB8E700DB3ED1 /* RCTAdSupport.xcodeproj */, 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */, ); @@ -395,6 +414,10 @@ ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; }, + { + ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; + ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + }, { ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; @@ -470,6 +493,13 @@ remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 146834041AC3E56700842450 /* libReact.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/SampleApp/SampleAppTests/SampleAppTests.m b/Examples/SampleApp/SampleAppTests/SampleAppTests.m index 64794271376b04..d8dce811dac25d 100644 --- a/Examples/SampleApp/SampleAppTests/SampleAppTests.m +++ b/Examples/SampleApp/SampleAppTests/SampleAppTests.m @@ -44,8 +44,8 @@ - (void)testRendersWelcomeScreen { NSString *redboxError = nil; while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; diff --git a/Examples/SampleApp/_flowconfig b/Examples/SampleApp/_flowconfig index 1082fb195b01e3..9d4fb361444f83 100644 --- a/Examples/SampleApp/_flowconfig +++ b/Examples/SampleApp/_flowconfig @@ -15,6 +15,12 @@ .*/node_modules/react-tools/src/core/ReactInstanceHandles.js .*/node_modules/react-tools/src/event/EventPropagators.js +# Ignore commoner tests +.*/node_modules/react-tools/node_modules/commoner/test/.* + +# See https://github.com/facebook/flow/issues/442 +.*/react-tools/node_modules/commoner/lib/reader.js + # Ignore jest .*/react-native/node_modules/jest-cli/.* diff --git a/Examples/SampleApp/iOS/AppDelegate.m b/Examples/SampleApp/iOS/AppDelegate.m index bb30741d86939d..7e8d5fecfb2915 100644 --- a/Examples/SampleApp/iOS/AppDelegate.m +++ b/Examples/SampleApp/iOS/AppDelegate.m @@ -17,25 +17,33 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( { NSURL *jsCodeLocation; - // Loading JavaScript code - uncomment the one you want. - - // OPTION 1 - // Load from development server. Start the server from the repository root: - // - // $ npm start - // - // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and - // iOS device are on the same Wi-Fi network. + /** + * Loading JavaScript code - uncomment the one you want. + * + * OPTION 1 + * Load from development server. Start the server from the repository root: + * + * $ npm start + * + * To run on device, change `localhost` to the IP address of your computer + * (you can get this by typing `ifconfig` into the terminal and selecting the + * `inet` value under `en0:`) and make sure your computer and iOS device are + * on the same Wi-Fi network. + */ + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/SampleApp/index.ios.bundle"]; - // OPTION 2 - // Load from pre-bundled file on disk. To re-generate the static bundle, - // from the root of your project directory, run - // - // $ react-native bundle - // - // and uncomment the next following line - // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + /** + * OPTION 2 + * Load from pre-bundled file on disk. To re-generate the static bundle + * from the root of your project directory, run + * + * $ react-native bundle --minify + * + * see http://facebook.github.io/react-native/docs/runningondevice.html + */ + +// jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"SampleApp" diff --git a/Examples/SampleApp/iOS/Base.lproj/LaunchScreen.xib b/Examples/SampleApp/iOS/Base.lproj/LaunchScreen.xib index 73cc9d07c74a8e..3b8cd49b8820a0 100644 --- a/Examples/SampleApp/iOS/Base.lproj/LaunchScreen.xib +++ b/Examples/SampleApp/iOS/Base.lproj/LaunchScreen.xib @@ -1,8 +1,8 @@ - + - + diff --git a/Examples/SampleApp/iOS/main.jsbundle b/Examples/SampleApp/iOS/main.jsbundle index 7cc6a2adcf6796..b702b30c66dc4c 100644 --- a/Examples/SampleApp/iOS/main.jsbundle +++ b/Examples/SampleApp/iOS/main.jsbundle @@ -1,5 +1,8 @@ // Offline JS -// To re-generate the offline bundle, run this from root of your project -// $ curl 'http://localhost:8081/Examples/SampleApp/index.ios.bundle?dev=false&minify=true' -o iOS/main.jsbundle +// To re-generate the offline bundle, run this from the root of your project: +// +// $ react-native bundle --minify +// +// See http://facebook.github.io/react-native/docs/runningondevice.html for more details. throw new Error('Offline JS file is empty. See iOS/main.jsbundle for instructions'); diff --git a/Examples/SampleApp/index.ios.js b/Examples/SampleApp/index.ios.js index 629118d10721ad..46c0712bd9b286 100644 --- a/Examples/SampleApp/index.ios.js +++ b/Examples/SampleApp/index.ios.js @@ -24,7 +24,7 @@ var SampleApp = React.createClass({ Press Cmd+R to reload,{'\n'} - Cmd+Control+Z for dev menu + Cmd+D or shake for dev menu ); diff --git a/Examples/TicTacToe/TicTacToe/AppDelegate.m b/Examples/TicTacToe/TicTacToe/AppDelegate.m index a118b94dc47051..9c328a3a83a57e 100644 --- a/Examples/TicTacToe/TicTacToe/AppDelegate.m +++ b/Examples/TicTacToe/TicTacToe/AppDelegate.m @@ -22,24 +22,33 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( { NSURL *jsCodeLocation; - // Loading JavaScript code - uncomment the one you want. - - // OPTION 1 - // Load from development server. Start the server from the repository root: - // - // $ npm start - // - // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and - // iOS device are on the same Wi-Fi network. + /** + * Loading JavaScript code - uncomment the one you want. + * + * OPTION 1 + * Load from development server. Start the server from the repository root: + * + * $ npm start + * + * To run on device, change `localhost` to the IP address of your computer + * (you can get this by typing `ifconfig` into the terminal and selecting the + * `inet` value under `en0:`) and make sure your computer and iOS device are + * on the same Wi-Fi network. + */ + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle"]; - // OPTION 2 - // Load from pre-bundled file on disk. To re-generate the static bundle, run - // - // $ curl http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle -o main.jsbundle - // - // and uncomment the next following line - // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + /** + * OPTION 2 + * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` + * to your Xcode project folder in the terminal, and run + * + * $ curl 'http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle' -o main.jsbundle + * + * then add the `main.jsbundle` file to your project and uncomment this line: + */ + +// jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"TicTacToeApp" diff --git a/Examples/UIExplorer/ActivityIndicatorIOSExample.js b/Examples/UIExplorer/ActivityIndicatorIOSExample.js index 4151609e068de3..9655d680c71b63 100644 --- a/Examples/UIExplorer/ActivityIndicatorIOSExample.js +++ b/Examples/UIExplorer/ActivityIndicatorIOSExample.js @@ -57,6 +57,7 @@ var ToggleAnimatingActivityIndicator = React.createClass({ } }); +exports.displayName = (undefined: ?string); exports.framework = 'React'; exports.title = ''; exports.description = 'Animated loading indicators.'; diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index d9c2acf9a654dc..1790dc49164d46 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -57,6 +57,17 @@ var styles = StyleSheet.create({ borderLeftWidth: 40, borderLeftColor: 'blue', }, + border5: { + borderRadius: 50, + borderTopWidth: 10, + borderTopColor: 'red', + borderRightWidth: 20, + borderRightColor: 'yellow', + borderBottomWidth: 30, + borderBottomColor: 'green', + borderLeftWidth: 40, + borderLeftColor: 'blue', + }, }); exports.title = 'Border'; @@ -71,7 +82,7 @@ exports.examples = [ }, { title: 'Equal-Width / Same-Color', - description: 'borderWidth & borderColor', + description: 'borderWidth & borderColor & borderRadius', render() { return ; } @@ -97,4 +108,11 @@ exports.examples = [ return ; } }, + { + title: 'Custom Borders', + description: 'border*Width & border*Color', + render() { + return ; + } + }, ]; diff --git a/Examples/UIExplorer/GeolocationExample.js b/Examples/UIExplorer/GeolocationExample.js index c55bd351b09ce4..9bd744678df243 100644 --- a/Examples/UIExplorer/GeolocationExample.js +++ b/Examples/UIExplorer/GeolocationExample.js @@ -50,7 +50,8 @@ var GeolocationExample = React.createClass({ componentDidMount: function() { navigator.geolocation.getCurrentPosition( (initialPosition) => this.setState({initialPosition}), - (error) => console.error(error) + (error) => console.error(error), + {enableHighAccuracy: true, timeout: 100, maximumAge: 1000} ); this.watchID = navigator.geolocation.watchPosition((lastPosition) => { this.setState({lastPosition}); diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js index b888acbf74ca41..3f1883fa65c5bb 100644 --- a/Examples/UIExplorer/ImageMocks.js +++ b/Examples/UIExplorer/ImageMocks.js @@ -39,3 +39,8 @@ declare module 'image!uie_thumb_selected' { declare var uri: string; declare var isStatic: boolean; } + +declare module 'image!NavBarButtonPlus' { + declare var uri: string; + declare var isStatic: boolean; +} diff --git a/Examples/UIExplorer/LayoutEventsExample.js b/Examples/UIExplorer/LayoutEventsExample.js new file mode 100644 index 00000000000000..6aec6257e77629 --- /dev/null +++ b/Examples/UIExplorer/LayoutEventsExample.js @@ -0,0 +1,150 @@ +/** + * 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, + LayoutAnimation, + StyleSheet, + Text, + View, +} = React; + +type LayoutEvent = { + nativeEvent: { + layout: { + x: number; + y: number; + width: number; + height: number; + }; + }; +}; + +var LayoutEventExample = React.createClass({ + getInitialState: function() { + return { + viewStyle: { + margin: 20, + }, + }; + }, + animateViewLayout: function() { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.spring, + () => { + console.log('layout animation done.'); + this.addWrapText(); + }, + (error) => { throw new Error(JSON.stringify(error)); } + ); + this.setState({ + viewStyle: { + margin: this.state.viewStyle.margin > 20 ? 20 : 60, + } + }); + }, + addWrapText: function() { + this.setState( + {extraText: ' And a bunch more text to wrap around a few lines.'}, + this.changeContainer + ); + }, + changeContainer: function() { + this.setState({containerStyle: {width: 280}}); + }, + onViewLayout: function(e: LayoutEvent) { + console.log('received view layout event\n', e.nativeEvent); + this.setState({viewLayout: e.nativeEvent.layout}); + }, + onTextLayout: function(e: LayoutEvent) { + console.log('received text layout event\n', e.nativeEvent); + this.setState({textLayout: e.nativeEvent.layout}); + }, + onImageLayout: function(e: LayoutEvent) { + console.log('received image layout event\n', e.nativeEvent); + this.setState({imageLayout: e.nativeEvent.layout}); + }, + render: function() { + var viewStyle = [styles.view, this.state.viewStyle]; + var textLayout = this.state.textLayout || {width: '?', height: '?'}; + var imageLayout = this.state.imageLayout || {x: '?', y: '?'}; + return ( + + + onLayout events are called on mount and whenever layout is updated, + including after layout animations complete.{' '} + + Press here to change layout. + + + + + + ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'} + + + A simple piece of text.{this.state.extraText} + + + {'\n'} + Text w/h: {textLayout.width}/{textLayout.height + '\n'} + Image x/y: {imageLayout.x}/{imageLayout.y} + + + + ); + } +}); + +var styles = StyleSheet.create({ + view: { + padding: 12, + borderColor: 'black', + borderWidth: 0.5, + backgroundColor: 'transparent', + }, + text: { + alignSelf: 'flex-start', + borderColor: 'rgba(0, 0, 255, 0.2)', + borderWidth: 0.5, + }, + image: { + width: 50, + height: 50, + marginBottom: 10, + alignSelf: 'center', + }, + pressText: { + fontWeight: 'bold', + }, +}); + +exports.title = 'onLayout'; +exports.description = 'Layout events can be used to measure view size and position.'; +exports.examples = [ +{ + title: 'onLayout', + render: function(): ReactElement { + return ; + }, +}]; diff --git a/Examples/UIExplorer/Navigator/NavigatorExample.js b/Examples/UIExplorer/Navigator/NavigatorExample.js index ef8001f94c6ee7..4a182a5a40b8cc 100644 --- a/Examples/UIExplorer/Navigator/NavigatorExample.js +++ b/Examples/UIExplorer/Navigator/NavigatorExample.js @@ -175,4 +175,6 @@ var styles = StyleSheet.create({ } }); +TabBarExample.external = true; + module.exports = TabBarExample; diff --git a/Examples/UIExplorer/NavigatorIOSColorsExample.js b/Examples/UIExplorer/NavigatorIOSColorsExample.js new file mode 100644 index 00000000000000..386586f230bb89 --- /dev/null +++ b/Examples/UIExplorer/NavigatorIOSColorsExample.js @@ -0,0 +1,90 @@ +/** + * 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. +*/ +'use strict'; + +var React = require('react-native'); +var { + NavigatorIOS, + StatusBarIOS, + StyleSheet, + Text, + View +} = React; + +var EmptyPage = React.createClass({ + + render: function() { + return ( + + + {this.props.text} + + + ); + }, + +}); + +var NavigatorIOSColors = React.createClass({ + + statics: { + title: ' - Custom', + description: 'iOS navigation with custom nav bar colors', + }, + + render: function() { + // Set StatusBar with light contents to get better contrast + StatusBarIOS.setStyle(StatusBarIOS.Style['lightContent']); + + return ( + ', + rightButtonTitle: 'Done', + onRightButtonPress: () => { + StatusBarIOS.setStyle(StatusBarIOS.Style['default']); + this.props.onExampleExit(); + }, + passProps: { + text: 'The nav bar has custom colors with tintColor, ' + + 'barTintColor and titleTextColor props.', + }, + }} + tintColor="#FFFFFF" + barTintColor="#183E63" + titleTextColor="#FFFFFF" + /> + ); + }, + +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + emptyPage: { + flex: 1, + paddingTop: 64, + }, + emptyPageText: { + margin: 10, + }, +}); + +NavigatorIOSColors.external = true; + +module.exports = NavigatorIOSColors; diff --git a/Examples/UIExplorer/NavigatorIOSExample.js b/Examples/UIExplorer/NavigatorIOSExample.js index eee731bd53834a..4a2011a654611c 100644 --- a/Examples/UIExplorer/NavigatorIOSExample.js +++ b/Examples/UIExplorer/NavigatorIOSExample.js @@ -19,6 +19,7 @@ var React = require('react-native'); var ViewExample = require('./ViewExample'); var createExamplePage = require('./createExamplePage'); var { + AlertIOS, PixelRatio, ScrollView, StyleSheet, @@ -92,6 +93,30 @@ var NavigatorIOSExample = React.createClass({ } }); })} + {this._renderRow('Custom Left & Right Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonTitle: 'Custom Left', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonIcon: require('image!NavBarButtonPlus'), + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} {this._renderRow('Pop', () => { this.props.navigator.pop(); })} diff --git a/Examples/UIExplorer/NetInfoExample.js b/Examples/UIExplorer/NetInfoExample.js index 18a79ae4a571f4..c322a743228306 100644 --- a/Examples/UIExplorer/NetInfoExample.js +++ b/Examples/UIExplorer/NetInfoExample.js @@ -131,12 +131,12 @@ exports.description = 'Monitor network status'; exports.examples = [ { title: 'NetInfo.isConnected', - description: 'Asyncronously load and observe connectivity', + description: 'Asynchronously load and observe connectivity', render(): ReactElement { return ; } }, { title: 'NetInfo.reachabilityIOS', - description: 'Asyncronously load and observe iOS reachability', + description: 'Asynchronously load and observe iOS reachability', render(): ReactElement { return ; } }, { diff --git a/Examples/UIExplorer/SegmentedControlIOSExample.js b/Examples/UIExplorer/SegmentedControlIOSExample.js new file mode 100644 index 00000000000000..119196d8f8a97d --- /dev/null +++ b/Examples/UIExplorer/SegmentedControlIOSExample.js @@ -0,0 +1,169 @@ +/** + * 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 { + SegmentedControlIOS, + Text, + View, + StyleSheet +} = React; + +var BasicSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + } +}); + +var PreSelectedSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var MomentarySegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + } +}); + +var DisabledSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + ); + }, +}); + +var ColorSegmentedControlExample = React.createClass({ + render() { + return ( + + + + + + + + + ); + }, +}); + +var EventSegmentedControlExample = React.createClass({ + getInitialState() { + return { + values: ['One', 'Two', 'Three'], + value: 'Not selected', + selectedIndex: undefined + }; + }, + + render() { + return ( + + + Value: {this.state.value} + + + Index: {this.state.selectedIndex} + + + + ); + }, + + _onChange(event) { + this.setState({ + selectedIndex: event.nativeEvent.selectedIndex, + }); + }, + + _onValueChange(value) { + this.setState({ + value: value, + }); + } +}); + +var styles = StyleSheet.create({ + text: { + fontSize: 14, + textAlign: 'center', + fontWeight: '500', + margin: 10, + }, +}); + +exports.title = ''; +exports.displayName = 'SegmentedControlExample'; +exports.description = 'Native segmented control'; +exports.examples = [ + { + title: 'Segmented controls can have values', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can have a pre-selected value', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be momentary', + render(): ReactElement { return ; } + }, + { + title: 'Segmented controls can be disabled', + render(): ReactElement { return ; } + }, + { + title: 'Custom colors can be provided', + render(): ReactElement { return ; } + }, + { + title: 'Change events can be detected', + render(): ReactElement { return ; } + } +]; diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index a8f913a07de228..9b748ee33695a6 100644 --- a/Examples/UIExplorer/TabBarIOSExample.js +++ b/Examples/UIExplorer/TabBarIOSExample.js @@ -78,7 +78,7 @@ var TabBarExample = React.createClass({ this.setState({ selectedTab: 'greenTab', presses: this.state.presses + 1 - }); + }); }}> {this._renderContent('#21551C', 'Green Tab')} diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index e0ae1b46517070..922dd9607d4b88 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -88,9 +88,9 @@ var styles = StyleSheet.create({ height: 26, borderWidth: 0.5, borderColor: '#0f0f0f', - padding: 4, flex: 1, fontSize: 13, + padding: 4, }, multiline: { borderWidth: 0.5, @@ -98,6 +98,22 @@ var styles = StyleSheet.create({ flex: 1, fontSize: 13, height: 50, + padding: 4, + marginBottom: 4, + }, + multilineWithFontStyles: { + color: 'blue', + fontWeight: 'bold', + fontSize: 18, + fontFamily: 'Cochin', + height: 60, + }, + multilineChild: { + width: 50, + height: 40, + position: 'absolute', + right: 5, + backgroundColor: 'red', }, eventLabel: { margin: 3, @@ -118,7 +134,7 @@ var styles = StyleSheet.create({ }); exports.title = ''; -exports.description = 'Single-line text inputs.'; +exports.description = 'Single and multi-line text inputs.'; exports.examples = [ { title: 'Auto-focus', @@ -313,7 +329,7 @@ exports.examples = [ }, { title: 'Clear and select', - render: function () { + render: function() { return ( @@ -336,4 +352,42 @@ exports.examples = [ ); } }, + { + title: 'Multiline', + render: function() { + return ( + + + + + + + + + ) + } + } ]; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 348d04f0d03999..698fd3b3f42fff 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; }; 14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14DC67F11AB71876001358AB /* libRCTPushNotification.a */; }; 58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58005BEE1ABA80530062E044 /* libRCTTest.a */; }; + 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; }; D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; /* End PBXBuildFile section */ @@ -103,6 +104,13 @@ remoteGlobalIDString = 580C376F1AB104AF0015E709; remoteInfo = RCTTest; }; + 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; @@ -129,6 +137,7 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = UIExplorer/Images.xcassets; sourceTree = ""; }; 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 = ""; }; 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = ../../Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj; sourceTree = ""; }; 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; @@ -148,6 +157,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */, 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, 00D2771A1AB8C3E100DC1E48 /* libRCTWebSocketDebugger.a in Frameworks */, 58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */, @@ -200,6 +210,7 @@ 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */, 13417FE31AA91428003F314A /* RCTImage.xcodeproj */, 134180261AA91779003F314A /* RCTNetwork.xcodeproj */, + 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */, 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */, 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */, 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */, @@ -293,6 +304,14 @@ name = Products; sourceTree = ""; }; + 834C36CE1AF8DA610019C93C /* Products */ = { + isa = PBXGroup; + children = ( + 834C36D21AF8DA610019C93C /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( @@ -411,6 +430,10 @@ ProductGroup = 14DC67E81AB71876001358AB /* Products */; ProjectRef = 14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */; }, + { + ProductGroup = 834C36CE1AF8DA610019C93C /* Products */; + ProjectRef = 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */; + }, { ProductGroup = 58005BE51ABA80530062E044 /* Products */; ProjectRef = 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */; @@ -511,6 +534,13 @@ remoteRef = 58005BED1ABA80530062E044 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 834C36D21AF8DA610019C93C /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -632,6 +662,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index b231b77ee84fc9..488c0077dcdbd3 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -1,6 +1,6 @@ @@ -127,6 +135,7 @@ class UIExplorerList extends React.Component { onChangeText={this._search.bind(this)} placeholder="Search..." style={styles.searchTextInput} + value={this.state.searchText} /> 0 && !foundElement && !redboxError) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; - - foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { + foundElement = [self findSubviewInView:vc.view matching:^(UIView *view) { if ([view respondsToSelector:@selector(attributedText)]) { NSString *text = [(id)view attributedText].string; if ([text isEqualToString:@""]) { @@ -120,6 +118,7 @@ - (void)testTabBarExampleSnapshot [_runner runTest:_cmd module:@"TabBarExample"]; } +// Make sure this test runs last - (void)testZZZ_NotInRecordMode { RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit."); diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 8813a8afdb1a01..d1e990cb4b4995 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -94,6 +94,7 @@ var WebViewExample = React.createClass({ automaticallyAdjustContentInsets={false} style={styles.webView} url={this.state.url} + javaScriptEnabledAndroid={true} onNavigationStateChange={this.onNavigationStateChange} startInLoadingState={true} /> diff --git a/Examples/UIExplorer/createExamplePage.js b/Examples/UIExplorer/createExamplePage.js index ab725e84465fb6..4bc933ebaa11b9 100644 --- a/Examples/UIExplorer/createExamplePage.js +++ b/Examples/UIExplorer/createExamplePage.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var ReactIOS = require('ReactIOS'); var UIExplorerBlock = require('./UIExplorerBlock'); var UIExplorerPage = require('./UIExplorerPage'); @@ -46,20 +47,28 @@ var createExamplePage = function(title: ?string, exampleModule: ExampleModule) getBlock: function(example, i) { // Hack warning: This is a hack because the www UI explorer requires // renderComponent to be called. - var originalRenderComponent = React.renderComponent; var originalRender = React.render; + var originalRenderComponent = React.renderComponent; + var originalIOSRender = ReactIOS.render; + var originalIOSRenderComponent = ReactIOS.renderComponent; var renderedComponent; // TODO remove typecasts when Flow bug #6560135 is fixed // and workaround is removed from react-native.js - (React: Object).render = (React: Object).renderComponent = function(element, container) { - renderedComponent = element; - }; + (React: Object).render = + (React: Object).renderComponent = + (ReactIOS: Object).render = + (ReactIOS: Object).renderComponent = + function(element, container) { + renderedComponent = element; + }; var result = example.render(null); if (result) { renderedComponent = result; } - (React: Object).renderComponent = originalRenderComponent; (React: Object).render = originalRender; + (React: Object).renderComponent = originalRenderComponent; + (ReactIOS: Object).render = originalIOSRender; + (ReactIOS: Object).renderComponent = originalIOSRenderComponent; return ( { + debug('layout animation done.'); + this.checkLayout(this.addWrapText); + }, + (error) => { throw new Error(JSON.stringify(error)); } + ); + this.setState({viewStyle: {margin: 60}}); + }, + addWrapText: function() { + this.setState( + {extraText: ' And a bunch more text to wrap around a few lines.'}, + () => this.checkLayout(this.changeContainer) + ); + }, + changeContainer: function() { + this.setState( + {containerStyle: {width: 280}}, + () => this.checkLayout(TestModule.markTestCompleted) + ); + }, + checkLayout: function(next?: ?Function) { + if (!this.isMounted()) { + return; + } + this.refs.view.measure((x, y, width, height) => { + this.compare('view', {x, y, width, height}, this.state.viewLayout); + if (typeof next === 'function') { + next(); + } else if (!this.state.didAnimation) { + // Trigger first state change after onLayout fires + this.animateViewLayout(); + this.state.didAnimation = true; + } + }); + this.refs.txt.measure((x, y, width, height) => { + this.compare('txt', {x, y, width, height}, this.state.textLayout); + }); + this.refs.img.measure((x, y, width, height) => { + this.compare('img', {x, y, width, height}, this.state.imageLayout); + }); + }, + compare: function(node: string, measured: any, onLayout: any): void { + if (deepDiffer(measured, onLayout)) { + var data = {measured, onLayout}; + throw new Error( + node + ' onLayout mismatch with measure ' + + JSON.stringify(data, null, ' ') + ); + } + }, + onViewLayout: function(e: LayoutEvent) { + debug('received view layout event\n', e.nativeEvent); + this.setState({viewLayout: e.nativeEvent.layout}, this.checkLayout); + }, + onTextLayout: function(e: LayoutEvent) { + debug('received text layout event\n', e.nativeEvent); + this.setState({textLayout: e.nativeEvent.layout}, this.checkLayout); + }, + onImageLayout: function(e: LayoutEvent) { + debug('received image layout event\n', e.nativeEvent); + this.setState({imageLayout: e.nativeEvent.layout}, this.checkLayout); + }, + render: function() { + var viewStyle = [styles.view, this.state.viewStyle]; + var textLayout = this.state.textLayout || {width: '?', height: '?'}; + var imageLayout = this.state.imageLayout || {x: '?', y: '?'}; + return ( + + + + + ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'} + + + A simple piece of text.{this.state.extraText} + + + {'\n'} + Text w/h: {textLayout.width}/{textLayout.height + '\n'} + Image x/y: {imageLayout.x}/{imageLayout.y} + + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + margin: 40, + }, + view: { + margin: 20, + padding: 12, + borderColor: 'black', + borderWidth: 0.5, + backgroundColor: 'transparent', + }, + text: { + alignSelf: 'flex-start', + borderColor: 'rgba(0, 0, 255, 0.2)', + borderWidth: 0.5, + }, + image: { + width: 50, + height: 50, + marginBottom: 10, + alignSelf: 'center', + }, +}); + +module.exports = LayoutEventsTest; diff --git a/Libraries/ART/ART.xcodeproj/project.pbxproj b/Libraries/ART/ART.xcodeproj/project.pbxproj new file mode 100644 index 00000000000000..c3255c9656a9da --- /dev/null +++ b/Libraries/ART/ART.xcodeproj/project.pbxproj @@ -0,0 +1,371 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0CF68B051AF0549300FF9E5C /* ARTGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68ADE1AF0549300FF9E5C /* ARTGroup.m */; }; + 0CF68B061AF0549300FF9E5C /* ARTNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE01AF0549300FF9E5C /* ARTNode.m */; }; + 0CF68B071AF0549300FF9E5C /* ARTRenderable.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE21AF0549300FF9E5C /* ARTRenderable.m */; }; + 0CF68B081AF0549300FF9E5C /* ARTShape.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE41AF0549300FF9E5C /* ARTShape.m */; }; + 0CF68B091AF0549300FF9E5C /* ARTSurfaceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE61AF0549300FF9E5C /* ARTSurfaceView.m */; }; + 0CF68B0A1AF0549300FF9E5C /* ARTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AE81AF0549300FF9E5C /* ARTText.m */; }; + 0CF68B0B1AF0549300FF9E5C /* ARTBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AEC1AF0549300FF9E5C /* ARTBrush.m */; }; + 0CF68B0C1AF0549300FF9E5C /* ARTLinearGradient.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AEE1AF0549300FF9E5C /* ARTLinearGradient.m */; }; + 0CF68B0D1AF0549300FF9E5C /* ARTPattern.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF01AF0549300FF9E5C /* ARTPattern.m */; }; + 0CF68B0E1AF0549300FF9E5C /* ARTRadialGradient.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF21AF0549300FF9E5C /* ARTRadialGradient.m */; }; + 0CF68B0F1AF0549300FF9E5C /* ARTSolidColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF41AF0549300FF9E5C /* ARTSolidColor.m */; }; + 0CF68B101AF0549300FF9E5C /* RCTConvert+ART.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AF71AF0549300FF9E5C /* RCTConvert+ART.m */; }; + 0CF68B111AF0549300FF9E5C /* ARTGroupManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AFA1AF0549300FF9E5C /* ARTGroupManager.m */; }; + 0CF68B121AF0549300FF9E5C /* ARTNodeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AFC1AF0549300FF9E5C /* ARTNodeManager.m */; }; + 0CF68B131AF0549300FF9E5C /* ARTRenderableManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68AFE1AF0549300FF9E5C /* ARTRenderableManager.m */; }; + 0CF68B141AF0549300FF9E5C /* ARTShapeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68B001AF0549300FF9E5C /* ARTShapeManager.m */; }; + 0CF68B151AF0549300FF9E5C /* ARTSurfaceViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68B021AF0549300FF9E5C /* ARTSurfaceViewManager.m */; }; + 0CF68B161AF0549300FF9E5C /* ARTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CF68B041AF0549300FF9E5C /* ARTTextManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0CF68ABF1AF0540F00FF9E5C /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0CF68AC11AF0540F00FF9E5C /* libART.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libART.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 0CF68ADB1AF0549300FF9E5C /* ARTCGFloatArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTCGFloatArray.h; sourceTree = ""; }; + 0CF68ADC1AF0549300FF9E5C /* ARTContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTContainer.h; sourceTree = ""; }; + 0CF68ADD1AF0549300FF9E5C /* ARTGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTGroup.h; sourceTree = ""; }; + 0CF68ADE1AF0549300FF9E5C /* ARTGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTGroup.m; sourceTree = ""; }; + 0CF68ADF1AF0549300FF9E5C /* ARTNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTNode.h; sourceTree = ""; }; + 0CF68AE01AF0549300FF9E5C /* ARTNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTNode.m; sourceTree = ""; }; + 0CF68AE11AF0549300FF9E5C /* ARTRenderable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTRenderable.h; sourceTree = ""; }; + 0CF68AE21AF0549300FF9E5C /* ARTRenderable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTRenderable.m; sourceTree = ""; }; + 0CF68AE31AF0549300FF9E5C /* ARTShape.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTShape.h; sourceTree = ""; }; + 0CF68AE41AF0549300FF9E5C /* ARTShape.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTShape.m; sourceTree = ""; }; + 0CF68AE51AF0549300FF9E5C /* ARTSurfaceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTSurfaceView.h; sourceTree = ""; }; + 0CF68AE61AF0549300FF9E5C /* ARTSurfaceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTSurfaceView.m; sourceTree = ""; }; + 0CF68AE71AF0549300FF9E5C /* ARTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTText.h; sourceTree = ""; }; + 0CF68AE81AF0549300FF9E5C /* ARTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTText.m; sourceTree = ""; }; + 0CF68AE91AF0549300FF9E5C /* ARTTextFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTTextFrame.h; sourceTree = ""; }; + 0CF68AEB1AF0549300FF9E5C /* ARTBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTBrush.h; sourceTree = ""; }; + 0CF68AEC1AF0549300FF9E5C /* ARTBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTBrush.m; sourceTree = ""; }; + 0CF68AED1AF0549300FF9E5C /* ARTLinearGradient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTLinearGradient.h; sourceTree = ""; }; + 0CF68AEE1AF0549300FF9E5C /* ARTLinearGradient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTLinearGradient.m; sourceTree = ""; }; + 0CF68AEF1AF0549300FF9E5C /* ARTPattern.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTPattern.h; sourceTree = ""; }; + 0CF68AF01AF0549300FF9E5C /* ARTPattern.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTPattern.m; sourceTree = ""; }; + 0CF68AF11AF0549300FF9E5C /* ARTRadialGradient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTRadialGradient.h; sourceTree = ""; }; + 0CF68AF21AF0549300FF9E5C /* ARTRadialGradient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTRadialGradient.m; sourceTree = ""; }; + 0CF68AF31AF0549300FF9E5C /* ARTSolidColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTSolidColor.h; sourceTree = ""; }; + 0CF68AF41AF0549300FF9E5C /* ARTSolidColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTSolidColor.m; sourceTree = ""; }; + 0CF68AF61AF0549300FF9E5C /* RCTConvert+ART.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+ART.h"; sourceTree = ""; }; + 0CF68AF71AF0549300FF9E5C /* RCTConvert+ART.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+ART.m"; sourceTree = ""; }; + 0CF68AF91AF0549300FF9E5C /* ARTGroupManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTGroupManager.h; sourceTree = ""; }; + 0CF68AFA1AF0549300FF9E5C /* ARTGroupManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTGroupManager.m; sourceTree = ""; }; + 0CF68AFB1AF0549300FF9E5C /* ARTNodeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTNodeManager.h; sourceTree = ""; }; + 0CF68AFC1AF0549300FF9E5C /* ARTNodeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTNodeManager.m; sourceTree = ""; }; + 0CF68AFD1AF0549300FF9E5C /* ARTRenderableManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTRenderableManager.h; sourceTree = ""; }; + 0CF68AFE1AF0549300FF9E5C /* ARTRenderableManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTRenderableManager.m; sourceTree = ""; }; + 0CF68AFF1AF0549300FF9E5C /* ARTShapeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTShapeManager.h; sourceTree = ""; }; + 0CF68B001AF0549300FF9E5C /* ARTShapeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTShapeManager.m; sourceTree = ""; }; + 0CF68B011AF0549300FF9E5C /* ARTSurfaceViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTSurfaceViewManager.h; sourceTree = ""; }; + 0CF68B021AF0549300FF9E5C /* ARTSurfaceViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTSurfaceViewManager.m; sourceTree = ""; }; + 0CF68B031AF0549300FF9E5C /* ARTTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTTextManager.h; sourceTree = ""; }; + 0CF68B041AF0549300FF9E5C /* ARTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTTextManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0CF68ABE1AF0540F00FF9E5C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0CF68AB81AF0540F00FF9E5C = { + isa = PBXGroup; + children = ( + 0CF68ADB1AF0549300FF9E5C /* ARTCGFloatArray.h */, + 0CF68ADC1AF0549300FF9E5C /* ARTContainer.h */, + 0CF68ADD1AF0549300FF9E5C /* ARTGroup.h */, + 0CF68ADE1AF0549300FF9E5C /* ARTGroup.m */, + 0CF68ADF1AF0549300FF9E5C /* ARTNode.h */, + 0CF68AE01AF0549300FF9E5C /* ARTNode.m */, + 0CF68AE11AF0549300FF9E5C /* ARTRenderable.h */, + 0CF68AE21AF0549300FF9E5C /* ARTRenderable.m */, + 0CF68AE31AF0549300FF9E5C /* ARTShape.h */, + 0CF68AE41AF0549300FF9E5C /* ARTShape.m */, + 0CF68AE51AF0549300FF9E5C /* ARTSurfaceView.h */, + 0CF68AE61AF0549300FF9E5C /* ARTSurfaceView.m */, + 0CF68AE71AF0549300FF9E5C /* ARTText.h */, + 0CF68AE81AF0549300FF9E5C /* ARTText.m */, + 0CF68AE91AF0549300FF9E5C /* ARTTextFrame.h */, + 0CF68AEA1AF0549300FF9E5C /* Brushes */, + 0CF68AF61AF0549300FF9E5C /* RCTConvert+ART.h */, + 0CF68AF71AF0549300FF9E5C /* RCTConvert+ART.m */, + 0CF68AF81AF0549300FF9E5C /* ViewManagers */, + 0CF68AC21AF0540F00FF9E5C /* Products */, + ); + sourceTree = ""; + }; + 0CF68AC21AF0540F00FF9E5C /* Products */ = { + isa = PBXGroup; + children = ( + 0CF68AC11AF0540F00FF9E5C /* libART.a */, + ); + name = Products; + sourceTree = ""; + }; + 0CF68AEA1AF0549300FF9E5C /* Brushes */ = { + isa = PBXGroup; + children = ( + 0CF68AEB1AF0549300FF9E5C /* ARTBrush.h */, + 0CF68AEC1AF0549300FF9E5C /* ARTBrush.m */, + 0CF68AED1AF0549300FF9E5C /* ARTLinearGradient.h */, + 0CF68AEE1AF0549300FF9E5C /* ARTLinearGradient.m */, + 0CF68AEF1AF0549300FF9E5C /* ARTPattern.h */, + 0CF68AF01AF0549300FF9E5C /* ARTPattern.m */, + 0CF68AF11AF0549300FF9E5C /* ARTRadialGradient.h */, + 0CF68AF21AF0549300FF9E5C /* ARTRadialGradient.m */, + 0CF68AF31AF0549300FF9E5C /* ARTSolidColor.h */, + 0CF68AF41AF0549300FF9E5C /* ARTSolidColor.m */, + ); + path = Brushes; + sourceTree = ""; + }; + 0CF68AF81AF0549300FF9E5C /* ViewManagers */ = { + isa = PBXGroup; + children = ( + 0CF68AF91AF0549300FF9E5C /* ARTGroupManager.h */, + 0CF68AFA1AF0549300FF9E5C /* ARTGroupManager.m */, + 0CF68AFB1AF0549300FF9E5C /* ARTNodeManager.h */, + 0CF68AFC1AF0549300FF9E5C /* ARTNodeManager.m */, + 0CF68AFD1AF0549300FF9E5C /* ARTRenderableManager.h */, + 0CF68AFE1AF0549300FF9E5C /* ARTRenderableManager.m */, + 0CF68AFF1AF0549300FF9E5C /* ARTShapeManager.h */, + 0CF68B001AF0549300FF9E5C /* ARTShapeManager.m */, + 0CF68B011AF0549300FF9E5C /* ARTSurfaceViewManager.h */, + 0CF68B021AF0549300FF9E5C /* ARTSurfaceViewManager.m */, + 0CF68B031AF0549300FF9E5C /* ARTTextManager.h */, + 0CF68B041AF0549300FF9E5C /* ARTTextManager.m */, + ); + path = ViewManagers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0CF68AC01AF0540F00FF9E5C /* ART */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0CF68AD51AF0540F00FF9E5C /* Build configuration list for PBXNativeTarget "ART" */; + buildPhases = ( + 0CF68ABD1AF0540F00FF9E5C /* Sources */, + 0CF68ABE1AF0540F00FF9E5C /* Frameworks */, + 0CF68ABF1AF0540F00FF9E5C /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ART; + productName = ART; + productReference = 0CF68AC11AF0540F00FF9E5C /* libART.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0CF68AB91AF0540F00FF9E5C /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0620; + TargetAttributes = { + 0CF68AC01AF0540F00FF9E5C = { + CreatedOnToolsVersion = 6.2; + }; + }; + }; + buildConfigurationList = 0CF68ABC1AF0540F00FF9E5C /* Build configuration list for PBXProject "ART" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 0CF68AB81AF0540F00FF9E5C; + productRefGroup = 0CF68AC21AF0540F00FF9E5C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0CF68AC01AF0540F00FF9E5C /* ART */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 0CF68ABD1AF0540F00FF9E5C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CF68B161AF0549300FF9E5C /* ARTTextManager.m in Sources */, + 0CF68B111AF0549300FF9E5C /* ARTGroupManager.m in Sources */, + 0CF68B0D1AF0549300FF9E5C /* ARTPattern.m in Sources */, + 0CF68B0A1AF0549300FF9E5C /* ARTText.m in Sources */, + 0CF68B121AF0549300FF9E5C /* ARTNodeManager.m in Sources */, + 0CF68B051AF0549300FF9E5C /* ARTGroup.m in Sources */, + 0CF68B131AF0549300FF9E5C /* ARTRenderableManager.m in Sources */, + 0CF68B091AF0549300FF9E5C /* ARTSurfaceView.m in Sources */, + 0CF68B0E1AF0549300FF9E5C /* ARTRadialGradient.m in Sources */, + 0CF68B151AF0549300FF9E5C /* ARTSurfaceViewManager.m in Sources */, + 0CF68B081AF0549300FF9E5C /* ARTShape.m in Sources */, + 0CF68B071AF0549300FF9E5C /* ARTRenderable.m in Sources */, + 0CF68B101AF0549300FF9E5C /* RCTConvert+ART.m in Sources */, + 0CF68B061AF0549300FF9E5C /* ARTNode.m in Sources */, + 0CF68B0F1AF0549300FF9E5C /* ARTSolidColor.m in Sources */, + 0CF68B0C1AF0549300FF9E5C /* ARTLinearGradient.m in Sources */, + 0CF68B0B1AF0549300FF9E5C /* ARTBrush.m in Sources */, + 0CF68B141AF0549300FF9E5C /* ARTShapeManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 0CF68AD31AF0540F00FF9E5C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 0CF68AD41AF0540F00FF9E5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0CF68AD61AF0540F00FF9E5C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 0CF68AD71AF0540F00FF9E5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0CF68ABC1AF0540F00FF9E5C /* Build configuration list for PBXProject "ART" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0CF68AD31AF0540F00FF9E5C /* Debug */, + 0CF68AD41AF0540F00FF9E5C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0CF68AD51AF0540F00FF9E5C /* Build configuration list for PBXNativeTarget "ART" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0CF68AD61AF0540F00FF9E5C /* Debug */, + 0CF68AD71AF0540F00FF9E5C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0CF68AB91AF0540F00FF9E5C /* Project object */; +} diff --git a/Libraries/ART/ARTCGFloatArray.h b/Libraries/ART/ARTCGFloatArray.h new file mode 100644 index 00000000000000..9d748549973d30 --- /dev/null +++ b/Libraries/ART/ARTCGFloatArray.h @@ -0,0 +1,20 @@ +/** + * 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. + */ + +// A little helper to make sure we have the right memory allocation ready for use. +// We assume that we will only this in one place so no reference counting is necessary. +// Needs to be freed when dealloced. + +// This is fragile since this relies on these values not getting reused. Consider +// wrapping these in an Obj-C class or some ARC hackery to get refcounting. + +typedef struct { + size_t count; + CGFloat *array; +} ARTCGFloatArray; diff --git a/Libraries/ART/ARTContainer.h b/Libraries/ART/ARTContainer.h new file mode 100644 index 00000000000000..d83f7ae1a559f8 --- /dev/null +++ b/Libraries/ART/ARTContainer.h @@ -0,0 +1,18 @@ +/** + * 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 + +@protocol ARTContainer + +// This is used as a hook for child to mark it's parent as dirty. +// This bubbles up to the root which gets marked as dirty. +- (void)invalidate; + +@end diff --git a/Libraries/ART/ARTGroup.h b/Libraries/ART/ARTGroup.h new file mode 100644 index 00000000000000..15a8b643bb86fb --- /dev/null +++ b/Libraries/ART/ARTGroup.h @@ -0,0 +1,17 @@ +/** + * 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 + +#import "ARTContainer.h" +#import "ARTNode.h" + +@interface ARTGroup : ARTNode + +@end diff --git a/Libraries/ART/ARTGroup.m b/Libraries/ART/ARTGroup.m new file mode 100644 index 00000000000000..9ecbf8ee821b84 --- /dev/null +++ b/Libraries/ART/ARTGroup.m @@ -0,0 +1,23 @@ +/** + * 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 "ARTGroup.h" + +@implementation ARTGroup + +- (void)renderLayerTo:(CGContextRef)context +{ +// TO-DO: Clipping rectangle + + for (ARTNode *node in self.subviews) { + [node renderTo:context]; + } +} + +@end diff --git a/Libraries/ART/ARTNode.h b/Libraries/ART/ARTNode.h new file mode 100644 index 00000000000000..511c09a5a6aae2 --- /dev/null +++ b/Libraries/ART/ARTNode.h @@ -0,0 +1,33 @@ +/** + * 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 +#import + +/** + * ART nodes are implemented as empty UIViews but this is just an implementation detail to fit + * into the existing view management. They should also be shadow views and painted on a background + * thread. + */ + +@interface ARTNode : UIView + +@property (nonatomic, assign) CGFloat opacity; + +- (void)invalidate; +- (void)renderTo:(CGContextRef)context; + +/** + * renderTo will take opacity into account and draw renderLayerTo off-screen if there is opacity + * specified, then composite that onto the context. renderLayerTo always draws at opacity=1. + * @abstract + */ +- (void)renderLayerTo:(CGContextRef)context; + +@end diff --git a/Libraries/ART/ARTNode.m b/Libraries/ART/ARTNode.m new file mode 100644 index 00000000000000..d23d5880a95677 --- /dev/null +++ b/Libraries/ART/ARTNode.m @@ -0,0 +1,76 @@ +/** + * 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 "ARTNode.h" + +#import "ARTContainer.h" + +@implementation ARTNode + +- (void)insertSubview:(UIView *)subview atIndex:(NSInteger)index +{ + [self invalidate]; + [super insertSubview:subview atIndex:index]; +} + +- (void)removeFromSuperview +{ + [self invalidate]; + [super removeFromSuperview]; +} + +- (void)setOpacity:(CGFloat)opacity +{ + [self invalidate]; + _opacity = opacity; +} + +- (void)setTransform:(CGAffineTransform)transform +{ + [self invalidate]; + super.transform = transform; +} + +- (void)invalidate +{ + id container = (id)self.superview; + [container invalidate]; +} + +- (void)renderTo:(CGContextRef)context +{ + if (self.opacity <= 0) { + // Nothing to paint + return; + } + if (self.opacity >= 1) { + // Just paint at full opacity + CGContextSaveGState(context); + CGContextConcatCTM(context, self.transform); + CGContextSetAlpha(context, 1); + [self renderLayerTo:context]; + CGContextRestoreGState(context); + return; + } + // This needs to be painted on a layer before being composited. + CGContextSaveGState(context); + CGContextConcatCTM(context, self.transform); + CGContextSetAlpha(context, self.opacity); + CGContextBeginTransparencyLayer(context, NULL); + [self renderLayerTo:context]; + CGContextEndTransparencyLayer(context); + CGContextRestoreGState(context); +} + +- (void)renderLayerTo:(CGContextRef)context +{ + // abstract +} + +@end diff --git a/Libraries/ART/ARTRenderable.h b/Libraries/ART/ARTRenderable.h new file mode 100644 index 00000000000000..8eae9c25ae4a70 --- /dev/null +++ b/Libraries/ART/ARTRenderable.h @@ -0,0 +1,25 @@ +/** + * 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 + +#import "ARTBrush.h" +#import "ARTCGFloatArray.h" +#import "ARTNode.h" + +@interface ARTRenderable : ARTNode + +@property (nonatomic, strong) ARTBrush *fill; +@property (nonatomic, assign) CGColorRef stroke; +@property (nonatomic, assign) CGFloat strokeWidth; +@property (nonatomic, assign) CGLineCap strokeCap; +@property (nonatomic, assign) CGLineJoin strokeJoin; +@property (nonatomic, assign) ARTCGFloatArray strokeDash; + +@end diff --git a/Libraries/ART/ARTRenderable.m b/Libraries/ART/ARTRenderable.m new file mode 100644 index 00000000000000..7ba9a9a6073915 --- /dev/null +++ b/Libraries/ART/ARTRenderable.m @@ -0,0 +1,89 @@ +/** + * 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 "ARTRenderable.h" + +@implementation ARTRenderable + +- (void)setFill:(ARTBrush *)fill +{ + [self invalidate]; + _fill = fill; +} + +- (void)setStroke:(CGColorRef)stroke +{ + if (stroke == _stroke) { + return; + } + [self invalidate]; + CGColorRelease(_stroke); + _stroke = CGColorRetain(stroke); +} + +- (void)setStrokeWidth:(CGFloat)strokeWidth +{ + [self invalidate]; + _strokeWidth = strokeWidth; +} + +- (void)setStrokeCap:(CGLineCap)strokeCap +{ + [self invalidate]; + _strokeCap = strokeCap; +} + +- (void)setStrokeJoin:(CGLineJoin)strokeJoin +{ + [self invalidate]; + _strokeJoin = strokeJoin; +} + +- (void)setStrokeDash:(ARTCGFloatArray)strokeDash +{ + if (strokeDash.array == _strokeDash.array) { + return; + } + if (_strokeDash.array) { + free(_strokeDash.array); + } + [self invalidate]; + _strokeDash = strokeDash; +} + +- (void)dealloc +{ + CGColorRelease(_stroke); + if (_strokeDash.array) { + free(_strokeDash.array); + } +} + +- (void)renderTo:(CGContextRef)context +{ + if (self.opacity <= 0 || self.opacity >= 1 || (self.fill && self.stroke)) { + // If we have both fill and stroke, we will need to paint this using normal compositing + [super renderTo: context]; + return; + } + // This is a terminal with only one painting. Therefore we don't need to paint this + // off-screen. We can just composite it straight onto the buffer. + CGContextSaveGState(context); + CGContextConcatCTM(context, self.transform); + CGContextSetAlpha(context, self.opacity); + [self renderLayerTo:context]; + CGContextRestoreGState(context); +} + +- (void)renderLayerTo:(CGContextRef)context +{ + // abstract +} + +@end diff --git a/Libraries/ART/ARTSerializablePath.js b/Libraries/ART/ARTSerializablePath.js new file mode 100644 index 00000000000000..2df8ff6bb83bcd --- /dev/null +++ b/Libraries/ART/ARTSerializablePath.js @@ -0,0 +1,77 @@ +/** + * 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. + * + * @providesModule ARTSerializablePath + */ + +"use strict"; + +// TODO: Move this into an ART mode called "serialized" or something + +var Class = require('art/core/class.js'); +var Path = require('art/core/path.js'); + +var MOVE_TO = 0; +var CLOSE = 1; +var LINE_TO = 2; +var CURVE_TO = 3; +var ARC = 4; + +var SerializablePath = Class(Path, { + + initialize: function(path) { + this.reset(); + if (path instanceof SerializablePath) { + this.path = path.path.slice(0); + } else if (path) { + if (path.applyToPath) { + path.applyToPath(this); + } else { + this.push(path); + } + } + }, + + onReset: function() { + this.path = []; + }, + + onMove: function(sx, sy, x, y) { + this.path.push(MOVE_TO, x, y); + }, + + onLine: function(sx, sy, x, y) { + this.path.push(LINE_TO, x, y); + }, + + onBezierCurve: function(sx, sy, p1x, p1y, p2x, p2y, x, y) { + this.path.push(CURVE_TO, p1x, p1y, p2x, p2y, x, y); + }, + + _arcToBezier: Path.prototype.onArc, + + onArc: function(sx, sy, ex, ey, cx, cy, rx, ry, sa, ea, ccw, rotation) { + if (rx !== ry || rotation) { + return this._arcToBezier( + sx, sy, ex, ey, cx, cy, rx, ry, sa, ea, ccw, rotation + ); + } + this.path.push(ARC, cx, cy, rx, sa, ea, ccw ? 0 : 1); + }, + + onClose: function() { + this.path.push(CLOSE); + }, + + toJSON: function() { + return this.path; + } + +}); + +module.exports = SerializablePath; diff --git a/Libraries/ART/ARTShape.h b/Libraries/ART/ARTShape.h new file mode 100644 index 00000000000000..7d13c268f6e802 --- /dev/null +++ b/Libraries/ART/ARTShape.h @@ -0,0 +1,18 @@ +/** + * 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 + +#import "ARTRenderable.h" + +@interface ARTShape : ARTRenderable + +@property (nonatomic, assign) CGPathRef d; + +@end diff --git a/Libraries/ART/ARTShape.m b/Libraries/ART/ARTShape.m new file mode 100644 index 00000000000000..c07d68e62aa4a3 --- /dev/null +++ b/Libraries/ART/ARTShape.m @@ -0,0 +1,68 @@ +/** + * 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 "ARTShape.h" + +@implementation ARTShape + +- (void)setD:(CGPathRef)d +{ + if (d == _d) { + return; + } + [self invalidate]; + CGPathRelease(_d); + _d = CGPathRetain(d); +} + +- (void)dealloc +{ + CGPathRelease(_d); +} + +- (void)renderLayerTo:(CGContextRef)context +{ + if ((!self.fill && !self.stroke) || !self.d) { + return; + } + + CGPathDrawingMode mode = kCGPathStroke; + if (self.fill) { + if ([self.fill applyFillColor:context]) { + mode = kCGPathFill; + } else { + CGContextSaveGState(context); + CGContextAddPath(context, self.d); + CGContextClip(context); + [self.fill paint:context]; + CGContextRestoreGState(context); + if (!self.stroke) { + return; + } + } + } + if (self.stroke) { + CGContextSetStrokeColorWithColor(context, self.stroke); + CGContextSetLineWidth(context, self.strokeWidth); + CGContextSetLineCap(context, self.strokeCap); + CGContextSetLineJoin(context, self.strokeJoin); + ARTCGFloatArray dash = self.strokeDash; + if (dash.count) { + CGContextSetLineDash(context, 0, dash.array, dash.count); + } + if (mode == kCGPathFill) { + mode = kCGPathFillStroke; + } + } + + CGContextAddPath(context, self.d); + CGContextDrawPath(context, mode); +} + +@end diff --git a/Libraries/ART/ARTSurfaceView.h b/Libraries/ART/ARTSurfaceView.h new file mode 100644 index 00000000000000..8be8d95040c146 --- /dev/null +++ b/Libraries/ART/ARTSurfaceView.h @@ -0,0 +1,16 @@ +/** + * 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 + +#import "ARTContainer.h" + +@interface ARTSurfaceView : UIView + +@end diff --git a/Libraries/ART/ARTSurfaceView.m b/Libraries/ART/ARTSurfaceView.m new file mode 100644 index 00000000000000..8949e43c349d05 --- /dev/null +++ b/Libraries/ART/ARTSurfaceView.m @@ -0,0 +1,30 @@ +/** + * 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 "ARTSurfaceView.h" + +#import "ARTNode.h" +#import "RCTLog.h" + +@implementation ARTSurfaceView + +- (void)invalidate +{ + [self setNeedsDisplay]; +} + +- (void)drawRect:(CGRect)rect +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + for (ARTNode *node in self.subviews) { + [node renderTo:context]; + } +} + +@end diff --git a/Libraries/ART/ARTText.h b/Libraries/ART/ARTText.h new file mode 100644 index 00000000000000..ee976e329a46e8 --- /dev/null +++ b/Libraries/ART/ARTText.h @@ -0,0 +1,20 @@ +/** + * 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 + +#import "ARTRenderable.h" +#import "ARTTextFrame.h" + +@interface ARTText : ARTRenderable + +@property (nonatomic, assign) CTTextAlignment alignment; +@property (nonatomic, assign) ARTTextFrame textFrame; + +@end diff --git a/Libraries/ART/ARTText.m b/Libraries/ART/ARTText.m new file mode 100644 index 00000000000000..7c8a570270b7ff --- /dev/null +++ b/Libraries/ART/ARTText.m @@ -0,0 +1,127 @@ +/** + * 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 "ARTText.h" + +#import + +@implementation ARTText + +- (void)setAlignment:(CTTextAlignment)alignment +{ + [self invalidate]; + _alignment = alignment; +} + +- (void)setTextFrame:(ARTTextFrame)frame +{ + if (frame.lines != _textFrame.lines && _textFrame.count) { + // We must release each line before overriding the old one + for (int i = 0; i < _textFrame.count; i++) { + CFRelease(_textFrame.lines[0]); + } + free(_textFrame.lines); + free(_textFrame.widths); + } + [self invalidate]; + _textFrame = frame; +} + +- (void)dealloc +{ + if (_textFrame.count) { + // We must release each line before freeing up this struct + for (int i = 0; i < _textFrame.count; i++) { + CFRelease(_textFrame.lines[0]); + } + free(_textFrame.lines); + free(_textFrame.widths); + } +} + +- (void)renderLayerTo:(CGContextRef)context +{ + ARTTextFrame frame = self.textFrame; + + if ((!self.fill && !self.stroke) || !frame.count) { + return; + } + + // to-do: draw along a path + + CGTextDrawingMode mode = kCGTextStroke; + if (self.fill) { + if ([self.fill applyFillColor:context]) { + mode = kCGTextFill; + } else { + + for (int i = 0; i < frame.count; i++) { + CGContextSaveGState(context); + // Inverse the coordinate space since CoreText assumes a bottom-up coordinate space + CGContextScaleCTM(context, 1.0, -1.0); + CGContextSetTextDrawingMode(context, kCGTextClip); + [self renderLineTo:context atIndex:i]; + // Inverse the coordinate space back to the original before filling + CGContextScaleCTM(context, 1.0, -1.0); + [self.fill paint:context]; + // Restore the state so that the next line can be clipped separately + CGContextRestoreGState(context); + } + + if (!self.stroke) { + return; + } + } + } + if (self.stroke) { + CGContextSetStrokeColorWithColor(context, self.stroke); + CGContextSetLineWidth(context, self.strokeWidth); + CGContextSetLineCap(context, self.strokeCap); + CGContextSetLineJoin(context, self.strokeJoin); + ARTCGFloatArray dash = self.strokeDash; + if (dash.count) { + CGContextSetLineDash(context, 0, dash.array, dash.count); + } + if (mode == kCGTextFill) { + mode = kCGTextFillStroke; + } + } + + CGContextSetTextDrawingMode(context, mode); + + // Inverse the coordinate space since CoreText assumes a bottom-up coordinate space + CGContextScaleCTM(context, 1.0, -1.0); + for (int i = 0; i < frame.count; i++) { + [self renderLineTo:context atIndex:i]; + } +} + +- (void)renderLineTo:(CGContextRef)context atIndex:(int)index +{ + ARTTextFrame frame = self.textFrame; + CGFloat shift; + switch (self.alignment) { + case kCTTextAlignmentRight: + shift = frame.widths[index]; + break; + case kCTTextAlignmentCenter: + shift = (frame.widths[index] / 2); + break; + default: + shift = 0; + break; + } + // We should consider snapping this shift to device pixels to improve rendering quality + // when a line has subpixel width. + CGContextSetTextPosition(context, -shift, -frame.baseLine - frame.lineHeight * index); + CTLineRef line = frame.lines[index]; + CTLineDraw(line, context); +} + +@end diff --git a/Libraries/ART/ARTTextFrame.h b/Libraries/ART/ARTTextFrame.h new file mode 100644 index 00000000000000..1f6b557bfc77af --- /dev/null +++ b/Libraries/ART/ARTTextFrame.h @@ -0,0 +1,25 @@ +/** + * 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 + +// A little helper to make sure we have a set of lines including width ready for use. +// We assume that we will only this in one place so no reference counting is necessary. +// Needs to be freed when dealloced. + +// This is fragile since this relies on these values not getting reused. Consider +// wrapping these in an Obj-C class or some ARC hackery to get refcounting. + +typedef struct { + size_t count; + CGFloat baseLine; // Distance from the origin to the base line of the first line + CGFloat lineHeight; // Distance between lines + CTLineRef *lines; + CGFloat *widths; // Width of each line +} ARTTextFrame; diff --git a/Libraries/ART/Brushes/ARTBrush.h b/Libraries/ART/Brushes/ARTBrush.h new file mode 100644 index 00000000000000..05020dd7bafa26 --- /dev/null +++ b/Libraries/ART/Brushes/ARTBrush.h @@ -0,0 +1,35 @@ +/** + * 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 +#import + +@interface ARTBrush : NSObject + +/* @abstract */ +- (instancetype)initWithArray:(NSArray *)data NS_DESIGNATED_INITIALIZER; + +/** + * For certain brushes we can fast path a combined fill and stroke. + * For those brushes we override applyFillColor which sets the fill + * color to be used by those batch paints. Those return YES. + * We can't batch gradient painting in CoreGraphics, so those will + * return NO and paint gets called instead. + * @abstract + */ +- (BOOL)applyFillColor:(CGContextRef)context; + +/** + * paint fills the context with a brush. The context is assumed to + * be clipped. + * @abstract + */ +- (void)paint:(CGContextRef)context; + +@end diff --git a/Libraries/ART/Brushes/ARTBrush.m b/Libraries/ART/Brushes/ARTBrush.m new file mode 100644 index 00000000000000..efc82dea35bb25 --- /dev/null +++ b/Libraries/ART/Brushes/ARTBrush.m @@ -0,0 +1,29 @@ +/** + * 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 "ARTBrush.h" + +@implementation ARTBrush + +- (instancetype)initWithArray:(NSArray *)data +{ + return [super init]; +} + +- (BOOL)applyFillColor:(CGContextRef)context +{ + return NO; +} + +- (void)paint:(CGContextRef)context +{ + // abstract +} + +@end diff --git a/Libraries/ART/Brushes/ARTLinearGradient.h b/Libraries/ART/Brushes/ARTLinearGradient.h new file mode 100644 index 00000000000000..d7ff2e5684458f --- /dev/null +++ b/Libraries/ART/Brushes/ARTLinearGradient.h @@ -0,0 +1,14 @@ +/** + * 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 "ARTBrush.h" + +@interface ARTLinearGradient : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTLinearGradient.m b/Libraries/ART/Brushes/ARTLinearGradient.m new file mode 100644 index 00000000000000..8793ff07bf71ff --- /dev/null +++ b/Libraries/ART/Brushes/ARTLinearGradient.m @@ -0,0 +1,49 @@ +/** + * 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 "ARTLinearGradient.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTLinearGradient +{ + CGGradientRef _gradient; + CGPoint _startPoint; + CGPoint _endPoint; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + if (array.count < 5) { + RCTLogError(@"-[%@ %@] expects 5 elements, received %@", + self.class, NSStringFromSelector(_cmd), array); + return nil; + } + _startPoint = [RCTConvert CGPoint:array offset:1]; + _endPoint = [RCTConvert CGPoint:array offset:3]; + _gradient = CGGradientRetain([RCTConvert CGGradient:array offset:5]); + } + return self; +} + +- (void)dealloc +{ + CGGradientRelease(_gradient); +} + +- (void)paint:(CGContextRef)context +{ + CGGradientDrawingOptions extendOptions = + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; + CGContextDrawLinearGradient(context, _gradient, _startPoint, _endPoint, extendOptions); +} + +@end diff --git a/Libraries/ART/Brushes/ARTPattern.h b/Libraries/ART/Brushes/ARTPattern.h new file mode 100644 index 00000000000000..5f513ec60e2ae6 --- /dev/null +++ b/Libraries/ART/Brushes/ARTPattern.h @@ -0,0 +1,14 @@ +/** + * 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 "ARTBrush.h" + +@interface ARTPattern : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTPattern.m b/Libraries/ART/Brushes/ARTPattern.m new file mode 100644 index 00000000000000..07dd867001880f --- /dev/null +++ b/Libraries/ART/Brushes/ARTPattern.m @@ -0,0 +1,50 @@ +/** + * 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 "ARTPattern.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTPattern +{ + CGImageRef _image; + CGRect _rect; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + if (array.count < 6) { + RCTLogError(@"-[%@ %@] expects 6 elements, received %@", + self.class, NSStringFromSelector(_cmd), array); + return nil; + } + _image = CGImageRetain([RCTConvert CGImage:array[1]]); + _rect = [RCTConvert CGRect:array offset:2]; + } + return self; +} + +- (void)dealloc +{ + CGImageRelease(_image); +} + +// Note: This could use applyFillColor with a pattern. This could be more efficient but +// to do that, we need to calculate our own user space CTM. + +- (void)paint:(CGContextRef)context +{ + CGContextDrawTiledImage(context, _rect, _image); +} + + + +@end diff --git a/Libraries/ART/Brushes/ARTRadialGradient.h b/Libraries/ART/Brushes/ARTRadialGradient.h new file mode 100644 index 00000000000000..7f86d93058ce80 --- /dev/null +++ b/Libraries/ART/Brushes/ARTRadialGradient.h @@ -0,0 +1,14 @@ +/** + * 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 "ARTBrush.h" + +@interface ARTRadialGradient : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTRadialGradient.m b/Libraries/ART/Brushes/ARTRadialGradient.m new file mode 100644 index 00000000000000..b59b1736937a37 --- /dev/null +++ b/Libraries/ART/Brushes/ARTRadialGradient.m @@ -0,0 +1,56 @@ +/** + * 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 "ARTRadialGradient.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTRadialGradient +{ + CGGradientRef _gradient; + CGPoint _focusPoint; + CGPoint _centerPoint; + CGFloat _radius; + CGFloat _radiusRatio; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + if (array.count < 7) { + RCTLogError(@"-[%@ %@] expects 7 elements, received %@", + self.class, NSStringFromSelector(_cmd), array); + return nil; + } + _radius = [RCTConvert CGFloat:array[3]]; + _radiusRatio = [RCTConvert CGFloat:array[4]] / _radius; + _focusPoint.x = [RCTConvert CGFloat:array[1]]; + _focusPoint.y = [RCTConvert CGFloat:array[2]] / _radiusRatio; + _centerPoint.x = [RCTConvert CGFloat:array[5]]; + _centerPoint.y = [RCTConvert CGFloat:array[6]] / _radiusRatio; + _gradient = CGGradientRetain([RCTConvert CGGradient:array offset:7]); + } + return self; +} + +- (void)dealloc +{ + CGGradientRelease(_gradient); +} + +- (void)paint:(CGContextRef)context +{ + CGAffineTransform transform = CGAffineTransformMakeScale(1, _radiusRatio); + CGContextConcatCTM(context, transform); + CGGradientDrawingOptions extendOptions = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; + CGContextDrawRadialGradient(context, _gradient, _focusPoint, 0, _centerPoint, _radius, extendOptions); +} + +@end diff --git a/Libraries/ART/Brushes/ARTSolidColor.h b/Libraries/ART/Brushes/ARTSolidColor.h new file mode 100644 index 00000000000000..f212c735680f60 --- /dev/null +++ b/Libraries/ART/Brushes/ARTSolidColor.h @@ -0,0 +1,14 @@ +/** + * 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 "ARTBrush.h" + +@interface ARTSolidColor : ARTBrush + +@end diff --git a/Libraries/ART/Brushes/ARTSolidColor.m b/Libraries/ART/Brushes/ARTSolidColor.m new file mode 100644 index 00000000000000..229942ddec6490 --- /dev/null +++ b/Libraries/ART/Brushes/ARTSolidColor.m @@ -0,0 +1,39 @@ +/** + * 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 "ARTSolidColor.h" + +#import "RCTConvert+ART.h" +#import "RCTLog.h" + +@implementation ARTSolidColor +{ + CGColorRef _color; +} + +- (instancetype)initWithArray:(NSArray *)array +{ + if ((self = [super initWithArray:array])) { + _color = CGColorRetain([RCTConvert CGColor:array offset:1]); + } + return self; +} + +- (void)dealloc +{ + CGColorRelease(_color); +} + +- (BOOL)applyFillColor:(CGContextRef)context +{ + CGContextSetFillColorWithColor(context, _color); + return YES; +} + +@end diff --git a/Libraries/ART/RCTConvert+ART.h b/Libraries/ART/RCTConvert+ART.h new file mode 100644 index 00000000000000..3cbd0e787a24ea --- /dev/null +++ b/Libraries/ART/RCTConvert+ART.h @@ -0,0 +1,30 @@ +/** + * 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 + +#import "ARTBrush.h" +#import "ARTCGFloatArray.h" +#import "ARTTextFrame.h" +#import "RCTConvert.h" + +@interface RCTConvert (ART) + ++ (CGPathRef)CGPath:(id)json; ++ (CTTextAlignment)CTTextAlignment:(id)json; ++ (ARTTextFrame)ARTTextFrame:(id)json; ++ (ARTCGFloatArray)ARTCGFloatArray:(id)json; ++ (ARTBrush *)ARTBrush:(id)json; + ++ (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset; ++ (CGRect)CGRect:(id)json offset:(NSUInteger)offset; ++ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset; ++ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset; + +@end diff --git a/Libraries/ART/RCTConvert+ART.m b/Libraries/ART/RCTConvert+ART.m new file mode 100644 index 00000000000000..7a607a12c23691 --- /dev/null +++ b/Libraries/ART/RCTConvert+ART.m @@ -0,0 +1,223 @@ +/** + * 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 "RCTConvert+ART.h" + +#import "ARTLinearGradient.h" +#import "ARTPattern.h" +#import "ARTRadialGradient.h" +#import "ARTSolidColor.h" +#import "RCTLog.h" + +@implementation RCTConvert (ART) + ++ (CGPathRef)CGPath:(id)json +{ + NSArray *arr = [self NSNumberArray:json]; + + NSUInteger count = [arr count]; + +#define NEXT_VALUE [self double:arr[i++]] + + CGMutablePathRef path = CGPathCreateMutable(); + CGPathMoveToPoint(path, NULL, 0, 0); + + @try { + NSUInteger i = 0; + while (i < count) { + NSUInteger type = [arr[i++] unsignedIntegerValue]; + switch (type) { + case 0: + CGPathMoveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); + break; + case 1: + CGPathCloseSubpath(path); + break; + case 2: + CGPathAddLineToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); + break; + case 3: + CGPathAddCurveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE); + break; + case 4: + CGPathAddArc(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE == 0); + break; + default: + RCTLogError(@"Invalid CGPath type %zd at element %zd of %@", type, i, arr); + CGPathRelease(path); + return NULL; + } + } + } + @catch (NSException *exception) { + RCTLogError(@"Invalid CGPath format: %@", arr); + CGPathRelease(path); + return NULL; + } + + return (CGPathRef)CFAutorelease(path); +} + +RCT_ENUM_CONVERTER(CTTextAlignment, (@{ + @"auto": @(kCTTextAlignmentNatural), + @"left": @(kCTTextAlignmentLeft), + @"center": @(kCTTextAlignmentCenter), + @"right": @(kCTTextAlignmentRight), + @"justify": @(kCTTextAlignmentJustified), +}), kCTTextAlignmentNatural, integerValue) + +// This takes a tuple of text lines and a font to generate a CTLine for each text line. +// This prepares everything for rendering a frame of text in ARTText. ++ (ARTTextFrame)ARTTextFrame:(id)json +{ + NSDictionary *dict = [self NSDictionary:json]; + ARTTextFrame frame; + frame.count = 0; + + NSArray *lines = [self NSArray:dict[@"lines"]]; + NSUInteger lineCount = [lines count]; + if (lineCount == 0) { + return frame; + } + + NSDictionary *fontDict = dict[@"font"]; + CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"]]; + if (!font) { + return frame; + } + + // Create a dictionary for this font + CFDictionaryRef attributes = (__bridge CFDictionaryRef)@{ + (NSString *)kCTFontAttributeName: (__bridge id)font, + (NSString *)kCTForegroundColorFromContextAttributeName: @YES + }; + + // Set up text frame with font metrics + CGFloat size = CTFontGetSize(font); + frame.count = lineCount; + frame.baseLine = size; // estimate base line + frame.lineHeight = size * 1.1; // Base on ART canvas line height estimate + frame.lines = malloc(sizeof(CTLineRef) * lineCount); + frame.widths = malloc(sizeof(CGFloat) * lineCount); + + [lines enumerateObjectsUsingBlock:^(NSString *text, NSUInteger i, BOOL *stop) { + + CFStringRef string = (__bridge CFStringRef)text; + CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); + CTLineRef line = CTLineCreateWithAttributedString(attrString); + CFRelease(attrString); + + frame.lines[i] = line; + frame.widths[i] = CTLineGetTypographicBounds(line, NULL, NULL, NULL); + }]; + + return frame; +} + ++ (ARTCGFloatArray)ARTCGFloatArray:(id)json +{ + NSArray *arr = [self NSNumberArray:json]; + NSUInteger count = arr.count; + + ARTCGFloatArray array; + array.count = count; + array.array = NULL; + + if (count) { + // Ideally, these arrays should already use the same memory layout. + // In that case we shouldn't need this new malloc. + array.array = malloc(sizeof(CGFloat) * count); + for (NSUInteger i = 0; i < count; i++) { + array.array[i] = [arr[i] doubleValue]; + } + } + + return array; +} + ++ (ARTBrush *)ARTBrush:(id)json +{ + NSArray *arr = [self NSArray:json]; + NSUInteger type = [self NSUInteger:arr[0]]; + switch (type) { + case 0: // solid color + // These are probably expensive allocations since it's often the same value. + // We should memoize colors but look ups may be just as expensive. + return [[ARTSolidColor alloc] initWithArray:arr]; + case 1: // linear gradient + return [[ARTLinearGradient alloc] initWithArray:arr]; + case 2: // radial gradient + return [[ARTRadialGradient alloc] initWithArray:arr]; + case 3: // pattern + return [[ARTPattern alloc] initWithArray:arr]; + default: + RCTLogError(@"Unknown brush type: %zd", type); + return nil; + } +} + ++ (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset + 2) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", 2 + offset, arr); + return CGPointZero; + } + return (CGPoint){ + [self CGFloat:arr[offset]], + [self CGFloat:arr[offset + 1]], + }; +} + ++ (CGRect)CGRect:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset + 4) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr); + return CGRectZero; + } + return (CGRect){ + {[self CGFloat:arr[offset]], [self CGFloat:arr[offset + 1]]}, + {[self CGFloat:arr[offset + 2]], [self CGFloat:arr[offset + 3]]}, + }; +} + ++ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset + 4) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", 4 + offset, arr); + return NULL; + } + return [self CGColor:[arr subarrayWithRange:(NSRange){offset, 4}]]; +} + ++ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset +{ + NSArray *arr = [self NSArray:json]; + if (arr.count < offset) { + RCTLogError(@"Too few elements in array (expected at least %zd): %@", offset, arr); + return NULL; + } + arr = [arr subarrayWithRange:(NSRange){offset, arr.count - offset}]; + ARTCGFloatArray colorsAndOffsets = [self ARTCGFloatArray:arr]; + size_t stops = colorsAndOffsets.count / 5; + CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColorComponents( + rgb, + colorsAndOffsets.array, + colorsAndOffsets.array + stops * 4, + stops + ); + CGColorSpaceRelease(rgb); + free(colorsAndOffsets.array); + return (CGGradientRef)CFAutorelease(gradient); +} + +@end diff --git a/Libraries/ART/ReactIOSART.js b/Libraries/ART/ReactIOSART.js new file mode 100644 index 00000000000000..7eb0184717a52f --- /dev/null +++ b/Libraries/ART/ReactIOSART.js @@ -0,0 +1,585 @@ +/** + * 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. + * + * @providesModule ReactIOSART + */ + +"use strict"; + +var Color = require('art/core/color'); +var Path = require('ARTSerializablePath'); +var Transform = require('art/core/transform'); + +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +// Diff Helpers + +function arrayDiffer(a, b) { + if (a == null) { + return true; + } + if (a.length !== b.length) { + return true; + } + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return true; + } + } + return false; +} + +function fontAndLinesDiffer(a, b) { + if (a === b) { + return false; + } + if (a.font !== b.font) { + if (a.font === null) { + return true; + } + if (b.font === null) { + return true; + } + + if ( + a.font.fontFamily !== b.font.fontFamily || + a.font.fontSize !== b.font.fontSize || + a.font.fontWeight !== b.font.fontWeight || + a.font.fontStyle !== b.font.fontStyle + ) { + return true; + } + } + return arrayDiffer(a.lines, b.lines); +} + +// Native Attributes + +var SurfaceViewAttributes = merge(ReactIOSViewAttributes.UIView, { + // This should contain pixel information such as width, height and + // resolution to know what kind of buffer needs to be allocated. + // Currently we rely on UIViews and style to figure that out. +}); + +var NodeAttributes = { + transform: { diff: arrayDiffer }, + opacity: true, +}; + +var GroupAttributes = merge(NodeAttributes, { + clipping: { diff: arrayDiffer } +}); + +var RenderableAttributes = merge(NodeAttributes, { + fill: { diff: arrayDiffer }, + stroke: { diff: arrayDiffer }, + strokeWidth: true, + strokeCap: true, + strokeJoin: true, + strokeDash: { diff: arrayDiffer }, +}); + +var ShapeAttributes = merge(RenderableAttributes, { + d: { diff: arrayDiffer }, +}); + +var TextAttributes = merge(RenderableAttributes, { + alignment: true, + frame: { diff: fontAndLinesDiffer }, + path: { diff: arrayDiffer } +}); + +// Native Components + +var NativeSurfaceView = createReactIOSNativeComponentClass({ + validAttributes: SurfaceViewAttributes, + uiViewClassName: 'ARTSurfaceView', +}); + +var NativeGroup = createReactIOSNativeComponentClass({ + validAttributes: GroupAttributes, + uiViewClassName: 'ARTGroup', +}); + +var NativeShape = createReactIOSNativeComponentClass({ + validAttributes: ShapeAttributes, + uiViewClassName: 'ARTShape', +}); + +var NativeText = createReactIOSNativeComponentClass({ + validAttributes: TextAttributes, + uiViewClassName: 'ARTText', +}); + +// Utilities + +function childrenAsString(children) { + if (!children) { + return ''; + } + if (typeof children === 'string') { + return children; + } + if (children.length) { + return children.join('\n'); + } + return ''; +} + +// Surface - Root node of all ART + +var Surface = React.createClass({ + + render: function() { + var props = this.props; + var w = extractNumber(props.width, 0); + var h = extractNumber(props.height, 0); + return ( + + {this.props.children} + + ); + } + +}); + +// Node Props + +// TODO: The desktop version of ART has title and cursor. We should have +// accessibility support here too even though hovering doesn't work. + +function extractNumber(value, defaultValue) { + if (value == null) { + return defaultValue; + } + return +value; +} + +var pooledTransform = new Transform(); + +function extractTransform(props) { + var scaleX = props.scaleX != null ? props.scaleX : + props.scale != null ? props.scale : 1; + var scaleY = props.scaleY != null ? props.scaleY : + props.scale != null ? props.scale : 1; + + pooledTransform + .transformTo(1, 0, 0, 1, 0, 0) + .move(props.x || 0, props.y || 0) + .rotate(props.rotation || 0, props.originX, props.originY) + .scale(scaleX, scaleY, props.originX, props.originY); + + if (props.transform != null) { + pooledTransform.transform(props.transform); + } + + return [ + pooledTransform.xx, pooledTransform.yx, + pooledTransform.xy, pooledTransform.yy, + pooledTransform.x, pooledTransform.y, + ]; +} + +function extractOpacity(props) { + // TODO: visible === false should also have no hit detection + if (props.visible === false) { + return 0; + } + if (props.opacity == null) { + return 1; + } + return +props.opacity; +} + +// Groups + +// Note: ART has a notion of width and height on Group but AFAIK it's a noop in +// ReactART. + +var Group = React.createClass({ + + render: function() { + var props = this.props; + return ( + + {this.props.children} + + ); + } + +}); + +var ClippingRectangle = React.createClass({ + + render: function() { + var props = this.props; + var x = extractNumber(props.x, 0); + var y = extractNumber(props.y, 0); + var w = extractNumber(props.width, 0); + var h = extractNumber(props.height, 0); + var clipping = new Path() + .moveTo(x, y) + .line(w, 0) + .line(0, h) + .line(w, 0) + .close() + .toJSON(); + // The current clipping API requires x and y to be ignored in the transform + var propsExcludingXAndY = merge(props); + delete propsExcludingXAndY.x; + delete propsExcludingXAndY.y; + return ( + + {this.props.children} + + ); + } + +}); + +// Renderables + +var SOLID_COLOR = 0; +var LINEAR_GRADIENT = 1; +var RADIAL_GRADIENT = 2; +var PATTERN = 3; + +function insertColorIntoArray(color, targetArray, atIndex) { + var c = new Color(color); + targetArray[atIndex + 0] = c.red / 255; + targetArray[atIndex + 1] = c.green / 255; + targetArray[atIndex + 2] = c.blue / 255; + targetArray[atIndex + 3] = c.alpha; +} + +function insertColorsIntoArray(stops, targetArray, atIndex) { + var i = 0; + if ('length' in stops) { + while (i < stops.length) { + insertColorIntoArray(stops[i], targetArray, atIndex + i * 4); + i++; + } + } else { + for (var offset in stops) { + insertColorIntoArray(stops[offset], targetArray, atIndex + i * 4); + i++; + } + } + return atIndex + i * 4; +} + +function insertOffsetsIntoArray(stops, targetArray, atIndex, multi, reverse) { + var offsetNumber; + var i = 0; + if ('length' in stops) { + while (i < stops.length) { + offsetNumber = i / (stops.length - 1) * multi; + targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber; + i++; + } + } else { + for (var offsetString in stops) { + offsetNumber = (+offsetString) * multi; + targetArray[atIndex + i] = reverse ? 1 - offsetNumber : offsetNumber; + i++; + } + } + return atIndex + i; +} + +function insertColorStopsIntoArray(stops, targetArray, atIndex) { + var lastIndex = insertColorsIntoArray(stops, targetArray, atIndex); + insertOffsetsIntoArray(stops, targetArray, lastIndex, 1, false); +} + +function insertDoubleColorStopsIntoArray(stops, targetArray, atIndex) { + var lastIndex = insertColorsIntoArray(stops, targetArray, atIndex); + lastIndex = insertColorsIntoArray(stops, targetArray, lastIndex); + lastIndex = insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, false); + insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, true); +} + +function applyBoundingBoxToBrushData(brushData, props) { + var type = brushData[0]; + var width = +props.width; + var height = +props.height; + if (type === LINEAR_GRADIENT) { + brushData[1] *= width; + brushData[2] *= height; + brushData[3] *= width; + brushData[4] *= height; + } else if (type === RADIAL_GRADIENT) { + brushData[1] *= width; + brushData[2] *= height; + brushData[3] *= width; + brushData[4] *= height; + brushData[5] *= width; + brushData[6] *= height; + } else if (type === PATTERN) { + // todo + } +} + +function extractBrush(colorOrBrush, props) { + if (colorOrBrush == null) { + return null; + } + if (colorOrBrush._brush) { + if (colorOrBrush._bb) { + // The legacy API for Gradients allow for the bounding box to be used + // as a convenience for specifying gradient positions. This should be + // deprecated. It's not properly implemented in canvas mode. ReactART + // doesn't handle update to the bounding box correctly. That's why we + // mutate this so that if it's reused, we reuse the same resolved box. + applyBoundingBoxToBrushData(colorOrBrush._brush, props); + colorOrBrush._bb = false; + } + return colorOrBrush._brush; + } + var c = new Color(colorOrBrush); + return [SOLID_COLOR, c.red / 255, c.green / 255, c.blue / 255, c.alpha]; +} + +function extractColor(color) { + if (color == null) { + return null; + } + var c = new Color(color); + return [c.red / 255, c.green / 255, c.blue / 255, c.alpha]; +} + +function extractStrokeCap(strokeCap) { + switch (strokeCap) { + case 'butt': return 0; + case 'square': return 2; + default: return 1; // round + } +} + +function extractStrokeJoin(strokeJoin) { + switch (strokeJoin) { + case 'miter': return 0; + case 'bevel': return 2; + default: return 1; // round + } +} + +// Shape + +// Note: ART has a notion of width and height on Shape but AFAIK it's a noop in +// ReactART. + +var Shape = React.createClass({ + + render: function() { + var props = this.props; + var path = props.d || childrenAsString(props.children); + var d = new Path(path).toJSON(); + return ( + + ); + } + +}); + +// Text + +var cachedFontObjectsFromString = {}; + +var fontFamilyPrefix = /^[\s"']*/; +var fontFamilySuffix = /[\s"']*$/; + +function extractSingleFontFamily(fontFamilyString) { + // ART on the web allows for multiple font-families to be specified. + // For compatibility, we extract the first font-family, hoping + // we'll get a match. + return fontFamilyString.split(',')[0] + .replace(fontFamilyPrefix, '') + .replace(fontFamilySuffix, ''); +} + +function parseFontString(font) { + if (cachedFontObjectsFromString.hasOwnProperty(font)) { + return cachedFontObjectsFromString[font]; + } + var regexp = /^\s*((?:(?:normal|bold|italic)\s+)*)(?:(\d+(?:\.\d+)?)[ptexm\%]*(?:\s*\/.*?)?\s+)?\s*\"?([^\"]*)/i; + var match = regexp.exec(font); + if (!match) { + return null; + } + var fontFamily = extractSingleFontFamily(match[3]); + var fontSize = +match[2] || 12; + var isBold = /bold/.exec(match[1]); + var isItalic = /italic/.exec(match[1]); + cachedFontObjectsFromString[font] = { + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: isBold ? 'bold' : 'normal', + fontStyle: isItalic ? 'italic' : 'normal', + }; + return cachedFontObjectsFromString[font]; +} + +function extractFont(font) { + if (font == null) { + return null; + } + if (typeof font === 'string') { + return parseFontString(font); + } + var fontFamily = extractSingleFontFamily(font.fontFamily); + var fontSize = +font.fontSize || 12; + return { + // Normalize + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: font.fontWeight, + fontStyle: font.fontStyle, + }; +} + +var newLine = /\n/g; +function extractFontAndLines(font, text) { + return { font: extractFont(font), lines: text.split(newLine) }; +} + +function extractAlignment(alignment) { + switch (alignment) { + case 'right': + return 1; + case 'center': + return 2; + default: + return 0; + } +} + +var Text = React.createClass({ + + render: function() { + var props = this.props; + var textPath = props.path ? new Path(props.path).toJSON() : null; + var textFrame = extractFontAndLines( + props.font, + childrenAsString(props.children) + ); + return ( + + ); + } + +}); + +// Declarative fill type objects - API design not finalized + +function LinearGradient(stops, x1, y1, x2, y2) { + var type = LINEAR_GRADIENT; + + if (arguments.length < 5) { + var angle = ((x1 == null) ? 270 : x1) * Math.PI / 180; + + var x = Math.cos(angle); + var y = -Math.sin(angle); + var l = (Math.abs(x) + Math.abs(y)) / 2; + + x *= l; y *= l; + + x1 = 0.5 - x; + x2 = 0.5 + x; + y1 = 0.5 - y; + y2 = 0.5 + y; + this._bb = true; + } else { + this._bb = false; + } + + var brushData = [type, +x1, +y1, +x2, +y2]; + insertColorStopsIntoArray(stops, brushData, 5); + this._brush = brushData; +} + +function RadialGradient(stops, fx, fy, rx, ry, cx, cy) { + if (ry == null) { + ry = rx; + } + if (cx == null) { + cx = fx; + } + if (cy == null) { + cy = fy; + } + if (fx == null) { + // As a convenience we allow the whole radial gradient to cover the + // bounding box. We should consider dropping this API. + fx = fy = rx = ry = cx = cy = 0.5; + this._bb = true; + } else { + this._bb = false; + } + // The ART API expects the radial gradient to be repeated at the edges. + // To simulate this we render the gradient twice as large and add double + // color stops. Ideally this API would become more restrictive so that this + // extra work isn't needed. + var brushData = [RADIAL_GRADIENT, +fx, +fy, +rx * 2, +ry * 2, +cx, +cy]; + insertDoubleColorStopsIntoArray(stops, brushData, 7); + this._brush = brushData; +} + +function Pattern(url, width, height, left, top) { + this._brush = [PATTERN, url, +left || 0, +top || 0, +width, +height]; +} + +var ReactART = { + + LinearGradient: LinearGradient, + RadialGradient: RadialGradient, + Pattern: Pattern, + Transform: Transform, + Path: Path, + Surface: Surface, + Group: Group, + ClippingRectangle: ClippingRectangle, + Shape: Shape, + Text: Text, + +}; + +module.exports = ReactART; diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.h b/Libraries/ART/ViewManagers/ARTGroupManager.h new file mode 100644 index 00000000000000..0a90eb3d6c18c9 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTGroupManager.h @@ -0,0 +1,14 @@ +/** + * 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 "ARTNodeManager.h" + +@interface ARTGroupManager : ARTNodeManager + +@end diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.m b/Libraries/ART/ViewManagers/ARTGroupManager.m new file mode 100644 index 00000000000000..15f55d4df12d11 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTGroupManager.m @@ -0,0 +1,23 @@ +/** + * 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 "ARTGroupManager.h" + +#import "ARTGroup.h" + +@implementation ARTGroupManager + +RCT_EXPORT_MODULE() + +- (ARTNode *)node +{ + return [[ARTGroup alloc] init]; +} + +@end diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.h b/Libraries/ART/ViewManagers/ARTNodeManager.h new file mode 100644 index 00000000000000..1097eefde91246 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTNodeManager.h @@ -0,0 +1,17 @@ +/** + * 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 "ARTNode.h" +#import "RCTViewManager.h" + +@interface ARTNodeManager : RCTViewManager + +- (ARTNode *)node; + +@end diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.m b/Libraries/ART/ViewManagers/ARTNodeManager.m new file mode 100644 index 00000000000000..c2f0dba35ad065 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTNodeManager.m @@ -0,0 +1,36 @@ +/** + * 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 "ARTNodeManager.h" + +#import "ARTNode.h" + +@implementation ARTNodeManager + +RCT_EXPORT_MODULE() + +- (ARTNode *)node +{ + return [[ARTNode alloc] init]; +} + +- (UIView *)view +{ + return [self node]; +} + +- (RCTShadowView *)shadowView +{ + return nil; +} + +RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(transform, CGAffineTransform) + +@end diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.h b/Libraries/ART/ViewManagers/ARTRenderableManager.h new file mode 100644 index 00000000000000..376fcf518b3293 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTRenderableManager.h @@ -0,0 +1,17 @@ +/** + * 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 "ARTNodeManager.h" +#import "ARTRenderable.h" + +@interface ARTRenderableManager : ARTNodeManager + +- (ARTRenderable *)node; + +@end diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.m b/Libraries/ART/ViewManagers/ARTRenderableManager.m new file mode 100644 index 00000000000000..01b579dca4c940 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTRenderableManager.m @@ -0,0 +1,30 @@ +/** + * 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 "ARTRenderableManager.h" + +#import "RCTConvert+ART.h" + +@implementation ARTRenderableManager + +RCT_EXPORT_MODULE() + +- (ARTRenderable *)node +{ + return [[ARTRenderable alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(strokeCap, CGLineCap) +RCT_EXPORT_VIEW_PROPERTY(strokeJoin, CGLineJoin) +RCT_EXPORT_VIEW_PROPERTY(fill, ARTBrush) +RCT_EXPORT_VIEW_PROPERTY(stroke, CGColor) +RCT_EXPORT_VIEW_PROPERTY(strokeDash, ARTCGFloatArray) + +@end diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.h b/Libraries/ART/ViewManagers/ARTShapeManager.h new file mode 100644 index 00000000000000..d6bc76baa09fdb --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTShapeManager.h @@ -0,0 +1,14 @@ +/** + * 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 "ARTRenderableManager.h" + +@interface ARTShapeManager : ARTRenderableManager + +@end diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.m b/Libraries/ART/ViewManagers/ARTShapeManager.m new file mode 100644 index 00000000000000..426237fa75cfc8 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTShapeManager.m @@ -0,0 +1,26 @@ +/** + * 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 "ARTShapeManager.h" + +#import "ARTShape.h" +#import "RCTConvert+ART.h" + +@implementation ARTShapeManager + +RCT_EXPORT_MODULE() + +- (ARTRenderable *)node +{ + return [[ARTShape alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(d, CGPath) + +@end diff --git a/React/Views/RCTUIActivityIndicatorViewManager.h b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h similarity index 84% rename from React/Views/RCTUIActivityIndicatorViewManager.h rename to Libraries/ART/ViewManagers/ARTSurfaceViewManager.h index e5a10fdd75c746..6d8e140049e8fb 100644 --- a/React/Views/RCTUIActivityIndicatorViewManager.h +++ b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h @@ -9,6 +9,6 @@ #import "RCTViewManager.h" -@interface RCTUIActivityIndicatorViewManager : RCTViewManager +@interface ARTSurfaceViewManager : RCTViewManager @end diff --git a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m new file mode 100644 index 00000000000000..ddfba6697be52c --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m @@ -0,0 +1,23 @@ +/** + * 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 "ARTSurfaceViewManager.h" + +#import "ARTSurfaceView.h" + +@implementation ARTSurfaceViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[ARTSurfaceView alloc] init]; +} + +@end diff --git a/Libraries/ART/ViewManagers/ARTTextManager.h b/Libraries/ART/ViewManagers/ARTTextManager.h new file mode 100644 index 00000000000000..48da9c891f4757 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTTextManager.h @@ -0,0 +1,14 @@ +/** + * 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 "ARTRenderableManager.h" + +@interface ARTTextManager : ARTRenderableManager + +@end diff --git a/Libraries/ART/ViewManagers/ARTTextManager.m b/Libraries/ART/ViewManagers/ARTTextManager.m new file mode 100644 index 00000000000000..473d0cf4fa4a88 --- /dev/null +++ b/Libraries/ART/ViewManagers/ARTTextManager.m @@ -0,0 +1,27 @@ +/** + * 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 "ARTTextManager.h" + +#import "ARTText.h" +#import "RCTConvert+ART.h" + +@implementation ARTTextManager + +RCT_EXPORT_MODULE() + +- (ARTRenderable *)node +{ + return [[ARTText alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(alignment, CTTextAlignment) +RCT_REMAP_VIEW_PROPERTY(frame, textFrame, ARTTextFrame) + +@end diff --git a/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj/project.pbxproj b/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj/project.pbxproj index 7e420235efe607..8434df87d37151 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj/project.pbxproj +++ b/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ 58B511F01A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -206,6 +207,7 @@ LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTActionSheet; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -213,6 +215,7 @@ 58B511F11A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -221,6 +224,7 @@ LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTActionSheet; + RUN_CLANG_STATIC_ANALYZER = NO; SKIP_INSTALL = YES; }; name = Release; diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index c6d6e404cb3ad8..75798efaf37ada 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -30,96 +30,97 @@ - (instancetype)init return self; } +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options failureCallback:(RCTResponseSenderBlock)failureCallback successCallback:(RCTResponseSenderBlock)successCallback) { - dispatch_async(dispatch_get_main_queue(), ^{ - UIActionSheet *actionSheet = [[UIActionSheet alloc] init]; + UIActionSheet *actionSheet = [[UIActionSheet alloc] init]; - actionSheet.title = options[@"title"]; + actionSheet.title = options[@"title"]; - for (NSString *option in options[@"options"]) { - [actionSheet addButtonWithTitle:option]; - } + for (NSString *option in options[@"options"]) { + [actionSheet addButtonWithTitle:option]; + } - if (options[@"destructiveButtonIndex"]) { - actionSheet.destructiveButtonIndex = [options[@"destructiveButtonIndex"] integerValue]; - } - if (options[@"cancelButtonIndex"]) { - actionSheet.cancelButtonIndex = [options[@"cancelButtonIndex"] integerValue]; - } + if (options[@"destructiveButtonIndex"]) { + actionSheet.destructiveButtonIndex = [options[@"destructiveButtonIndex"] integerValue]; + } + if (options[@"cancelButtonIndex"]) { + actionSheet.cancelButtonIndex = [options[@"cancelButtonIndex"] integerValue]; + } - actionSheet.delegate = self; + actionSheet.delegate = self; - _callbacks[keyForInstance(actionSheet)] = successCallback; + _callbacks[RCTKeyForInstance(actionSheet)] = successCallback; - UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; - if (appWindow == nil) { - RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); - return; - } - [actionSheet showInView:appWindow]; - }); + UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; + if (appWindow == nil) { + RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); + return; + } + [actionSheet showInView:appWindow]; } RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options failureCallback:(RCTResponseSenderBlock)failureCallback successCallback:(RCTResponseSenderBlock)successCallback) { - dispatch_async(dispatch_get_main_queue(), ^{ - NSMutableArray *items = [NSMutableArray array]; - id message = options[@"message"]; - id url = options[@"url"]; - if ([message isKindOfClass:[NSString class]]) { - [items addObject:message]; - } - if ([url isKindOfClass:[NSString class]]) { - [items addObject:[NSURL URLWithString:url]]; - } - if ([items count] == 0) { - failureCallback(@[@"No `url` or `message` to share"]); - return; - } - UIActivityViewController *share = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; - UIViewController *ctrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; - if ([share respondsToSelector:@selector(setCompletionWithItemsHandler:)]) { - share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { - if (activityError) { - failureCallback(@[[activityError localizedDescription]]); - } else { - successCallback(@[@(completed), (activityType ?: [NSNull null])]); - } - }; - } else { + NSMutableArray *items = [NSMutableArray array]; + id message = options[@"message"]; + id url = options[@"url"]; + if ([message isKindOfClass:[NSString class]]) { + [items addObject:message]; + } + if ([url isKindOfClass:[NSString class]]) { + [items addObject:[NSURL URLWithString:url]]; + } + if ([items count] == 0) { + failureCallback(@[@"No `url` or `message` to share"]); + return; + } + UIActivityViewController *share = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; + UIViewController *ctrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + if ([share respondsToSelector:@selector(setCompletionWithItemsHandler:)]) { + share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + if (activityError) { + failureCallback(@[[activityError localizedDescription]]); + } else { + successCallback(@[@(completed), (activityType ?: [NSNull null])]); + } + }; + } else { #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - if (![UIActivityViewController instancesRespondToSelector:@selector(completionWithItemsHandler)]) { - // Legacy iOS 7 implementation - share.completionHandler = ^(NSString *activityType, BOOL completed) { - successCallback(@[@(completed), (activityType ?: [NSNull null])]); - }; - } else + if (![UIActivityViewController instancesRespondToSelector:@selector(completionWithItemsHandler)]) { + // Legacy iOS 7 implementation + share.completionHandler = ^(NSString *activityType, BOOL completed) { + successCallback(@[@(completed), (activityType ?: [NSNull null])]); + }; + } else #endif - { - // iOS 8 version - share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { - successCallback(@[@(completed), (activityType ?: [NSNull null])]); - }; - } + { + // iOS 8 version + share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + successCallback(@[@(completed), (activityType ?: [NSNull null])]); + }; } - [ctrl presentViewController:share animated:YES completion:nil]; - }); + } + [ctrl presentViewController:share animated:YES completion:nil]; } #pragma mark UIActionSheetDelegate Methods - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { - NSString *key = keyForInstance(actionSheet); + NSString *key = RCTKeyForInstance(actionSheet); RCTResponseSenderBlock callback = _callbacks[key]; if (callback) { callback(@[@(buttonIndex)]); @@ -133,7 +134,7 @@ - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger #pragma mark Private -NS_INLINE NSString *keyForInstance(id instance) +static NSString *RCTKeyForInstance(id instance) { return [NSString stringWithFormat:@"%p", instance]; } diff --git a/Libraries/AdSupport/RCTAdSupport.xcodeproj/project.pbxproj b/Libraries/AdSupport/RCTAdSupport.xcodeproj/project.pbxproj index 811d25e6304d57..1b89d7bfa9d6fc 100644 --- a/Libraries/AdSupport/RCTAdSupport.xcodeproj/project.pbxproj +++ b/Libraries/AdSupport/RCTAdSupport.xcodeproj/project.pbxproj @@ -208,6 +208,7 @@ 832C81951AAF6DF0007FA2F7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -215,6 +216,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -222,6 +224,7 @@ 832C81961AAF6DF0007FA2F7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/Animation/AnimationUtils.js b/Libraries/Animation/AnimationUtils.js index d6d95f62de248b..2ee2d8a7097591 100644 --- a/Libraries/Animation/AnimationUtils.js +++ b/Libraries/Animation/AnimationUtils.js @@ -20,6 +20,9 @@ type EasingFunction = (t: number) => number; var defaults = { + linear: function(t: number): number { + return t; + }, easeInQuad: function(t: number): number { return t * t; }, @@ -228,8 +231,10 @@ module.exports = { var tickCount = Math.round(duration * ticksPerSecond / 1000); var samples = []; - for (var i = 0; i <= tickCount; i++) { - samples.push(easing.call(defaults, i / tickCount)); + if (tickCount > 0) { + for (var i = 0; i <= tickCount; i++) { + samples.push(easing.call(defaults, i / tickCount)); + } } return samples; diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index eb2ddd1cd81266..6bcda39ae30d83 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -68,6 +68,11 @@ - (instancetype)init return self; } +- (dispatch_queue_t)methodQueue +{ + return _bridge.uiManager.methodQueue; +} + - (id (^)(CGFloat))interpolateFrom:(CGFloat[])fromArray to:(CGFloat[])toArray count:(NSUInteger)count typeName:(const char *)typeName { if (count == 1) { @@ -109,7 +114,7 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS animationTag:(NSNumber *)animationTag duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay - easingSample:(NSArray *)easingSample + easingSample:(NSNumberArray *)easingSample properties:(NSDictionary *)properties callback:(RCTResponseSenderBlock)callback) { @@ -259,7 +264,9 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS RCTAnimationExperimentalManager *strongSelf = weakSelf; NSNumber *reactTag = strongSelf->_animationRegistry[animationTag]; - if (!reactTag) return; + if (!reactTag) { + return; + } UIView *view = viewRegistry[reactTag]; for (NSString *animationKey in view.layer.animationKeys) { diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js b/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js new file mode 100644 index 00000000000000..4d5c7b34c3b9c8 --- /dev/null +++ b/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js @@ -0,0 +1,45 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var NativeModules = { + I18n: { + translationsDictionary: JSON.stringify({ + 'Good bye, {name}!|Bye message': '¡Adiós {name}!', + }), + }, + Timing: { + createTimer: jest.genMockFunction(), + deleteTimer: jest.genMockFunction(), + }, + GraphPhotoUpload: { + upload: jest.genMockFunction(), + }, + FacebookSDK: { + login: jest.genMockFunction(), + logout: jest.genMockFunction(), + queryGraphPath: jest.genMockFunction().mockImpl( + (path, method, params, callback) => callback() + ), + }, + DataManager: { + queryData: jest.genMockFunction(), + }, + UIManager: { + customBubblingEventTypes: {}, + customDirectEventTypes: {}, + Dimensions: {}, + }, + AsyncLocalStorage: { + getItem: jest.genMockFunction(), + setItem: jest.genMockFunction(), + removeItem: jest.genMockFunction(), + clear: jest.genMockFunction(), + }, + SourceCode: { + scriptURL: null, + }, +}; + +module.exports = NativeModules; diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js index f2bcbfd516c650..a3f1fe6be8c3d5 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js @@ -15,25 +15,19 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeModules = require('NativeModules'); var PropTypes = require('ReactPropTypes'); var React = require('React'); -var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); var View = require('View'); -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); -var keyMirror = require('keyMirror'); -var merge = require('merge'); - -var SpinnerSize = keyMirror({ - large: null, - small: null, -}); +var requireNativeComponent = require('requireNativeComponent'); +var verifyPropTypes = require('verifyPropTypes'); var GRAY = '#999999'; type DefaultProps = { animating: boolean; - size: 'small' | 'large'; color: string; + hidesWhenStopped: boolean; + size: 'small' | 'large'; }; var ActivityIndicatorIOS = React.createClass({ @@ -48,7 +42,10 @@ var ActivityIndicatorIOS = React.createClass({ * The foreground color of the spinner (default is gray). */ color: PropTypes.string, - + /** + * Whether the indicator should hide when not animating (true by default). + */ + hidesWhenStopped: PropTypes.bool, /** * Size of the indicator. Small has a height of 20, large has a height of 36. */ @@ -61,27 +58,18 @@ var ActivityIndicatorIOS = React.createClass({ getDefaultProps: function(): DefaultProps { return { animating: true, - size: SpinnerSize.small, color: GRAY, + hidesWhenStopped: true, + size: 'small', }; }, render: function() { - var style = styles.sizeSmall; - var NativeConstants = NativeModules.UIManager.UIActivityIndicatorView.Constants; - var activityIndicatorViewStyle = NativeConstants.StyleWhite; - if (this.props.size === 'large') { - style = styles.sizeLarge; - activityIndicatorViewStyle = NativeConstants.StyleWhiteLarge; - } + var {style, ...props} = this.props; + var sizeStyle = (this.props.size === 'large') ? styles.sizeLarge : styles.sizeSmall; return ( - - + + ); } @@ -100,14 +88,17 @@ var styles = StyleSheet.create({ } }); -var UIActivityIndicatorView = createReactIOSNativeComponentClass({ - validAttributes: merge( - ReactIOSViewAttributes.UIView, { - activityIndicatorViewStyle: true, // UIActivityIndicatorViewStyle=UIActivityIndicatorViewStyleWhite - animating: true, - color: true, - }), - uiViewClassName: 'UIActivityIndicatorView', -}); +var RCTActivityIndicatorView = requireNativeComponent( + 'RCTActivityIndicatorView', + null +); +if (__DEV__) { + var nativeOnlyProps = {activityIndicatorViewStyle: true}; + verifyPropTypes( + ActivityIndicatorIOS, + RCTActivityIndicatorView.viewConfig, + nativeOnlyProps + ); +} module.exports = ActivityIndicatorIOS; diff --git a/Libraries/Components/DatePicker/DatePickerIOS.ios.js b/Libraries/Components/DatePicker/DatePickerIOS.ios.js index 9bd0a2ac473080..41fc9b8779f50d 100644 --- a/Libraries/Components/DatePicker/DatePickerIOS.ios.js +++ b/Libraries/Components/DatePicker/DatePickerIOS.ios.js @@ -16,14 +16,11 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var PropTypes = require('ReactPropTypes'); var React = require('React'); -var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var RCTDatePickerIOSConsts = require('NativeModules').UIManager.RCTDatePicker.Constants; var StyleSheet = require('StyleSheet'); var View = require('View'); -var createReactIOSNativeComponentClass = - require('createReactIOSNativeComponentClass'); -var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); var DATEPICKER = 'datepicker'; @@ -148,18 +145,6 @@ var styles = StyleSheet.create({ }, }); -var rkDatePickerIOSAttributes = merge(ReactIOSViewAttributes.UIView, { - date: true, - maximumDate: true, - minimumDate: true, - mode: true, - minuteInterval: true, - timeZoneOffsetInMinutes: true, -}); - -var RCTDatePickerIOS = createReactIOSNativeComponentClass({ - validAttributes: rkDatePickerIOSAttributes, - uiViewClassName: 'RCTDatePicker', -}); +var RCTDatePickerIOS = requireNativeComponent('RCTDatePicker', DatePickerIOS); module.exports = DatePickerIOS; diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index 7beeabbeac5f78..e38dd9564bbc3c 100644 --- a/Libraries/Components/MapView/MapView.js +++ b/Libraries/Components/MapView/MapView.js @@ -13,6 +13,7 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType'); var NativeMethodsMixin = require('NativeMethodsMixin'); +var Platform = require('Platform'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var View = require('View'); @@ -21,8 +22,15 @@ var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentC var deepDiffer = require('deepDiffer'); var insetsDiffer = require('insetsDiffer'); var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); type Event = Object; +type MapRegion = { + latitude: number; + longitude: number; + latitudeDelta: number; + longitudeDelta: number; +}; var MapView = React.createClass({ mixins: [NativeMethodsMixin], @@ -150,46 +158,30 @@ var MapView = React.createClass({ }, render: function() { - return ( - - ); + return ; }, - }); -var RCTMap = createReactIOSNativeComponentClass({ - validAttributes: merge( - ReactIOSViewAttributes.UIView, { - showsUserLocation: true, - zoomEnabled: true, - rotateEnabled: true, - pitchEnabled: true, - scrollEnabled: true, - region: {diff: deepDiffer}, - annotations: {diff: deepDiffer}, - maxDelta: true, - minDelta: true, - legalLabelInsets: {diff: insetsDiffer}, - } - ), - uiViewClassName: 'RCTMap', -}); +if (Platform.OS === 'android') { + var RCTMap = createReactIOSNativeComponentClass({ + validAttributes: merge( + ReactIOSViewAttributes.UIView, { + showsUserLocation: true, + zoomEnabled: true, + rotateEnabled: true, + pitchEnabled: true, + scrollEnabled: true, + region: {diff: deepDiffer}, + annotations: {diff: deepDiffer}, + maxDelta: true, + minDelta: true, + legalLabelInsets: {diff: insetsDiffer}, + } + ), + uiViewClassName: 'RCTMap', + }); +} else { + var RCTMap = requireNativeComponent('RCTMap', MapView); +} module.exports = MapView; diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index fd90847f05c61f..103e749f78b237 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -12,6 +12,7 @@ 'use strict'; var EventEmitter = require('EventEmitter'); +var Image = require('Image'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var RCTNavigatorManager = require('NativeModules').NavigatorManager; @@ -47,11 +48,16 @@ var RCTNavigatorItem = createReactIOSNativeComponentClass({ // NavigatorIOS does not use them all, because some are problematic title: true, barTintColor: true, + leftButtonIcon: true, + leftButtonTitle: true, + onNavLeftButtonTap: true, + rightButtonIcon: true, rightButtonTitle: true, onNavRightButtonTap: true, + backButtonIcon: true, + backButtonTitle: true, tintColor: true, navigationBarHidden: true, - backButtonTitle: true, titleTextColor: true, style: true, }, @@ -79,7 +85,12 @@ type Route = { title: string; passProps: Object; backButtonTitle: string; + backButtonIcon: Object; + leftButtonTitle: string; + leftButtonIcon: Object; + onLeftButtonPress: Function; rightButtonTitle: string; + rightButtonIcon: Object; onRightButtonPress: Function; wrapperStyle: any; }; @@ -212,6 +223,13 @@ var NavigatorIOS = React.createClass({ */ passProps: PropTypes.object, + /** + * If set, the left header button image will appear with this source. Note + * that this doesn't apply for the header of the current view, but the + * ones of the views that are pushed afterward. + */ + backButtonIcon: Image.propTypes.source, + /** * If set, the left header button will appear with this name. Note that * this doesn't apply for the header of the current view, but the ones @@ -219,6 +237,26 @@ var NavigatorIOS = React.createClass({ */ backButtonTitle: PropTypes.string, + /** + * If set, the left header button image will appear with this source + */ + leftButtonIcon: Image.propTypes.source, + + /** + * If set, the left header button will appear with this name + */ + leftButtonTitle: PropTypes.string, + + /** + * Called when the left header button is pressed + */ + onLeftButtonPress: PropTypes.func, + + /** + * If set, the right header button image will appear with this source + */ + rightButtonIcon: Image.propTypes.source, + /** * If set, the right header button will appear with this name */ @@ -252,6 +290,16 @@ var NavigatorIOS = React.createClass({ */ tintColor: PropTypes.string, + /** + * The background color of the navigation bar + */ + barTintColor: PropTypes.string, + + /** + * The text color of the navigation bar title + */ + titleTextColor: PropTypes.string, + }, navigator: (undefined: ?Object), @@ -550,11 +598,18 @@ var NavigatorIOS = React.createClass({ this.props.itemWrapperStyle, route.wrapperStyle ]} + backButtonIcon={this._imageNameFromSource(route.backButtonIcon)} backButtonTitle={route.backButtonTitle} + leftButtonIcon={this._imageNameFromSource(route.leftButtonIcon)} + leftButtonTitle={route.leftButtonTitle} + onNavLeftButtonTap={route.onLeftButtonPress} + rightButtonIcon={this._imageNameFromSource(route.rightButtonIcon)} rightButtonTitle={route.rightButtonTitle} onNavRightButtonTap={route.onRightButtonPress} navigationBarHidden={this.props.navigationBarHidden} - tintColor={this.props.tintColor}> + tintColor={this.props.tintColor} + barTintColor={this.props.barTintColor} + titleTextColor={this.props.titleTextColor}> + + SegmentedControlIOS is not supported on this platform! + + + ); + }, +}); + +var styles = StyleSheet.create({ + dummy: { + width: 120, + height: 50, + backgroundColor: '#ffbcbc', + borderWidth: 1, + borderColor: 'red', + alignItems: 'center', + justifyContent: 'center', + }, + text: { + color: '#333333', + margin: 5, + fontSize: 10, + } +}); + +module.exports = Dummy; diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js new file mode 100644 index 00000000000000..23d952776bb96e --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js @@ -0,0 +1,120 @@ +/** + * 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. + * + * @providesModule SegmentedControlIOS + * @flow + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeModules = require('NativeModules'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); + +var requireNativeComponent = require('requireNativeComponent'); +var verifyPropTypes = require('verifyPropTypes'); + +type DefaultProps = { + values: Array; + enabled: boolean; +}; + +var SEGMENTED_CONTROL_REFERENCE = 'segmentedcontrol'; + +type Event = Object; + +/** + * Use `SegmentedControlIOS` to render a UISegmentedControl iOS. + */ +var SegmentedControlIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The labels for the control's segment buttons, in order. + */ + values: PropTypes.arrayOf(PropTypes.string), + + /** + * The index in `props.values` of the segment to be pre-selected + */ + selectedIndex: PropTypes.number, + + /** + * Callback that is called when the user taps a segment; + * passes the segment's value as an argument + */ + onValueChange: PropTypes.func, + + /** + * Callback that is called when the user taps a segment; + * passes the event as an argument + */ + onChange: PropTypes.func, + + /** + * If false the user won't be able to interact with the control. + * Default value is true. + */ + enabled: PropTypes.bool, + + /** + * Accent color of the control. + */ + tintColor: PropTypes.string, + + /** + * If true, then selecting a segment won't persist visually. + * The `onValueChange` callback will still work as expected. + */ + momentary: PropTypes.bool + }, + + getDefaultProps: function(): DefaultProps { + return { + values: [], + enabled: true + }; + }, + + _onChange: function(event: Event) { + this.props.onChange && this.props.onChange(event); + this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); + }, + + render: function() { + return ( + + ); + } +}); + +var styles = StyleSheet.create({ + segmentedControl: { + height: NativeModules.SegmentedControlManager.ComponentHeight + }, +}); + +var RCTSegmentedControl = requireNativeComponent( + 'RCTSegmentedControl', + null +); +if (__DEV__) { + verifyPropTypes( + RCTSegmentedControl, + RCTSegmentedControl.viewConfig + ); +} + +module.exports = SegmentedControlIOS; diff --git a/Libraries/Components/SliderIOS/SliderIOS.js b/Libraries/Components/SliderIOS/SliderIOS.js index 81815ba34c97c5..bfb2b92714c768 100644 --- a/Libraries/Components/SliderIOS/SliderIOS.js +++ b/Libraries/Components/SliderIOS/SliderIOS.js @@ -12,16 +12,11 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); -var Platform = require('Platform'); var PropTypes = require('ReactPropTypes'); var React = require('React'); -var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); var View = require('View'); -var createReactIOSNativeComponentClass = - require('createReactIOSNativeComponentClass'); -var merge = require('merge'); var requireNativeComponent = require('requireNativeComponent'); type Event = Object; @@ -56,6 +51,18 @@ var SliderIOS = React.createClass({ */ maximumValue: PropTypes.number, + /** + * The color used for the track to the left of the button. Overrides the + * default blue gradient image. + */ + minimumTrackTintColor: PropTypes.string, + + /** + * The color used for the track to the right of the button. Overrides the + * default blue gradient image. + */ + maximumTrackTintColor: PropTypes.string, + /** * Callback continuously called while the user is dragging the slider. */ @@ -86,6 +93,8 @@ var SliderIOS = React.createClass({ value={this.props.value} maximumValue={this.props.maximumValue} minimumValue={this.props.minimumValue} + minimumTrackTintColor={this.props.minimumTrackTintColor} + maximumTrackTintColor={this.props.maximumTrackTintColor} onChange={this._onValueChange} /> ); @@ -98,20 +107,6 @@ var styles = StyleSheet.create({ }, }); -if (Platform.OS === 'ios') { - var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS); -} else { - var validAttributes = { - ...ReactIOSViewAttributes.UIView, - value: true, - minimumValue: true, - maximumValue: true, - }; - - var RCTSlider = createReactIOSNativeComponentClass({ - validAttributes: validAttributes, - uiViewClassName: 'RCTSlider', - }); -} +var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS); module.exports = SliderIOS; diff --git a/Libraries/Components/SwitchIOS/SwitchIOS.ios.js b/Libraries/Components/SwitchIOS/SwitchIOS.ios.js index 70222794758e96..5a56e36b753d4f 100644 --- a/Libraries/Components/SwitchIOS/SwitchIOS.ios.js +++ b/Libraries/Components/SwitchIOS/SwitchIOS.ios.js @@ -16,11 +16,9 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var PropTypes = require('ReactPropTypes'); var React = require('React'); -var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); -var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); var SWITCH = 'switch'; @@ -88,20 +86,16 @@ var SwitchIOS = React.createClass({ // The underlying switch might have changed, but we're controlled, // and so want to ensure it represents our value. - this.refs[SWITCH].setNativeProps({on: this.props.value}); + this.refs[SWITCH].setNativeProps({value: this.props.value}); }, render: function() { return ( ); } @@ -114,17 +108,6 @@ var styles = StyleSheet.create({ }, }); -var rkSwitchAttributes = merge(ReactIOSViewAttributes.UIView, { - onTintColor: true, - tintColor: true, - thumbTintColor: true, - on: true, - enabled: true, -}); - -var RCTSwitch = createReactIOSNativeComponentClass({ - validAttributes: rkSwitchAttributes, - uiViewClassName: 'RCTSwitch', -}); +var RCTSwitch = requireNativeComponent('RCTSwitch', SwitchIOS); module.exports = SwitchIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js index 05ac37c74f5bc7..4163b2d7861b69 100644 --- a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js @@ -12,12 +12,11 @@ 'use strict'; var React = require('React'); -var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); var TabBarItemIOS = require('TabBarItemIOS'); var View = require('View'); -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var requireNativeComponent = require('requireNativeComponent'); var TabBarIOS = React.createClass({ statics: { @@ -43,10 +42,6 @@ var styles = StyleSheet.create({ } }); -var config = { - validAttributes: ReactIOSViewAttributes.UIView, - uiViewClassName: 'RCTTabBar', -}; -var RCTTabBar = createReactIOSNativeComponentClass(config); +var RCTTabBar = requireNativeComponent('RCTTabBar', TabBarIOS); module.exports = TabBarIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js index 86b38a8cda836d..b27e22d4b1e92c 100644 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js @@ -13,13 +13,11 @@ var Image = require('Image'); var React = require('React'); -var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); var View = require('View'); -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); -var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); var TabBarItemIOS = React.createClass({ propTypes: { @@ -68,6 +66,9 @@ var TabBarItemIOS = React.createClass({ * blank content, you probably forgot to add a selected one. */ selected: React.PropTypes.bool, + /** + * React style object. + */ style: View.propTypes.style, /** * Text that appears under the icon. It is ignored when a system icon @@ -88,7 +89,7 @@ var TabBarItemIOS = React.createClass({ } }, - componentWillReceiveProps: function(nextProps: { selected: boolean }) { + componentWillReceiveProps: function(nextProps: { selected?: boolean }) { if (this.state.hasBeenSelected || nextProps.selected) { this.setState({hasBeenSelected: true}); } @@ -121,7 +122,7 @@ var TabBarItemIOS = React.createClass({ selectedIcon={this.props.selectedIcon && this.props.selectedIcon.uri} onPress={this.props.onPress} selected={this.props.selected} - badgeValue={badge} + badge={badge} title={this.props.title} style={[styles.tab, this.props.style]}> {tabContents} @@ -140,15 +141,6 @@ var styles = StyleSheet.create({ } }); -var RCTTabBarItem = createReactIOSNativeComponentClass({ - validAttributes: merge(ReactIOSViewAttributes.UIView, { - title: true, - icon: true, - selectedIcon: true, - selected: true, - badgeValue: true, - }), - uiViewClassName: 'RCTTabBarItem', -}); +var RCTTabBarItem = requireNativeComponent('RCTTabBarItem', TabBarItemIOS); module.exports = TabBarItemIOS; diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index fb5f99949fbdcd..fa87beefc7b513 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -38,6 +38,7 @@ var returnKeyTypeConsts = RCTUIManager.UIReturnKeyType; var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { autoCorrect: true, autoCapitalize: true, + clearTextOnFocus: true, color: true, editable: true, fontFamily: true, @@ -48,6 +49,7 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { returnKeyType: true, enablesReturnKeyAutomatically: true, secureTextEntry: true, + selectTextOnFocus: true, mostRecentEventCounter: true, placeholder: true, placeholderTextColor: true, @@ -350,6 +352,9 @@ var TextInput = React.createClass({ componentWillUnmount: function() { this._focusSubscription && this._focusSubscription.remove(); + if (this.isFocused()) { + this.blur(); + } }, _bufferTimeout: (undefined: ?number), @@ -442,6 +447,7 @@ var TextInput = React.createClass({ onSubmitEditing={this.props.onSubmitEditing} onSelectionChangeShouldSetResponder={() => true} placeholder={this.props.placeholder} + placeholderTextColor={this.props.placeholderTextColor} text={this.state.bufferedValue} autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} @@ -495,6 +501,8 @@ var TextInput = React.createClass({ autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} clearButtonMode={clearButtonMode} + selectTextOnFocus={this.props.selectTextOnFocus} + clearTextOnFocus={this.props.clearTextOnFocus} />; } @@ -574,7 +582,7 @@ var TextInput = React.createClass({ var counter = event.nativeEvent.eventCounter; if (counter > this.state.mostRecentEventCounter) { this.setState({mostRecentEventCounter: counter}); - } + } }, }); diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 7cba2216472945..30d05210fb4a2f 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -11,14 +11,13 @@ */ 'use strict'; +var AnimationExperimental = require('AnimationExperimental'); var NativeMethodsMixin = require('NativeMethodsMixin'); -var React = require('React'); var POPAnimation = require('POPAnimation'); -var AnimationExperimental = require('AnimationExperimental'); +var React = require('React'); var Touchable = require('Touchable'); var merge = require('merge'); -var copyProperties = require('copyProperties'); var onlyChild = require('onlyChild'); type State = { @@ -123,9 +122,8 @@ var TouchableBounce = React.createClass({ }, render: function() { - // Note(vjeux): use cloneWithProps once React has been upgraded var child = onlyChild(this.props.children); - copyProperties(child.props, { + return React.cloneElement(child, { accessible: true, testID: this.props.testID, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, @@ -135,7 +133,6 @@ var TouchableBounce = React.createClass({ onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate }); - return child; } }); diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index e590999debd23f..8d93cd8416bf4c 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -20,6 +20,7 @@ var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var flattenStyle = require('flattenStyle'); var keyOf = require('keyOf'); var onlyChild = require('onlyChild'); @@ -105,12 +106,13 @@ var TouchableOpacity = React.createClass({ }, touchableHandleActivePressOut: function() { - this.setOpacityTo(1.0); + var child = onlyChild(this.props.children); + var childStyle = flattenStyle(child.props.style) || {}; + this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { - this.setOpacityTo(1.0); this.props.onPress && this.props.onPress(); }, diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index c981e41294b486..aa69ab0eb70c66 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -13,12 +13,13 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var PropTypes = require('ReactPropTypes'); +var RCTUIManager = require('NativeModules').UIManager; var React = require('React'); +var ReactIOSStyleAttributes = require('ReactIOSStyleAttributes'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheetPropType = require('StyleSheetPropType'); var ViewStylePropTypes = require('ViewStylePropTypes'); - var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); var stylePropType = StyleSheetPropType(ViewStylePropTypes); @@ -28,7 +29,7 @@ var stylePropType = StyleSheetPropType(ViewStylePropTypes); * container that supports layout with flexbox, style, some touch handling, and * accessibility controls, and is designed to be nested inside other views and * to have 0 to many children of any type. `View` maps directly to the native - * view equivalent on whatever platform react is running on, whether that is a + * view equivalent on whatever platform React is running on, whether that is a * `UIView`, `
`, `android.view`, etc. This example creates a `View` that * wraps two colored boxes and custom component in a row with padding. * @@ -89,6 +90,11 @@ var View = React.createClass({ onStartShouldSetResponder: PropTypes.func, onStartShouldSetResponderCapture: PropTypes.func, + /** + * Invoked on mount and layout changes with {x, y, width, height}. + */ + onLayout: PropTypes.func, + /** * In the absence of `auto` property, `none` is much like `CSS`'s `none` * value. `box-none` is as if you had applied the `CSS` class: @@ -136,6 +142,20 @@ var View = React.createClass({ * (or one of its superviews). */ removeClippedSubviews: PropTypes.bool, + + /** + * Whether this view should render itself (and all of its children) into a + * single hardware texture on the GPU. + * + * On Android, this is useful for animations and interactions that only + * modify opacity, rotation, translation, and/or scale: in those cases, the + * view doesn't have to be redrawn and display lists don't need to be + * re-executed. The texture can just be re-used and re-composited with + * different parameters. The downside is that this can use up limited video + * memory, so this prop should be set back to false at the end of the + * interaction/animation. + */ + renderToHardwareTextureAndroid: PropTypes.bool, }, render: function() { @@ -143,17 +163,26 @@ var View = React.createClass({ }, }); - var RCTView = createReactIOSNativeComponentClass({ validAttributes: ReactIOSViewAttributes.RCTView, uiViewClassName: 'RCTView', }); RCTView.propTypes = View.propTypes; +if (__DEV__) { + var viewConfig = RCTUIManager.viewConfigs && RCTUIManager.viewConfigs.RCTView || {}; + for (var prop in viewConfig.nativeProps) { + var viewAny: any = View; // Appease flow + if (!viewAny.propTypes[prop] && !ReactIOSStyleAttributes[prop]) { + throw new Error( + 'View is missing propType for native prop `' + prop + '`' + ); + } + } +} var ViewToExport = RCTView; if (__DEV__) { ViewToExport = View; } - module.exports = ViewToExport; diff --git a/Libraries/Components/View/ViewStylePropTypes.js b/Libraries/Components/View/ViewStylePropTypes.js index bb22c6b26511bf..c1f6b4b1ccf7d5 100644 --- a/Libraries/Components/View/ViewStylePropTypes.js +++ b/Libraries/Components/View/ViewStylePropTypes.js @@ -13,12 +13,14 @@ var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); +var TransformPropTypes = require('TransformPropTypes'); /** * Warning: Some of these properties may not be supported in all releases. */ var ViewStylePropTypes = { ...LayoutPropTypes, + ...TransformPropTypes, backgroundColor: ReactPropTypes.string, borderColor: ReactPropTypes.string, borderTopColor: ReactPropTypes.string, @@ -30,16 +32,10 @@ var ViewStylePropTypes = { overflow: ReactPropTypes.oneOf(['visible', 'hidden']), shadowColor: ReactPropTypes.string, shadowOffset: ReactPropTypes.shape( - {h: ReactPropTypes.number, w: ReactPropTypes.number} + {width: ReactPropTypes.number, height: ReactPropTypes.number} ), shadowOpacity: ReactPropTypes.number, shadowRadius: ReactPropTypes.number, - transformMatrix: ReactPropTypes.arrayOf(ReactPropTypes.number), - rotation: ReactPropTypes.number, - scaleX: ReactPropTypes.number, - scaleY: ReactPropTypes.number, - translateX: ReactPropTypes.number, - translateY: ReactPropTypes.number, }; module.exports = ViewStylePropTypes; diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index bfc823f9f85928..79ded650658a2a 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -42,6 +42,7 @@ var WebView = React.createClass({ onNavigationStateChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load style: View.propTypes.style, + javaScriptEnabledAndroid: PropTypes.bool, /** * Used to locate this view in end-to-end tests. */ @@ -90,6 +91,7 @@ var WebView = React.createClass({ key="webViewKey" style={webViewStyles} url={this.props.url} + javaScriptEnabledAndroid={this.props.javaScriptEnabledAndroid} contentInset={this.props.contentInset} automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets} onLoadingStart={this.onLoadingStart} @@ -107,15 +109,27 @@ var WebView = React.createClass({ }, goForward: function() { - RCTUIManager.webViewGoForward(this.getWebWiewHandle()); + RCTUIManager.dispatchViewManagerCommand( + this.getWebWiewHandle(), + RCTUIManager.RCTWebView.Commands.goForward, + null + ); }, goBack: function() { - RCTUIManager.webViewGoBack(this.getWebWiewHandle()); + RCTUIManager.dispatchViewManagerCommand( + this.getWebWiewHandle(), + RCTUIManager.RCTWebView.Commands.goBack, + null + ); }, reload: function() { - RCTUIManager.webViewReload(this.getWebWiewHandle()); + RCTUIManager.dispatchViewManagerCommand( + this.getWebWiewHandle(), + RCTUIManager.RCTWebView.Commands.reload, + null + ); }, /** @@ -157,6 +171,7 @@ var WebView = React.createClass({ var RCTWebView = createReactIOSNativeComponentClass({ validAttributes: merge(ReactIOSViewAttributes.UIView, { url: true, + javaScriptEnabledAndroid: true, }), uiViewClassName: 'RCTWebView', }); diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 83c90a1fdbe915..ed2c98fae02f5a 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -19,16 +19,13 @@ var StyleSheet = require('StyleSheet'); var Text = require('Text'); var View = require('View'); -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var invariant = require('invariant'); var keyMirror = require('keyMirror'); -var insetsDiffer = require('insetsDiffer'); -var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); var PropTypes = React.PropTypes; var RCTWebViewManager = require('NativeModules').WebViewManager; -var invariant = require('invariant'); - var BGWASH = 'rgba(255,255,255,0.8)'; var RCT_WEBVIEW_REF = 'webview'; @@ -87,12 +84,18 @@ var WebView = React.createClass({ html: PropTypes.string, renderError: PropTypes.func, // view to show if there's an error renderLoading: PropTypes.func, // loading indicator to show + bounces: PropTypes.bool, + scrollEnabled: PropTypes.bool, automaticallyAdjustContentInsets: PropTypes.bool, shouldInjectAJAXHandler: PropTypes.bool, contentInset: EdgeInsetsPropType, onNavigationStateChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load style: View.propTypes.style, + /** + * Used for android only, JS is enabled by default for WebView on iOS + */ + javaScriptEnabledAndroid: PropTypes.bool, }, getInitialState: function() { @@ -131,7 +134,7 @@ var WebView = React.createClass({ ); } - var webViewStyles = [styles.container, this.props.style]; + var webViewStyles = [styles.container, styles.webView, this.props.style]; if (this.state.viewState === WebViewState.LOADING || this.state.viewState === WebViewState.ERROR) { // if we're in either LOADING or ERROR states, don't show the webView @@ -145,6 +148,8 @@ var WebView = React.createClass({ style={webViewStyles} url={this.props.url} html={this.props.html} + bounces={this.props.bounces} + scrollEnabled={this.props.scrollEnabled} shouldInjectAJAXHandler={this.props.shouldInjectAJAXHandler} contentInset={this.props.contentInset} automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets} @@ -209,16 +214,7 @@ var WebView = React.createClass({ }, }); -var RCTWebView = createReactIOSNativeComponentClass({ - validAttributes: merge(ReactIOSViewAttributes.UIView, { - url: true, - html: true, - contentInset: {diff: insetsDiffer}, - automaticallyAdjustContentInsets: true, - shouldInjectAJAXHandler: true - }), - uiViewClassName: 'RCTWebView', -}); +var RCTWebView = requireNativeComponent('RCTWebView', WebView); var styles = StyleSheet.create({ container: { @@ -250,6 +246,9 @@ var styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, + webView: { + backgroundColor: '#ffffff', + } }); module.exports = WebView; diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index f783c34eaf147d..c009e8086f10d9 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -28,6 +28,7 @@ var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; var BackAndroid = require('BackAndroid'); +var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); var NavigatorInterceptor = require('NavigatorInterceptor'); @@ -43,8 +44,9 @@ var Subscribable = require('Subscribable'); var TimerMixin = require('react-timer-mixin'); var View = require('View'); -var getNavigatorContext = require('getNavigatorContext'); var clamp = require('clamp'); +var flattenStyle = require('flattenStyle'); +var getNavigatorContext = require('getNavigatorContext'); var invariant = require('invariant'); var keyMirror = require('keyMirror'); var merge = require('merge'); @@ -52,7 +54,17 @@ var rebound = require('rebound'); var PropTypes = React.PropTypes; -var OFF_SCREEN = {style: {opacity: 0}}; +// TODO: this is not ideal because there is no guarantee that the navigator +// is full screen, hwoever we don't have a good way to measure the actual +// size of the navigator right now, so this is the next best thing. +var SCREEN_WIDTH = Dimensions.get('window').width; +var SCREEN_HEIGHT = Dimensions.get('window').height; +var SCENE_DISABLED_NATIVE_PROPS = { + style: { + left: SCREEN_WIDTH, + opacity: 0, + }, +}; var __uid = 0; function getuid() { @@ -72,7 +84,7 @@ var styles = StyleSheet.create({ bottom: 0, top: 0, }, - currentScene: { + baseScene: { position: 'absolute', overflow: 'hidden', left: 0, @@ -80,11 +92,8 @@ var styles = StyleSheet.create({ bottom: 0, top: 0, }, - futureScene: { - overflow: 'hidden', - position: 'absolute', - left: 0, - opacity: 0, + disabledScene: { + left: SCREEN_WIDTH, }, transitioner: { flex: 1, @@ -135,11 +144,12 @@ var GESTURE_ACTIONS = [ * /> * ``` * - * ### Navigation Methods + * ### Navigator Methods * - * `Navigator` can be told to navigate in two ways. If you have a ref to - * the element, you can invoke several methods on it to trigger navigation: + * If you have a ref to the Navigator element, you can invoke several methods + * on it to trigger navigation: * + * - `getCurrentRoutes()` - returns the current list of routes * - `jumpBack()` - Jump backward without unmounting the current scene * - `jumpForward()` - Jump forward to the next scene in the route stack * - `jumpTo(route)` - Transition to an existing scene without unmounting @@ -156,18 +166,39 @@ var GESTURE_ACTIONS = [ * - `popToTop()` - Pop to the first scene in the stack, unmounting every * other scene * - * ### Navigator Object + * ### Navigation Context + * + * The navigator context object is made available to scenes through the + * `renderScene` function. Alternatively, any scene or component inside a + * Navigator can get the navigation context by calling + * `Navigator.getContext(this)`. * - * The navigator object is made available to scenes through the `renderScene` - * function. The object has all of the navigation methods on it, as well as a - * few utilities: + * Unlike the Navigator methods, the functions in navigation context do not + * directly control a specific navigator. Instead, the navigator context allows + * a scene to request navigation from its parents. Navigation requests will + * travel up through the hierarchy of Navigators, and will be resolved by the + * deepest active navigator. * - * - `parentNavigator` - a refrence to the parent navigator object that was - * passed in through props.navigator - * - `onWillFocus` - used to pass a navigation focus event up to the parent - * navigator - * - `onDidFocus` - used to pass a navigation focus event up to the parent - * navigator + * Navigation context objects contain the following: + * + * - `getCurrentRoutes()` - returns the routes for the closest navigator + * - `jumpBack()` - Jump backward without unmounting the current scene + * - `jumpForward()` - Jump forward to the next scene in the route stack + * - `jumpTo(route)` - Transition to an existing scene without unmounting + * - `parentNavigator` - a refrence to the parent navigation context + * - `push(route)` - Navigate forward to a new scene, squashing any scenes + * that you could `jumpForward` to + * - `pop()` - Transition back and unmount the current scene + * - `replace(route)` - Replace the current scene with a new route + * - `replaceAtIndex(route, index)` - Replace a scene as specified by an index + * - `replacePrevious(route)` - Replace the previous scene + * - `route` - The route that was used to render the scene with this context + * - `immediatelyResetRouteStack(routeStack)` - Reset every scene with an + * array of routes + * - `popToRoute(route)` - Pop to a particular scene, as specified by it's + * route. All scenes after it will be unmounted + * - `popToTop()` - Pop to the first scene in the stack, unmounting every + * other scene * */ var Navigator = React.createClass({ @@ -196,29 +227,28 @@ var Navigator = React.createClass({ renderScene: PropTypes.func.isRequired, /** - * Provide a single "route" to start on. A route is an arbitrary object - * that the navigator will use to identify each scene before rendering. - * Either initialRoute or initialRouteStack is required. + * Specify a route to start on. A route is an object that the navigator + * will use to identify each scene to render. `initialRoute` must be + * a route in the `initialRouteStack` if both props are provided. The + * `initialRoute` will default to the last item in the `initialRouteStack`. */ initialRoute: PropTypes.object, /** - * Provide a set of routes to initially mount the scenes for. Required if no - * initialRoute is provided + * Provide a set of routes to initially mount. Required if no initialRoute + * is provided. Otherwise, it will default to an array containing only the + * `initialRoute` */ initialRouteStack: PropTypes.arrayOf(PropTypes.object), /** - * Will emit the target route upon mounting and before each nav transition, - * overriding the handler in this.props.navigator. This overrides the onDidFocus - * handler that would be found in this.props.navigator + * Will emit the target route upon mounting and before each nav transition */ onWillFocus: PropTypes.func, /** * Will be called with the new route of each scene after the transition is - * complete or after the initial mounting. This overrides the onDidFocus - * handler that would be found in this.props.navigator + * complete or after the initial mounting */ onDidFocus: PropTypes.func, @@ -245,7 +275,8 @@ var Navigator = React.createClass({ }, contextTypes: { - navigator: PropTypes.object, + // TODO (t6707746) Re-enable this when owner context switches to parent context + // navigator: PropTypes.object, }, statics: { @@ -266,21 +297,18 @@ var Navigator = React.createClass({ }, getInitialState: function() { - var routeStack = this.props.initialRouteStack || []; - var initialRouteIndex = 0; - if (this.props.initialRoute && routeStack.length) { + var routeStack = this.props.initialRouteStack || [this.props.initialRoute]; + invariant( + routeStack.length >= 1, + 'Navigator requires props.initialRoute or props.initialRouteStack.' + ); + var initialRouteIndex = routeStack.length - 1; + if (this.props.initialRoute) { initialRouteIndex = routeStack.indexOf(this.props.initialRoute); invariant( initialRouteIndex !== -1, 'initialRoute is not in initialRouteStack.' ); - } else if (this.props.initialRoute) { - routeStack = [this.props.initialRoute]; - } else { - invariant( - routeStack.length >= 1, - 'Navigator requires props.initialRoute or props.initialRouteStack.' - ); } return { sceneConfigStack: routeStack.map( @@ -292,13 +320,11 @@ var Navigator = React.createClass({ // On first render, we will render every scene in the initialRouteStack updatingRangeStart: 0, updatingRangeLength: routeStack.length, - // Either animating or gesturing. - isAnimating: false, - jumpToIndex: routeStack.length - 1, presentedIndex: initialRouteIndex, - isResponderOnlyToBlockTouches: false, - fromIndex: initialRouteIndex, - toIndex: initialRouteIndex, + transitionFromIndex: null, + activeGesture: null, + pendingGestureProgress: null, + transitionQueue: [], }; }, @@ -306,31 +332,51 @@ var Navigator = React.createClass({ this.parentNavigator = getNavigatorContext(this) || this.props.navigator; this._subRouteFocus = []; this.navigatorContext = { + // Actions for child navigators or interceptors: setHandlerForRoute: this.setHandlerForRoute, request: this.request, + // Contextual utilities parentNavigator: this.parentNavigator, getCurrentRoutes: this.getCurrentRoutes, + // `route` is injected by NavigatorStaticContextContainer + + // Contextual nav actions + pop: this.requestPop, + popToRoute: this.requestPopTo, - // Legacy, imperitive nav actions. Use request when possible. + // Legacy, imperitive nav actions. Will transition these to contextual actions jumpBack: this.jumpBack, jumpForward: this.jumpForward, jumpTo: this.jumpTo, push: this.push, - pop: this.pop, replace: this.replace, replaceAtIndex: this.replaceAtIndex, replacePrevious: this.replacePrevious, replacePreviousAndPop: this.replacePreviousAndPop, immediatelyResetRouteStack: this.immediatelyResetRouteStack, resetTo: this.resetTo, - popToRoute: this.popToRoute, popToTop: this.popToTop, }; this._handlers = {}; - + this.springSystem = new rebound.SpringSystem(); + this.spring = this.springSystem.createSpring(); + this.spring.setRestSpeedThreshold(0.05); + this.spring.setCurrentValue(0).setAtRest(); + this.spring.addListener({ + onSpringEndStateChange: () => { + if (!this._interactionHandle) { + this._interactionHandle = this.createInteractionHandle(); + } + }, + onSpringUpdate: () => { + this._handleSpringUpdate(); + }, + onSpringAtRest: () => { + this._completeTransition(); + }, + }); this.panGesture = PanResponder.create({ - onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture, onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, onPanResponderGrant: this._handlePanResponderGrant, onPanResponderRelease: this._handlePanResponderRelease, @@ -349,6 +395,14 @@ var Navigator = React.createClass({ return this._handleRequest.apply(null, arguments); }, + requestPop: function(popToBeforeRoute) { + return this.request('pop', popToBeforeRoute); + }, + + requestPopTo: function(route) { + return this.request('popTo', route); + }, + _handleRequest: function(action, arg1, arg2) { var childHandler = this._handlers[this.state.presentedIndex]; if (childHandler && childHandler(action, arg1, arg2)) { @@ -356,7 +410,9 @@ var Navigator = React.createClass({ } switch (action) { case 'pop': - return this._handlePop(); + return this._handlePop(arg1); + case 'popTo': + return this._handlePopTo(arg1); case 'push': return this._handlePush(arg1); default: @@ -365,11 +421,40 @@ var Navigator = React.createClass({ } }, - _handlePop: function() { + _handlePop: function(popToBeforeRoute) { + if (popToBeforeRoute) { + var popToBeforeRouteIndex = this.state.routeStack.indexOf(popToBeforeRoute); + if (popToBeforeRouteIndex === -1) { + return false; + } + invariant( + popToBeforeRouteIndex <= this.state.presentedIndex, + 'Cannot pop past a route that is forward in the navigator' + ); + this._popN(this.state.presentedIndex - popToBeforeRouteIndex + 1); + return true; + } if (this.state.presentedIndex === 0) { return false; } - this._popN(1); + this.pop(); + return true; + }, + + _handlePopTo: function(destRoute) { + if (destRoute) { + var hasRoute = this.state.routeStack.indexOf(destRoute) !== -1; + if (hasRoute) { + this.popToRoute(destRoute); + return true; + } else { + return false; + } + } + if (this.state.presentedIndex === 0) { + return false; + } + this.pop(); return true; }, @@ -382,20 +467,8 @@ var Navigator = React.createClass({ this._handlers[this.state.routeStack.indexOf(route)] = handler; }, - _configureSpring: function(animationConfig) { - var config = this.spring.getSpringConfig(); - config.friction = animationConfig.springFriction; - config.tension = animationConfig.springTension; - }, - componentDidMount: function() { - this.springSystem = new rebound.SpringSystem(); - this.spring = this.springSystem.createSpring(); - this.spring.setRestSpeedThreshold(0.05); - var animationConfig = this.state.sceneConfigStack[this.state.presentedIndex]; - animationConfig && this._configureSpring(animationConfig); - this.spring.addListener(this); - this.onSpringUpdate(); + this._handleSpringUpdate(); this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]); if (this.parentNavigator) { this.parentNavigator.setHandler(this._handleRequest); @@ -415,7 +488,7 @@ var Navigator = React.createClass({ }, _handleAndroidBackPress: function() { - var didPop = this.pop(); + var didPop = this.requestPop(); if (!didPop) { BackAndroid.exitApp(); } @@ -439,96 +512,120 @@ var Navigator = React.createClass({ updatingRangeStart: 0, updatingRangeLength: nextRouteStack.length, presentedIndex: destIndex, - jumpToIndex: destIndex, - toIndex: destIndex, - fromIndex: destIndex, + activeGesture: null, + transitionFromIndex: null, + transitionQueue: [], }, () => { - this.onSpringUpdate(); + this._handleSpringUpdate(); }); }, + _transitionTo: function(destIndex, velocity, jumpSpringTo, cb) { + if (destIndex === this.state.presentedIndex) { + return; + } + if (this.state.transitionFromIndex !== null) { + this.state.transitionQueue.push({ + destIndex, + velocity, + cb, + }); + return; + } + this.state.transitionFromIndex = this.state.presentedIndex; + this.state.presentedIndex = destIndex; + this.state.transitionCb = cb; + this._onAnimationStart(); + if (AnimationsDebugModule) { + AnimationsDebugModule.startRecordingFps(); + } + var sceneConfig = this.state.sceneConfigStack[this.state.transitionFromIndex] || + this.state.sceneConfigStack[this.state.presentedIndex]; + invariant( + sceneConfig, + 'Cannot configure scene at index ' + this.state.transitionFromIndex + ); + if (jumpSpringTo != null) { + this.spring.setCurrentValue(jumpSpringTo); + } + this.spring.setOvershootClampingEnabled(true); + this.spring.getSpringConfig().friction = sceneConfig.springFriction; + this.spring.getSpringConfig().tension = sceneConfig.springTension; + this.spring.setVelocity(velocity || sceneConfig.defaultTransitionVelocity); + this.spring.setEndValue(1); + var willFocusRoute = this._subRouteFocus[this.state.presentedIndex] || this.state.routeStack[this.state.presentedIndex]; + this._emitWillFocus(willFocusRoute); + }, + /** - * TODO: Accept callback for spring completion. + * This happens for each frame of either a gesture or a transition. If both are + * happening, we only set values for the transition and the gesture will catch up later */ - _requestTransitionTo: function(topOfStack) { - if (topOfStack !== this.state.presentedIndex) { - invariant(!this.state.isAnimating, 'Cannot navigate while transitioning'); - this.state.fromIndex = this.state.presentedIndex; - this.state.toIndex = topOfStack; - this.spring.setOvershootClampingEnabled(false); - if (AnimationsDebugModule) { - AnimationsDebugModule.startRecordingFps(); - } - this._transitionToToIndexWithVelocity( - this.state.sceneConfigStack[this.state.fromIndex].defaultTransitionVelocity + _handleSpringUpdate: function() { + // Prioritize handling transition in progress over a gesture: + if (this.state.transitionFromIndex != null) { + this._transitionBetween( + this.state.transitionFromIndex, + this.state.presentedIndex, + this.spring.getCurrentValue() + ); + } else if (this.state.activeGesture != null) { + this._transitionBetween( + this.state.presentedIndex, + this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture), + this.spring.getCurrentValue() ); } }, /** - * `onSpring*` spring delegate. Wired up via `spring.addListener(this)` + * This happens at the end of a transition started by transitionTo, and when the spring catches up to a pending gesture */ - onSpringEndStateChange: function() { - if (!this._interactionHandle) { - this._interactionHandle = this.createInteractionHandle(); + _completeTransition: function() { + if (this.spring.getCurrentValue() !== 1) { + // The spring has finished catching up to a gesture in progress. Remove the pending progress + // and we will be in a normal activeGesture state + if (this.state.pendingGestureProgress) { + this.state.pendingGestureProgress = null; + } + return; } - }, - - onSpringUpdate: function() { - this._transitionBetween( - this.state.fromIndex, - this.state.toIndex, - this.spring.getCurrentValue() - ); - }, - - onSpringAtRest: function() { - this.state.isAnimating = false; - this._completeTransition(); + this._onAnimationEnd(); + var presentedIndex = this.state.presentedIndex; + var didFocusRoute = this._subRouteFocus[presentedIndex] || this.state.routeStack[presentedIndex]; + this._emitDidFocus(didFocusRoute); + if (AnimationsDebugModule) { + AnimationsDebugModule.stopRecordingFps(Date.now()); + } + this.state.transitionFromIndex = null; this.spring.setCurrentValue(0).setAtRest(); + this._hideScenes(); + if (this.state.transitionCb) { + this.state.transitionCb(); + this.state.transitionCb = null; + } if (this._interactionHandle) { this.clearInteractionHandle(this._interactionHandle); this._interactionHandle = null; } - }, - - _completeTransition: function() { - if (this.spring.getCurrentValue() === 1) { - var presentedIndex = this.state.toIndex; - this.state.presentedIndex = presentedIndex; - this.state.fromIndex = presentedIndex; - var didFocusRoute = this._subRouteFocus[presentedIndex] || this.state.routeStack[presentedIndex]; - this._emitDidFocus(didFocusRoute); - this._removePoppedRoutes(); - if (AnimationsDebugModule) { - AnimationsDebugModule.stopRecordingFps(Date.now()); - } - } else { - this.state.fromIndex = this.state.presentedIndex; - this.state.toIndex = this.state.presentedIndex; + if (this.state.pendingGestureProgress) { + // A transition completed, but there is already another gesture happening. + // Enable the scene and set the spring to catch up with the new gesture + var gestureToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + this._enableScene(gestureToIndex); + this.spring.setEndValue(this.state.pendingGestureProgress); + return; + } + if (this.state.transitionQueue.length) { + var queuedTransition = this.state.transitionQueue.shift(); + this._enableScene(queuedTransition.destIndex); + this._transitionTo( + queuedTransition.destIndex, + queuedTransition.velocity, + null, + queuedTransition.cb + ); } - this._hideOtherScenes(this.state.presentedIndex); - }, - - _transitionToToIndexWithVelocity: function(v) { - this._configureSpring( - // For visual consistency, the from index is always used to configure the spring - this.state.sceneConfigStack[this.state.fromIndex] - ); - this.state.isAnimating = true; - this.spring.setVelocity(v); - this.spring.setEndValue(1); - var willFocusRoute = this._subRouteFocus[this.state.toIndex] || this.state.routeStack[this.state.toIndex]; - this._emitWillFocus(willFocusRoute); - }, - - _transitionToFromIndexWithVelocity: function(v) { - this._configureSpring( - this.state.sceneConfigStack[this.state.fromIndex] - ); - this.state.isAnimating = true; - this.spring.setVelocity(v); - this.spring.setEndValue(0); }, _emitDidFocus: function(route) { @@ -538,7 +635,8 @@ var Navigator = React.createClass({ this._lastDidFocus = route; if (this.props.onDidFocus) { this.props.onDidFocus(route); - } else if (this.parentNavigator && this.parentNavigator.onDidFocus) { + } + if (this.parentNavigator && this.parentNavigator.onDidFocus) { this.parentNavigator.onDidFocus(route); } }, @@ -554,33 +652,86 @@ var Navigator = React.createClass({ } if (this.props.onWillFocus) { this.props.onWillFocus(route); - } else if (this.parentNavigator && this.parentNavigator.onWillFocus) { + } + if (this.parentNavigator && this.parentNavigator.onWillFocus) { this.parentNavigator.onWillFocus(route); } }, /** - * Does not delete the scenes - merely hides them. + * Hides scenes that we are not currently on or transitioning from */ - _hideOtherScenes: function(activeIndex) { + _hideScenes: function() { for (var i = 0; i < this.state.routeStack.length; i++) { - if (i === activeIndex) { + if (i === this.state.presentedIndex || i === this.state.transitionFromIndex) { continue; } - var sceneRef = 'scene_' + i; - this.refs[sceneRef] && - this.refs['scene_' + i].setNativeProps(OFF_SCREEN); + this._disableScene(i); } }, /** - * Becomes the responder on touch start (capture) while animating so that it - * blocks all touch interactions inside of it. However, this responder lock - * means nothing more than that. We record if the sole reason for being - * responder is to block interactions (`isResponderOnlyToBlockTouches`). + * Push a scene off the screen, so that opacity:0 scenes will not block touches sent to the presented scenes */ - _handleStartShouldSetPanResponderCapture: function(e, gestureState) { - return this.state.isAnimating; + _disableScene: function(sceneIndex) { + this.refs['scene_' + sceneIndex] && + this.refs['scene_' + sceneIndex].setNativeProps(SCENE_DISABLED_NATIVE_PROPS); + }, + + /** + * Put the scene back into the state as defined by props.sceneStyle, so transitions can happen normally + */ + _enableScene: function(sceneIndex) { + // First, determine what the defined styles are for scenes in this navigator + var sceneStyle = flattenStyle(this.props.sceneStyle); + // Then restore the left value for this scene + var enabledSceneNativeProps = { + left: sceneStyle.left, + }; + if (sceneIndex !== this.state.transitionFromIndex && + sceneIndex !== this.state.presentedIndex) { + // If we are not in a transition from this index, make sure opacity is 0 + // to prevent the enabled scene from flashing over the presented scene + enabledSceneNativeProps.opacity = 0; + } + this.refs['scene_' + sceneIndex] && + this.refs['scene_' + sceneIndex].setNativeProps(enabledSceneNativeProps); + }, + + _onAnimationStart: function() { + var fromIndex = this.state.presentedIndex; + var toIndex = this.state.presentedIndex; + if (this.state.transitionFromIndex != null) { + fromIndex = this.state.transitionFromIndex; + } else if (this.state.activeGesture) { + toIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + } + this._setRenderSceneToHarwareTextureAndroid(fromIndex, true); + this._setRenderSceneToHarwareTextureAndroid(toIndex, true); + var navBar = this._navBar; + if (navBar && navBar.onAnimationStart) { + navBar.onAnimationStart(fromIndex, toIndex); + } + }, + + _onAnimationEnd: function() { + var max = this.state.routeStack.length - 1; + for (var index = 0; index <= max; index++) { + this._setRenderSceneToHarwareTextureAndroid(index, false); + } + + var navBar = this._navBar; + if (navBar && navBar.onAnimationEnd) { + navBar.onAnimationEnd(); + } + }, + + _setRenderSceneToHarwareTextureAndroid: function(sceneIndex, shouldRenderToHardwareTexture) { + var viewAtIndex = this.refs['scene_' + sceneIndex]; + if (viewAtIndex === null || viewAtIndex === undefined) { + return; + } + viewAtIndex.setNativeProps({renderToHardwareTextureAndroid: shouldRenderToHardwareTexture}); }, _handleMoveShouldSetPanResponder: function(e, gestureState) { @@ -600,17 +751,12 @@ var Navigator = React.createClass({ _handlePanResponderGrant: function(e, gestureState) { invariant( - this._expectingGestureGrant || this.state.isAnimating, + this._expectingGestureGrant, 'Responder granted unexpectedly.' ); - this._activeGestureAction = this._expectingGestureGrant; + this._attachGesture(this._expectingGestureGrant); + this._onAnimationStart(); this._expectingGestureGrant = null; - this.state.isResponderOnlyToBlockTouches = this.state.isAnimating; - if (!this.state.isAnimating) { - this.state.fromIndex = this.state.presentedIndex; - var gestureSceneDelta = this._deltaForGestureAction(this._activeGestureAction); - this.state.toIndex = this.state.presentedIndex + gestureSceneDelta; - } }, _deltaForGestureAction: function(gestureAction) { @@ -628,13 +774,13 @@ var Navigator = React.createClass({ _handlePanResponderRelease: function(e, gestureState) { var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; - var releaseGestureAction = this._activeGestureAction; - this._activeGestureAction = null; - if (this.state.isResponderOnlyToBlockTouches) { - this.state.isResponderOnlyToBlockTouches = false; + var releaseGestureAction = this.state.activeGesture; + if (!releaseGestureAction) { + // The gesture may have been detached while responder, so there is no action here return; } var releaseGesture = sceneConfig.gestures[releaseGestureAction]; + var destIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); if (this.spring.getCurrentValue() === 0) { // The spring is at zero, so the gesture is already complete this.spring.setCurrentValue(0).setAtRest(); @@ -657,38 +803,89 @@ var Navigator = React.createClass({ var hasGesturedEnoughToComplete = gestureDistance > releaseGesture.fullDistance * releaseGesture.stillCompletionRatio; transitionVelocity = hasGesturedEnoughToComplete ? releaseGesture.snapVelocity : -releaseGesture.snapVelocity; } - this.spring.setOvershootClampingEnabled(true); if (transitionVelocity < 0 || this._doesGestureOverswipe(releaseGestureAction)) { - this._transitionToFromIndexWithVelocity(transitionVelocity); + // This gesture is to an overswiped region or does not have enough velocity to complete + // If we are currently mid-transition, then this gesture was a pending gesture. Because this gesture takes no action, we can stop here + if (this.state.transitionFromIndex == null) { + // There is no current transition, so we need to transition back to the presented index + var transitionBackToPresentedIndex = this.state.presentedIndex; + // slight hack: change the presented index for a moment in order to transitionTo correctly + this.state.presentedIndex = destIndex; + this._transitionTo( + transitionBackToPresentedIndex, + - transitionVelocity, + 1 - this.spring.getCurrentValue() + ); + } } else { - this._transitionToToIndexWithVelocity(transitionVelocity); + // The gesture has enough velocity to complete, so we transition to the gesture's destination + this._transitionTo(destIndex, transitionVelocity); } + this._detachGesture(); }, _handlePanResponderTerminate: function(e, gestureState) { - this._activeGestureAction = null; - this.state.isResponderOnlyToBlockTouches = false; - this._transitionToFromIndexWithVelocity(0); + var destIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + this._detachGesture(); + var transitionBackToPresentedIndex = this.state.presentedIndex; + // slight hack: change the presented index for a moment in order to transitionTo correctly + this.state.presentedIndex = destIndex; + this._transitionTo( + transitionBackToPresentedIndex, + null, + 1 - this.spring.getCurrentValue() + ); + }, + + _attachGesture: function(gestureId) { + this.state.activeGesture = gestureId; + var gesturingToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + this._enableScene(gesturingToIndex); + }, + + _detachGesture: function() { + this.state.activeGesture = null; + this.state.pendingGestureProgress = null; + this._hideScenes(); }, _handlePanResponderMove: function(e, gestureState) { - if (!this.state.isResponderOnlyToBlockTouches) { - var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; - var gesture = sceneConfig.gestures[this._activeGestureAction]; - var isTravelVertical = gesture.direction === 'top-to-bottom' || gesture.direction === 'bottom-to-top'; - var isTravelInverted = gesture.direction === 'right-to-left' || gesture.direction === 'bottom-to-top'; - var distance = isTravelVertical ? gestureState.dy : gestureState.dx; - distance = isTravelInverted ? - distance : distance; - var gestureDetectMovement = gesture.gestureDetectMovement; - var nextProgress = (distance - gestureDetectMovement) / - (gesture.fullDistance - gestureDetectMovement); - if (this._doesGestureOverswipe(this._activeGestureAction)) { - var frictionConstant = gesture.overswipe.frictionConstant; - var frictionByDistance = gesture.overswipe.frictionByDistance; - var frictionRatio = 1 / ((frictionConstant) + (Math.abs(nextProgress) * frictionByDistance)); - nextProgress *= frictionRatio; - } - this.spring.setCurrentValue(clamp(0, nextProgress, 1)); + var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; + if (this.state.activeGesture) { + var gesture = sceneConfig.gestures[this.state.activeGesture]; + return this._moveAttachedGesture(gesture, gestureState); + } + var matchedGesture = this._matchGestureAction(sceneConfig.gestures, gestureState); + if (matchedGesture) { + this._attachGesture(matchedGesture); + } + }, + + _moveAttachedGesture: function(gesture, gestureState) { + var isTravelVertical = gesture.direction === 'top-to-bottom' || gesture.direction === 'bottom-to-top'; + var isTravelInverted = gesture.direction === 'right-to-left' || gesture.direction === 'bottom-to-top'; + var distance = isTravelVertical ? gestureState.dy : gestureState.dx; + distance = isTravelInverted ? - distance : distance; + var gestureDetectMovement = gesture.gestureDetectMovement; + var nextProgress = (distance - gestureDetectMovement) / + (gesture.fullDistance - gestureDetectMovement); + if (nextProgress < 0 && gesture.isDetachable) { + this._detachGesture(); + this.spring.setCurrentValue(0); + } + if (this._doesGestureOverswipe(this.state.activeGesture)) { + var frictionConstant = gesture.overswipe.frictionConstant; + var frictionByDistance = gesture.overswipe.frictionByDistance; + var frictionRatio = 1 / ((frictionConstant) + (Math.abs(nextProgress) * frictionByDistance)); + nextProgress *= frictionRatio; + } + nextProgress = clamp(0, nextProgress, 1); + if (this.state.transitionFromIndex != null) { + this.state.pendingGestureProgress = nextProgress; + } else if (this.state.pendingGestureProgress) { + this.spring.setEndValue(nextProgress); + } else { + this.spring.setCurrentValue(nextProgress); } }, @@ -696,9 +893,6 @@ var Navigator = React.createClass({ if (!gestures) { return null; } - if (this.state.isResponderOnlyToBlockTouches || this.state.isAnimating) { - return null; - } var matchedGesture = null; GESTURE_ACTIONS.some((gestureName) => { var gesture = gestures[gestureName]; @@ -715,13 +909,17 @@ var Navigator = React.createClass({ var travelDist = isTravelVertical ? gestureState.dy : gestureState.dx; var oppositeAxisTravelDist = isTravelVertical ? gestureState.dx : gestureState.dy; + var edgeHitWidth = gesture.edgeHitWidth; if (isTravelInverted) { currentLoc = -currentLoc; travelDist = -travelDist; oppositeAxisTravelDist = -oppositeAxisTravelDist; + edgeHitWidth = isTravelVertical ? + -(SCREEN_HEIGHT - edgeHitWidth) : + -(SCREEN_WIDTH - edgeHitWidth); } var moveStartedInRegion = gesture.edgeHitWidth == null || - currentLoc < gesture.edgeHitWidth; + currentLoc < edgeHitWidth; var moveTravelledFarEnough = travelDist >= gesture.gestureDetectMovement && travelDist > oppositeAxisTravelDist * gesture.directionRatio; @@ -739,7 +937,7 @@ var Navigator = React.createClass({ return; } // Use toIndex animation when we move forwards. Use fromIndex when we move back - var sceneConfigIndex = this.state.presentedIndex < toIndex ? toIndex : fromIndex; + var sceneConfigIndex = fromIndex < toIndex ? toIndex : fromIndex; var sceneConfig = this.state.sceneConfigStack[sceneConfigIndex]; // this happens for overswiping when there is no scene at toIndex if (!sceneConfig) { @@ -774,10 +972,6 @@ var Navigator = React.createClass({ this.state.updatingRangeLength = this.state.routeStack.length; }, - _canNavigate: function() { - return !this.state.isAnimating; - }, - _getDestIndexWithinBounds: function(n) { var currentIndex = this.state.presentedIndex; var destIndex = currentIndex + n; @@ -795,17 +989,14 @@ var Navigator = React.createClass({ _jumpN: function(n) { var destIndex = this._getDestIndexWithinBounds(n); - if (!this._canNavigate()) { - return; // It's busy animating or transitioning. - } var requestTransitionAndResetUpdatingRange = () => { - this._requestTransitionTo(destIndex); + this._enableScene(destIndex); + this._transitionTo(destIndex); this._resetUpdatingRange(); }; this.setState({ updatingRangeStart: destIndex, updatingRangeLength: 1, - toIndex: destIndex, }, requestTransitionAndResetUpdatingRange); }, @@ -828,52 +1019,52 @@ var Navigator = React.createClass({ push: function(route) { invariant(!!route, 'Must supply route to push'); - if (!this._canNavigate()) { - return; // It's busy animating or transitioning. - } 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), ]); var requestTransitionAndResetUpdatingRange = () => { - this._requestTransitionTo(nextStack.length - 1); + this._enableScene(destIndex); + this._transitionTo(destIndex); this._resetUpdatingRange(); }; - var navigationState = { - toRoute: route, - fromRoute: this.state.routeStack[this.state.routeStack.length - 1], - }; this.setState({ idStack: nextIDStack, routeStack: nextStack, sceneConfigStack: nextAnimationConfigStack, - jumpToIndex: nextStack.length - 1, updatingRangeStart: nextStack.length - 1, updatingRangeLength: 1, }, requestTransitionAndResetUpdatingRange); }, _popN: function(n) { - if (n === 0 || !this._canNavigate()) { + if (n === 0) { return; } invariant( this.state.presentedIndex - n >= 0, 'Cannot pop below zero' ); - this.state.jumpToIndex = this.state.presentedIndex - n; - this._requestTransitionTo( - this.state.presentedIndex - n + var popIndex = this.state.presentedIndex - n; + this._enableScene(popIndex); + this._transitionTo( + popIndex, + null, // default velocity + null, // no spring jumping + () => { + this._cleanScenesPastIndex(popIndex); + } ); }, pop: function() { - return this.request('pop'); + this._popN(1); }, /** @@ -882,7 +1073,7 @@ var Navigator = React.createClass({ * `index` specifies the route in the stack that should be replaced. * If it's negative, it counts from the back. */ - replaceAtIndex: function(route, index) { + replaceAtIndex: function(route, index, cb) { invariant(!!route, 'Must supply route to replace'); if (index < 0) { index += this.state.routeStack.length; @@ -913,6 +1104,7 @@ var Navigator = React.createClass({ this._emitWillFocus(route); this._emitDidFocus(route); } + cb && cb(); }); }, @@ -940,7 +1132,7 @@ var Navigator = React.createClass({ indexOfRoute !== -1, 'Calling pop to route for a route that doesn\'t exist!' ); - return this.state.routeStack.length - indexOfRoute - 1; + return this.state.presentedIndex - indexOfRoute; }, popToRoute: function(route) { @@ -949,7 +1141,7 @@ var Navigator = React.createClass({ }, replacePreviousAndPop: function(route) { - if (this.state.routeStack.length < 2 || !this._canNavigate()) { + if (this.state.routeStack.length < 2) { return; } this.replacePrevious(route); @@ -958,10 +1150,9 @@ var Navigator = React.createClass({ resetTo: function(route) { invariant(!!route, 'Must supply route to push'); - if (this._canNavigate()) { - this.replaceAtIndex(route, 0); + this.replaceAtIndex(route, 0, () => { this.popToRoute(route); - } + }); }, getCurrentRoutes: function() { @@ -977,8 +1168,8 @@ var Navigator = React.createClass({ this.props.onItemRef && this.props.onItemRef(ref, itemIndex); }, - _removePoppedRoutes: function() { - var newStackLength = this.state.jumpToIndex + 1; + _cleanScenesPastIndex: function(index) { + var newStackLength = index + 1; // Remove any unneeded rendered routes. if (newStackLength < this.state.routeStack.length) { var updatingRangeStart = newStackLength; // One past the top @@ -1004,8 +1195,7 @@ var Navigator = React.createClass({ // To avoid visual glitches, we never re-render scenes during a transition. // We assume that `state.updatingRangeLength` will have a length during the // initial render of any scene - var shouldRenderScenes = !this.state.isAnimating && - this.state.updatingRangeLength !== 0; + var shouldRenderScenes = this.state.updatingRangeLength !== 0; if (shouldRenderScenes) { return ( @@ -1070,13 +1260,18 @@ var Navigator = React.createClass({ route, sceneNavigatorContext ); - var initialSceneStyle = i === this.state.presentedIndex ? - styles.currentScene : styles.futureScene; + var disabledSceneStyle = null; + if (i !== this.state.presentedIndex) { + disabledSceneStyle = styles.disabledScene; + } return ( + onStartShouldSetResponderCapture={() => { + return i !== this.state.presentedIndex; + }} + style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}> {React.cloneElement(child, { ref: this._handleItemRef.bind(null, this.state.idStack[i]), })} diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js index b4bad9982ce931..a43f2dafdebba9 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 invariant = require('invariant'); + var Interpolators = NavigatorBreadcrumbNavigationBarStyles.Interpolators; var PropTypes = React.PropTypes; @@ -99,6 +101,10 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ var oldDistToCenter = index - fromIndex; var newDistToCenter = index - toIndex; var interpolate; + invariant( + Interpolators[index], + 'Cannot find breadcrumb interpolators for ' + index + ); if (oldDistToCenter > 0 && newDistToCenter === 0 || newDistToCenter > 0 && oldDistToCenter === 0) { interpolate = Interpolators[index].RightToCenter; @@ -138,6 +144,36 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ } }, + onAnimationStart: function(fromIndex, toIndex) { + var max = Math.max(fromIndex, toIndex); + var min = Math.min(fromIndex, toIndex); + for (var index = min; index <= max; index++) { + this._setRenderViewsToHardwareTextureAndroid(index, true); + } + }, + + onAnimationEnd: function() { + var max = this.props.navState.routeStack.length - 1; + for (var index = 0; index <= max; index++) { + this._setRenderViewsToHardwareTextureAndroid(index, false); + } + }, + + _setRenderViewsToHardwareTextureAndroid: function(index, renderToHardwareTexture) { + var props = { + renderToHardwareTextureAndroid: renderToHardwareTexture, + }; + + this.refs['crumb_' + index].setNativeProps(props); + this.refs['icon_' + index].setNativeProps(props); + this.refs['separator_' + index].setNativeProps(props); + this.refs['title_' + index].setNativeProps(props); + var right = this.refs['right_' + index]; + if (right) { + right.setNativeProps(props); + } + }, + render: function() { var navState = this.props.navState; var icons = navState && navState.routeStack.map(this._renderOrReturnBreadcrumb); @@ -260,7 +296,7 @@ var styles = StyleSheet.create({ height: NavigatorNavigationBarStyles.General.TotalNavHeight, top: 0, left: 0, - width: NavigatorNavigationBarStyles.General.ScreenWidth, + right: 0, }, }); diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBarStyles.ios.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBarStyles.ios.js index 7bc78ee929390a..69d0d52a254870 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBarStyles.ios.js +++ b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBarStyles.ios.js @@ -26,12 +26,13 @@ */ 'use strict'; +var Dimensions = require('Dimensions'); var NavigatorNavigationBarStyles = require('NavigatorNavigationBarStyles'); var buildStyleInterpolator = require('buildStyleInterpolator'); var merge = require('merge'); -var SCREEN_WIDTH = NavigatorNavigationBarStyles.General.ScreenWidth; +var SCREEN_WIDTH = Dimensions.get('window').width; var STATUS_BAR_HEIGHT = NavigatorNavigationBarStyles.General.StatusBarHeight; var NAV_BAR_HEIGHT = NavigatorNavigationBarStyles.General.NavBarHeight; @@ -39,7 +40,6 @@ var SPACING = 4; var ICON_WIDTH = 40; var SEPARATOR_WIDTH = 9; var CRUMB_WIDTH = ICON_WIDTH + SEPARATOR_WIDTH; -var RIGHT_BUTTON_WIDTH = 58; var OPACITY_RATIO = 100; var ICON_INACTIVE_OPACITY = 0.6; @@ -74,18 +74,17 @@ var TITLE_BASE = { // For first title styles, make sure first title is centered var FIRST_TITLE_BASE = merge(TITLE_BASE, { left: 0, + right: 0, alignItems: 'center', - width: SCREEN_WIDTH, height: NAV_BAR_HEIGHT, }); var RIGHT_BUTTON_BASE = { position: 'absolute', top: STATUS_BAR_HEIGHT, - left: SCREEN_WIDTH - SPACING - RIGHT_BUTTON_WIDTH, + right: SPACING, overflow: 'hidden', opacity: 1, - width: RIGHT_BUTTON_WIDTH, height: NAV_BAR_HEIGHT, backgroundColor: 'transparent', }; diff --git a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js index dcc5d43efa9c25..efff4c9dc729e1 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js +++ b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js @@ -78,9 +78,11 @@ var NavigatorInterceptor = React.createClass({ } switch (action) { case 'pop': - return this.props.onPopRequest && this.props.onPopRequest(action, arg1, arg2); + return this.props.onPopRequest && this.props.onPopRequest(arg1, arg2); + case 'popTo': + return this.props.onPopToRequest && this.props.onPopToRequest(arg1, arg2); case 'push': - return this.props.onPushRequest && this.props.onPushRequest(action, arg1, arg2); + return this.props.onPushRequest && this.props.onPushRequest(arg1, arg2); default: return false; } diff --git a/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js index 20c43a94a45b2d..172819de2fa280 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js +++ b/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js @@ -192,7 +192,7 @@ var styles = StyleSheet.create({ height: NavigatorNavigationBarStyles.General.TotalNavHeight, top: 0, left: 0, - width: NavigatorNavigationBarStyles.General.ScreenWidth, + right: 0, backgroundColor: 'transparent', }, }); diff --git a/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js b/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js index 266c5df2414d02..fff116745bed98 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js +++ b/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStyles.ios.js @@ -169,7 +169,6 @@ module.exports = { NavBarHeight: NAV_BAR_HEIGHT, StatusBarHeight: STATUS_BAR_HEIGHT, TotalNavHeight: NAV_HEIGHT, - ScreenWidth: SCREEN_WIDTH, }, Interpolators, Stages, diff --git a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js index ac16542adc2ea7..3b4666b9388ce5 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js +++ b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js @@ -101,6 +101,30 @@ var FadeToTheLeft = { }, }; +var FadeIn = { + opacity: { + from: 0, + to: 1, + min: 0.5, + max: 1, + type: 'linear', + extrapolate: false, + round: 100, + }, +}; + +var FadeOut = { + opacity: { + from: 1, + to: 0, + min: 0, + max: 0.5, + type: 'linear', + extrapolate: false, + round: 100, + }, +}; + var ToTheLeft = { transformTranslate: { from: {x: 0, y: 0, z: 0}, @@ -115,8 +139,17 @@ var ToTheLeft = { value: 1.0, type: 'constant', }, -}; + translateX: { + from: 0, + to: -Dimensions.get('window').width, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, +}; var FromTheRight = { opacity: { @@ -236,6 +269,43 @@ var FromTheFront = { }, }; +var ToTheBackAndroid = { + opacity: { + value: 1, + type: 'constant', + }, +}; + +var FromTheFrontAndroid = { + opacity: { + from: 0, + to: 1, + min: 0, + max: 1, + type: 'linear', + extrapolate: false, + round: 100, + }, + transformTranslate: { + from: {x: 0, y: 50, z: 0}, + to: {x: 0, y: 0, z: 0}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + translateY: { + from: 50, + to: 0, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, +}; + var BaseOverswipeConfig = { frictionConstant: 1, frictionByDistance: 1.5, @@ -243,6 +313,9 @@ var BaseOverswipeConfig = { var BaseLeftToRightGesture = { + // If the gesture can end and restart during one continuous touch + isDetachable: false, + // How far the swipe must drag to start transitioning gestureDetectMovement: 2, @@ -316,6 +389,22 @@ var NavigatorSceneConfigs = { out: buildStyleInterpolator(ToTheBack), }, }, + FloatFromBottomAndroid: { + ...BaseConfig, + gestures: null, + animationInterpolators: { + into: buildStyleInterpolator(FromTheFrontAndroid), + out: buildStyleInterpolator(ToTheBackAndroid), + }, + }, + FadeAndroid: { + ...BaseConfig, + gestures: null, + animationInterpolators: { + into: buildStyleInterpolator(FadeIn), + out: buildStyleInterpolator(FadeOut), + }, + }, HorizontalSwipeJump: { ...BaseConfig, gestures: { @@ -323,11 +412,13 @@ var NavigatorSceneConfigs = { ...BaseLeftToRightGesture, overswipe: BaseOverswipeConfig, edgeHitWidth: null, + isDetachable: true, }, jumpForward: { ...BaseRightToLeftGesture, overswipe: BaseOverswipeConfig, edgeHitWidth: null, + isDetachable: true, }, }, animationInterpolators: { diff --git a/Libraries/Fetch/fetch.js b/Libraries/Fetch/fetch.js index 241172669f9066..829f7c4256d63a 100644 --- a/Libraries/Fetch/fetch.js +++ b/Libraries/Fetch/fetch.js @@ -47,6 +47,23 @@ var self = {}; return } + function normalizeName(name) { + if (typeof name !== 'string') { + name = name.toString(); + } + if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { + throw new TypeError('Invalid character in header field name') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = value.toString(); + } + return value + } + function Headers(headers) { this.map = {} @@ -66,7 +83,8 @@ var self = {}; } Headers.prototype.append = function(name, value) { - name = name.toLowerCase() + name = normalizeName(name) + value = normalizeValue(value) var list = this.map[name] if (!list) { list = [] @@ -76,24 +94,24 @@ var self = {}; } Headers.prototype['delete'] = function(name) { - delete this.map[name.toLowerCase()] + delete this.map[normalizeName(name)] } Headers.prototype.get = function(name) { - var values = this.map[name.toLowerCase()] + var values = this.map[normalizeName(name)] return values ? values[0] : null } Headers.prototype.getAll = function(name) { - return this.map[name.toLowerCase()] || [] + return this.map[normalizeName(name)] || [] } Headers.prototype.has = function(name) { - return this.map.hasOwnProperty(name.toLowerCase()) + return this.map.hasOwnProperty(normalizeName(name)) } Headers.prototype.set = function(name, value) { - this.map[name.toLowerCase()] = [value] + this.map[normalizeName(name)] = [normalizeValue(value)] } // Instead of iterable for now. @@ -134,22 +152,51 @@ var self = {}; return fileReaderReady(reader) } - var blobSupport = 'FileReader' in self && 'Blob' in self && (function() { - try { - new Blob(); - return true - } catch(e) { - return false - } - })(); + var support = { + blob: 'FileReader' in self && 'Blob' in self && (function() { + try { + new Blob(); + return true + } catch(e) { + return false + } + })(), + formData: 'FormData' in self + } function Body() { this.bodyUsed = false - if (blobSupport) { + + this._initBody = function(body) { + this._bodyInit = body + if (typeof body === 'string') { + this._bodyText = body + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body + } else if (!body) { + this._bodyText = '' + } else { + throw new Error('unsupported BodyInit type') + } + } + + if (support.blob) { this.blob = function() { var rejected = consumed(this) - return rejected ? rejected : Promise.resolve(this._bodyBlob) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } } this.arrayBuffer = function() { @@ -157,7 +204,18 @@ var self = {}; } this.text = function() { - return this.blob().then(readBlobAsText) + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } } } else { this.text = function() { @@ -166,7 +224,7 @@ var self = {}; } } - if ('FormData' in self) { + if (support.formData) { this.formData = function() { return this.text().then(decode) } @@ -190,12 +248,17 @@ var self = {}; function Request(url, options) { options = options || {} this.url = url - this._body = options.body + this.credentials = options.credentials || 'omit' this.headers = new Headers(options.headers) this.method = normalizeMethod(options.method || 'GET') this.mode = options.mode || null this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && options.body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(options.body) } function decode(body) { @@ -223,11 +286,43 @@ var self = {}; return head } - Request.prototype.fetch = function() { - var self = this + Body.call(Request.prototype) + + function Response(bodyInit, options) { + if (!options) { + options = {} + } + + this._initBody(bodyInit) + this.type = 'default' + this.url = null + this.status = options.status + this.ok = this.status >= 200 && this.status < 300 + this.statusText = options.statusText + this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) + this.url = options.url || '' + } + + Body.call(Response.prototype) + + self.Headers = Headers; + self.Request = Request; + self.Response = Response; + + self.fetch = function(input, init) { + // TODO: Request constructor should accept input, init + var request + if (Request.prototype.isPrototypeOf(input) && !init) { + request = input + } else { + request = new Request(input, init) + } return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest() + if (request.credentials === 'cors') { + xhr.withCredentials = true; + } function responseURL() { if ('responseURL' in xhr) { @@ -262,57 +357,24 @@ var self = {}; reject(new TypeError('Network request failed')) } - xhr.open(self.method, self.url) - if ('responseType' in xhr && blobSupport) { + xhr.open(request.method, request.url, true) + + if ('responseType' in xhr && support.blob) { xhr.responseType = 'blob' } - self.headers.forEach(function(name, values) { + request.headers.forEach(function(name, values) { values.forEach(function(value) { xhr.setRequestHeader(name, value) }) }) - xhr.send((self._body === undefined) ? null : self._body) + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) }) } - - Body.call(Request.prototype) - - function Response(bodyInit, options) { - if (!options) { - options = {} - } - - if (blobSupport) { - if (typeof bodyInit === 'string') { - this._bodyBlob = new Blob([bodyInit]) - } else { - this._bodyBlob = bodyInit - } - } else { - this._bodyText = bodyInit - } - this.type = 'default' - this.url = null - this.status = options.status - this.statusText = options.statusText - this.headers = options.headers - this.url = options.url || '' - } - - Body.call(Response.prototype) - - self.Headers = Headers; - self.Request = Request; - self.Response = Response; - - self.fetch = function (url, options) { - return new Request(url, options).fetch() - } self.fetch.polyfill = true })(); /** End of the third-party code */ -module.exports = self.fetch; +module.exports = self; diff --git a/Libraries/Geolocation/Geolocation.ios.js b/Libraries/Geolocation/Geolocation.js similarity index 86% rename from Libraries/Geolocation/Geolocation.ios.js rename to Libraries/Geolocation/Geolocation.js index 13fe40a2364447..fae309aef5fe68 100644 --- a/Libraries/Geolocation/Geolocation.ios.js +++ b/Libraries/Geolocation/Geolocation.js @@ -22,6 +22,12 @@ var subscriptions = []; var updatesEnabled = false; +type GeoOptions = { + timeout: number; + maximumAge: number; + enableHighAccuracy: bool; +} + /** * You need to include the `NSLocationWhenInUseUsageDescription` key * in Info.plist to enable geolocation. Geolocation is enabled by default @@ -32,10 +38,14 @@ var updatesEnabled = false; */ var Geolocation = { + /* + * Invokes the success callback once with the latest location info. Supported + * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool) + */ getCurrentPosition: function( geo_success: Function, geo_error?: Function, - geo_options?: Object + geo_options?: GeoOptions ) { invariant( typeof geo_success === 'function', @@ -48,7 +58,11 @@ var Geolocation = { ); }, - watchPosition: function(success: Function, error?: Function, options?: Object): number { + /* + * Invokes the success callback whenever the location changes. Supported + * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool) + */ + watchPosition: function(success: Function, error?: Function, options?: GeoOptions): number { if (!updatesEnabled) { RCTLocationObserver.startObserving(options || {}); updatesEnabled = true; diff --git a/Libraries/Geolocation/RCTGeolocation.xcodeproj/project.pbxproj b/Libraries/Geolocation/RCTGeolocation.xcodeproj/project.pbxproj index dd17e5808c421c..ee79e7571fd973 100644 --- a/Libraries/Geolocation/RCTGeolocation.xcodeproj/project.pbxproj +++ b/Libraries/Geolocation/RCTGeolocation.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ 58B511F01A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -206,6 +207,7 @@ LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTGeolocation; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -213,6 +215,7 @@ 58B511F11A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 6bb95acb0ff248..f21233dbfa0576 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -28,12 +28,14 @@ typedef NS_ENUM(NSInteger, RCTPositionErrorCode) { #define RCT_DEFAULT_LOCATION_ACCURACY kCLLocationAccuracyHundredMeters typedef struct { - NSTimeInterval timeout; - NSTimeInterval maximumAge; - CLLocationAccuracy accuracy; + double timeout; + double maximumAge; + double accuracy; } RCTLocationOptions; -static RCTLocationOptions RCTLocationOptionsWithJSON(id json) +@implementation RCTConvert (RCTLocationOptions) + ++ (RCTLocationOptions)RCTLocationOptions:(id)json { NSDictionary *options = [RCTConvert NSDictionary:json]; return (RCTLocationOptions){ @@ -43,6 +45,8 @@ static RCTLocationOptions RCTLocationOptionsWithJSON(id json) }; } +@end + static NSDictionary *RCTPositionError(RCTPositionErrorCode code, NSString *msg /* nil for default */) { if (!msg) { @@ -121,6 +125,12 @@ - (instancetype)init - (void)dealloc { [_locationManager stopUpdatingLocation]; + _locationManager.delegate = nil; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); } #pragma mark - Private API @@ -153,41 +163,33 @@ - (void)timeout:(NSTimer *)timer #pragma mark - Public API -RCT_EXPORT_METHOD(startObserving:(NSDictionary *)optionsJSON) +RCT_EXPORT_METHOD(startObserving:(RCTLocationOptions)options) { [self checkLocationConfig]; - dispatch_async(dispatch_get_main_queue(), ^{ - - // Select best options - _observerOptions = RCTLocationOptionsWithJSON(optionsJSON); - for (RCTLocationRequest *request in _pendingRequests) { - _observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy); - } - - _locationManager.desiredAccuracy = _observerOptions.accuracy; - [self beginLocationUpdates]; - _observingLocation = YES; + // Select best options + _observerOptions = options; + for (RCTLocationRequest *request in _pendingRequests) { + _observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy); + } - }); + _locationManager.desiredAccuracy = _observerOptions.accuracy; + [self beginLocationUpdates]; + _observingLocation = YES; } RCT_EXPORT_METHOD(stopObserving) { - dispatch_async(dispatch_get_main_queue(), ^{ - - // Stop observing - _observingLocation = NO; - - // Stop updating if no pending requests - if (_pendingRequests.count == 0) { - [_locationManager stopUpdatingLocation]; - } + // Stop observing + _observingLocation = NO; - }); + // Stop updating if no pending requests + if (_pendingRequests.count == 0) { + [_locationManager stopUpdatingLocation]; + } } -RCT_EXPORT_METHOD(getCurrentPosition:(NSDictionary *)optionsJSON +RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options withSuccessCallback:(RCTResponseSenderBlock)successBlock errorCallback:(RCTResponseSenderBlock)errorBlock) { @@ -198,56 +200,49 @@ - (void)timeout:(NSTimer *)timer return; } - dispatch_async(dispatch_get_main_queue(), ^{ - - if (![CLLocationManager locationServicesEnabled]) { - if (errorBlock) { - errorBlock(@[ - RCTPositionError(RCTPositionErrorUnavailable, @"Location services disabled.") - ]); - return; - } + if (![CLLocationManager locationServicesEnabled]) { + if (errorBlock) { + errorBlock(@[ + RCTPositionError(RCTPositionErrorUnavailable, @"Location services disabled.") + ]); + return; } + } - if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) { - if (errorBlock) { - errorBlock(@[ - RCTPositionError(RCTPositionErrorDenied, nil) - ]); - return; - } + if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) { + if (errorBlock) { + errorBlock(@[ + RCTPositionError(RCTPositionErrorDenied, nil) + ]); + return; } + } - // Get options - RCTLocationOptions options = RCTLocationOptionsWithJSON(optionsJSON); - - // Check if previous recorded location exists and is good enough - if (_lastLocationEvent && - CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge && - [_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) { + // Check if previous recorded location exists and is good enough + if (_lastLocationEvent && + CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge && + [_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) { - // Call success block with most recent known location - successBlock(@[_lastLocationEvent]); - return; - } + // Call success block with most recent known location + successBlock(@[_lastLocationEvent]); + return; + } - // Create request - RCTLocationRequest *request = [[RCTLocationRequest alloc] init]; - request.successBlock = successBlock; - request.errorBlock = errorBlock ?: ^(NSArray *args){}; - request.options = options; - request.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:options.timeout - target:self - selector:@selector(timeout:) - userInfo:request - repeats:NO]; - [_pendingRequests addObject:request]; - - // Configure location manager and begin updating location - _locationManager.desiredAccuracy = MIN(_locationManager.desiredAccuracy, options.accuracy); - [self beginLocationUpdates]; - - }); + // Create request + RCTLocationRequest *request = [[RCTLocationRequest alloc] init]; + request.successBlock = successBlock; + request.errorBlock = errorBlock ?: ^(NSArray *args){}; + request.options = options; + request.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:options.timeout + target:self + selector:@selector(timeout:) + userInfo:request + repeats:NO]; + [_pendingRequests addObject:request]; + + // Configure location manager and begin updating location + _locationManager.desiredAccuracy = MIN(_locationManager.desiredAccuracy, options.accuracy); + [self beginLocationUpdates]; } #pragma mark - CLLocationManagerDelegate diff --git a/Libraries/Image/AssetRegistry.js b/Libraries/Image/AssetRegistry.js new file mode 100644 index 00000000000000..df4173e78fe584 --- /dev/null +++ b/Libraries/Image/AssetRegistry.js @@ -0,0 +1,20 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AssetRegistry + */ +'use strict'; + +var assets = []; + +function registerAsset(asset) { + // `push` returns new array length, so the first asset will + // get id 1 (not 0) to make the value truthy + return assets.push(asset); +} + +function getAssetByID(assetId) { + return assets[assetId - 1]; +} + +module.exports = { registerAsset, getAssetByID }; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index fed358e1337ead..32965c213ac449 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -12,28 +12,26 @@ 'use strict'; var EdgeInsetsPropType = require('EdgeInsetsPropType'); +var ImageResizeMode = require('ImageResizeMode'); +var ImageStylePropTypes = require('ImageStylePropTypes'); var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeModules = require('NativeModules'); -var Platform = require('Platform'); var PropTypes = require('ReactPropTypes'); -var ImageResizeMode = require('ImageResizeMode'); -var ImageStylePropTypes = require('ImageStylePropTypes'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); var StyleSheetPropType = require('StyleSheetPropType'); -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); var flattenStyle = require('flattenStyle'); -var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); var merge = require('merge'); var requireNativeComponent = require('requireNativeComponent'); -var warning = require('warning'); +var resolveAssetSource = require('resolveAssetSource'); var verifyPropTypes = require('verifyPropTypes'); +var warning = require('warning'); /** - * A react component for displaying different types of images, + * A React component for displaying different types of images, * including network images, static resources, temporary local images, and * images from local disk, such as the camera roll. * @@ -125,10 +123,12 @@ var Image = React.createClass({ 'not be set directly on Image.'); } } - var style = flattenStyle([styles.base, this.props.style]); - invariant(style, "style must be initialized"); - var source = this.props.source; - invariant(source, "source must be initialized"); + var source = resolveAssetSource(this.props.source) || {}; + + var {width, height} = source; + var style = flattenStyle([{width, height}, styles.base, this.props.style]); + invariant(style, 'style must be initialized'); + var isNetwork = source.uri && source.uri.match(/^https?:/); invariant( !(isNetwork && source.isStatic), @@ -156,12 +156,6 @@ var Image = React.createClass({ contentMode, tintColor: style.tintColor, }); - if (Platform.OS === 'android') { - // TODO: update android native code to not need this - nativeProps.resizeMode = contentMode; - delete nativeProps.contentMode; - } - if (isStored) { nativeProps.imageTag = source.uri; } else { @@ -180,30 +174,9 @@ var styles = StyleSheet.create({ }, }); -if (Platform.OS === 'android') { - var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, { - accessible: true, - accessibilityLabel: true, - capInsets: {diff: insetsDiffer}, // UIEdgeInsets=UIEdgeInsetsZero - imageTag: true, - resizeMode: true, - src: true, - testID: PropTypes.string, - }); - - var RCTStaticImage = createReactIOSNativeComponentClass({ - validAttributes: merge(CommonImageViewAttributes, { tintColor: true }), - uiViewClassName: 'RCTStaticImage', - }); - - var RCTNetworkImage = createReactIOSNativeComponentClass({ - validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }), - uiViewClassName: 'RCTNetworkImageView', - }); -} else { - var RCTStaticImage = requireNativeComponent('RCTStaticImage', null); - var RCTNetworkImage = requireNativeComponent('RCTNetworkImageView', null); -} +var RCTNetworkImage = requireNativeComponent('RCTNetworkImageView', null); +var RCTStaticImage = requireNativeComponent('RCTStaticImage', null); + var nativeOnlyProps = { src: true, defaultImageSrc: true, diff --git a/Libraries/Image/ImageResizeMode.js b/Libraries/Image/ImageResizeMode.js index 9a7a2f9541f9ff..b947b95258f38c 100644 --- a/Libraries/Image/ImageResizeMode.js +++ b/Libraries/Image/ImageResizeMode.js @@ -30,8 +30,8 @@ var ImageResizeMode = keyMirror({ cover: null, /** * stretch - The image will be stretched to fill the entire frame of the - * view without clipping. This may change the aspect ratio of the image, - * distoring it. Only supported on iOS. + * view without clipping. This may change the aspect ratio of the image, + * distoring it. */ stretch: null, }); diff --git a/Libraries/Image/ImageStylePropTypes.js b/Libraries/Image/ImageStylePropTypes.js index 4e361d9dea1293..c70bee73a1908e 100644 --- a/Libraries/Image/ImageStylePropTypes.js +++ b/Libraries/Image/ImageStylePropTypes.js @@ -14,9 +14,11 @@ var ImageResizeMode = require('ImageResizeMode'); var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); +var TransformPropTypes = require('TransformPropTypes'); var ImageStylePropTypes = { ...LayoutPropTypes, + ...TransformPropTypes, resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)), backgroundColor: ReactPropTypes.string, borderColor: ReactPropTypes.string, @@ -29,19 +31,4 @@ var ImageStylePropTypes = { opacity: ReactPropTypes.number, }; -// Image doesn't support padding correctly (#4841912) -var unsupportedProps = Object.keys({ - padding: null, - paddingTop: null, - paddingLeft: null, - paddingRight: null, - paddingBottom: null, - paddingVertical: null, - paddingHorizontal: null, -}); - -for (var i = 0; i < unsupportedProps.length; i++) { - delete ImageStylePropTypes[unsupportedProps[i]]; -} - module.exports = ImageStylePropTypes; diff --git a/Libraries/Image/RCTGIFImage.m b/Libraries/Image/RCTGIFImage.m index e0975f4ac25e4f..99704e305dd0b6 100644 --- a/Libraries/Image/RCTGIFImage.m +++ b/Libraries/Image/RCTGIFImage.m @@ -14,7 +14,6 @@ static CAKeyframeAnimation *RCTGIFImageWithImageSource(CGImageSourceRef imageSource) { if (!UTTypeConformsTo(CGImageSourceGetType(imageSource), kUTTypeGIF)) { - CFRelease(imageSource); return nil; } diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 33a5a65a8382c0..3431def50f0532 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -240,6 +240,7 @@ 58B511721A9E6B3D00147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -251,6 +252,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTImage; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -258,6 +260,7 @@ 58B511731A9E6B3D00147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -269,6 +272,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTImage; + RUN_CLANG_STATIC_ANALYZER = NO; SKIP_INSTALL = YES; }; name = Release; diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js new file mode 100644 index 00000000000000..c5fc3bbe1ea3dc --- /dev/null +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -0,0 +1,143 @@ +/** + * 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. + */ +'use strict'; + +jest + .dontMock('AssetRegistry') + .dontMock('../resolveAssetSource'); + +var resolveAssetSource; +var SourceCode; +var AssetRegistry; + +function expectResolvesAsset(input, expectedSource) { + var assetId = AssetRegistry.registerAsset(input); + expect(resolveAssetSource(assetId)).toEqual(expectedSource); +} + +describe('resolveAssetSource', () => { + beforeEach(() => { + jest.resetModuleRegistry(); + SourceCode = require('NativeModules').SourceCode; + AssetRegistry = require('AssetRegistry'); + resolveAssetSource = require('../resolveAssetSource'); + }); + + it('returns same source for simple static and network images', () => { + var source1 = {uri: 'https://www.facebook.com/logo'}; + expect(resolveAssetSource(source1)).toBe(source1); + + var source2 = {isStatic: true, uri: 'logo'}; + expect(resolveAssetSource(source2)).toBe(source2); + }); + + it('does not change deprecated assets', () => { + expect(resolveAssetSource({ + isStatic: true, + deprecated: true, + width: 100, + height: 200, + uri: 'logo', + })).toEqual({ + isStatic: true, + deprecated: true, + width: 100, + height: 200, + uri: 'logo', + }); + }); + + it('ignores any weird data', () => { + expect(resolveAssetSource(null)).toBe(null); + expect(resolveAssetSource(42)).toBe(null); + expect(resolveAssetSource('nonsense')).toBe(null); + }); + + describe('bundle was loaded from network', () => { + beforeEach(() => { + SourceCode.scriptURL = 'http://10.0.0.1:8081/main.bundle'; + }); + + it('uses network image', () => { + expectResolvesAsset({ + __packager_asset: true, + fileSystemLocation: '/root/app/module/a', + httpServerLocation: '/assets/module/a', + width: 100, + height: 200, + scales: [1], + hash: '5b6f00f', + name: 'logo', + type: 'png', + }, { + isStatic: false, + width: 100, + height: 200, + uri: 'http://10.0.0.1:8081/assets/module/a/logo.png?hash=5b6f00f', + }); + }); + + it('picks matching scale', () => { + expectResolvesAsset({ + __packager_asset: true, + fileSystemLocation: '/root/app/module/a', + httpServerLocation: '/assets/module/a', + width: 100, + height: 200, + scales: [1, 2, 3], + hash: '5b6f00f', + name: 'logo', + type: 'png', + }, { + isStatic: false, + width: 100, + height: 200, + uri: 'http://10.0.0.1:8081/assets/module/a/logo@2x.png?hash=5b6f00f', + }); + }); + + }); + + describe('bundle was loaded from file', () => { + beforeEach(() => { + SourceCode.scriptURL = 'file:///Path/To/Simulator/main.bundle'; + }); + + it('uses pre-packed image', () => { + expectResolvesAsset({ + __packager_asset: true, + fileSystemLocation: '/root/app/module/a', + httpServerLocation: '/assets/module/a', + width: 100, + height: 200, + scales: [1], + hash: '5b6f00f', + name: 'logo', + type: 'png', + }, { + isStatic: true, + width: 100, + height: 200, + uri: 'assets/module/a/logo.png', + }); + }); + }); + +}); + +describe('resolveAssetSource.pickScale', () => { + it('picks matching scale', () => { + expect(resolveAssetSource.pickScale([1], 2)).toBe(1); + expect(resolveAssetSource.pickScale([1, 2, 3], 2)).toBe(2); + expect(resolveAssetSource.pickScale([1, 2], 3)).toBe(2); + expect(resolveAssetSource.pickScale([1, 2, 3, 4], 3.5)).toBe(4); + expect(resolveAssetSource.pickScale([3, 4], 2)).toBe(3); + expect(resolveAssetSource.pickScale([], 2)).toBe(1); + }); +}); diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js new file mode 100644 index 00000000000000..29d59a9a5dfc5b --- /dev/null +++ b/Libraries/Image/resolveAssetSource.js @@ -0,0 +1,92 @@ +/** + * 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. + * + * @providesModule resolveAssetSource + */ +'use strict'; + +var AssetRegistry = require('AssetRegistry'); +var PixelRatio = require('PixelRatio'); +var SourceCode = require('NativeModules').SourceCode; + +var _serverURL; + +function getServerURL() { + if (_serverURL === undefined) { + var scriptURL = SourceCode.scriptURL; + var match = scriptURL && scriptURL.match(/^https?:\/\/.*?\//); + if (match) { + _serverURL = match[0]; + } else { + _serverURL = null; + } + } + + return _serverURL; +} + +function pickScale(scales, deviceScale) { + // Packager guarantees that `scales` array is sorted + for (var i = 0; i < scales.length; i++) { + if (scales[i] >= deviceScale) { + return scales[i]; + } + } + + // If nothing matches, device scale is larger than any available + // scales, so we return the biggest one. Unless the array is empty, + // in which case we default to 1 + return scales[scales.length - 1] || 1; +} + +function resolveAssetSource(source) { + if (typeof source === 'object') { + return source; + } + + var asset = AssetRegistry.getAssetByID(source); + if (asset) { + return assetToImageSource(asset); + } + + return null; +} + +function assetToImageSource(asset) { + // TODO(frantic): currently httpServerLocation is used both as + // path in http URL and path within IPA. Should we have zipArchiveLocation? + var path = asset.httpServerLocation; + if (path[0] === '/') { + path = path.substr(1); + } + + var scale = pickScale(asset.scales, PixelRatio.get()); + var scaleSuffix = scale === 1 ? '' : '@' + scale + 'x'; + + var fileName = asset.name + scaleSuffix + '.' + asset.type; + var serverURL = getServerURL(); + if (serverURL) { + return { + width: asset.width, + height: asset.height, + uri: serverURL + path + '/' + fileName + + '?hash=' + asset.hash, + isStatic: false, + }; + } else { + return { + width: asset.width, + height: asset.height, + uri: path + '/' + fileName, + isStatic: true, + }; + } +} + +module.exports = resolveAssetSource; +module.exports.pickScale = pickScale; diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index d9118b7484f0e7..2677a0029c5dfe 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -11,11 +11,11 @@ */ 'use strict'; -var Platform = require('Platform'); var RCTExceptionsManager = require('NativeModules').ExceptionsManager; var loadSourceMap = require('loadSourceMap'); var parseErrorStack = require('parseErrorStack'); +var stringifySafe = require('stringifySafe'); var sourceMapPromise; @@ -25,17 +25,11 @@ type Exception = { message: string; } -function handleException(e: Exception) { - var stack = parseErrorStack(e); - console.error( - 'Error: ' + - '\n stack: \n' + stackToString(stack) + - '\n URL: ' + e.sourceURL + - '\n line: ' + e.line + - '\n message: ' + e.message - ); - +function reportException(e: Exception, stack?: any) { if (RCTExceptionsManager) { + if (!stack) { + stack = parseErrorStack(e); + } RCTExceptionsManager.reportUnhandledException(e.message, stack); if (__DEV__) { (sourceMapPromise = sourceMapPromise || loadSourceMap()) @@ -50,6 +44,47 @@ function handleException(e: Exception) { } } +function handleException(e: Exception) { + var stack = parseErrorStack(e); + var msg = + 'Error: ' + e.message + + '\n stack: \n' + stackToString(stack) + + '\n URL: ' + e.sourceURL + + '\n line: ' + e.line + + '\n message: ' + e.message; + if (console.errorOriginal) { + console.errorOriginal(msg); + } else { + console.error(msg); + } + reportException(e, stack); +} + +/** + * Shows a redbox with stacktrace for all console.error messages. Disable by + * setting `console.reportErrorsAsExceptions = false;` in your app. + */ +function installConsoleErrorReporter() { + if (console.reportException) { + return; // already installed + } + console.reportException = reportException; + console.errorOriginal = console.error.bind(console); + console.error = function reactConsoleError() { + console.errorOriginal.apply(null, arguments); + if (!console.reportErrorsAsExceptions) { + return; + } + var str = Array.prototype.map.call(arguments, stringifySafe).join(', '); + var error: any = new Error('console.error: ' + str); + error.framesToPop = 1; + reportException(error); + }; + if (console.reportErrorsAsExceptions === undefined) { + console.reportErrorsAsExceptions = true; // Individual apps can disable this + } +} + function stackToString(stack) { var maxLength = Math.max.apply(null, stack.map(frame => frame.methodName.length)); return stack.map(frame => stackFrameToString(frame, maxLength)).join('\n'); @@ -71,4 +106,4 @@ function fillSpaces(n) { return new Array(n + 1).join(' '); } -module.exports = { handleException }; +module.exports = { handleException, installConsoleErrorReporter }; diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 51f6809ccd9b28..7bddf87b69c786 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -79,6 +79,16 @@ function setupRedBoxErrorHandler() { ErrorUtils.setGlobalHandler(handleErrorWithRedBox); } +function setupRedBoxConsoleErrorHandler() { + // ExceptionsManager transitively requires Promise so we install it after + var ExceptionsManager = require('ExceptionsManager'); + var Platform = require('Platform'); + // TODO (#6925182): Enable console.error redbox on Android + if (__DEV__ && Platform.OS === 'ios') { + ExceptionsManager.installConsoleErrorReporter(); + } +} + /** * Sets up a set of window environment wrappers that ensure that the * BatchedBridge is flushed after each tick. In both the case of the @@ -125,7 +135,12 @@ function setupXHR() { // The native XMLHttpRequest in Chrome dev tools is CORS aware and won't // let you fetch anything from the internet GLOBAL.XMLHttpRequest = require('XMLHttpRequest'); - GLOBAL.fetch = require('fetch'); + + var fetchPolyfill = require('fetch'); + GLOBAL.fetch = fetchPolyfill.fetch; + GLOBAL.Headers = fetchPolyfill.Headers; + GLOBAL.Request = fetchPolyfill.Request; + GLOBAL.Response = fetchPolyfill.Response; } function setupGeolocation() { @@ -139,4 +154,5 @@ setupTimers(); setupAlert(); setupPromise(); setupXHR(); +setupRedBoxConsoleErrorHandler(); setupGeolocation(); diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js index e168daae66201d..a826db7bf5cb34 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js @@ -7,8 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule loadSourceMap - * - * -- disabled flow due to mysterious validation errors -- + * @flow */ 'use strict'; @@ -18,8 +17,6 @@ var RCTSourceCode = require('NativeModules').SourceCode; var SourceMapConsumer = require('SourceMap').SourceMapConsumer; var SourceMapURL = require('./source-map-url'); -var fetch = require('fetch'); - function loadSourceMap(): Promise { return fetchSourceMap() .then(map => new SourceMapConsumer(map)); @@ -40,7 +37,10 @@ function fetchSourceMap(): Promise { .then(response => response.text()) } -function extractSourceMapURL({url, text}): string { +function extractSourceMapURL({url, text, fullSourceMappingURL}): string { + if (fullSourceMappingURL) { + return fullSourceMappingURL; + } var mapURL = SourceMapURL.getFrom(text); var baseURL = url.match(/(.+:\/\/.*?)\//)[1]; return baseURL + mapURL; diff --git a/Libraries/LinkingIOS/RCTLinkingManager.h b/Libraries/LinkingIOS/RCTLinkingManager.h index 81680f056a3844..caa3aa2a3a9264 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.h +++ b/Libraries/LinkingIOS/RCTLinkingManager.h @@ -9,7 +9,7 @@ #import -#import "../../React/Base/RCTBridgeModule.h" +#import "RCTBridgeModule.h" @interface RCTLinkingManager : NSObject diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index 990c5900fee20d..eec17a012f9eeb 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -36,6 +36,11 @@ - (void)dealloc [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("com.facebook.React.LinkingManager", DISPATCH_QUEUE_SERIAL); +} + + (BOOL)application:(UIApplication *)application openURL:(NSURL *)URL sourceApplication:(NSString *)sourceApplication @@ -56,12 +61,14 @@ - (void)handleOpenURLNotification:(NSNotification *)notification RCT_EXPORT_METHOD(openURL:(NSURL *)URL) { + // Doesn't really matter what thread we call this on since it exits the app [[UIApplication sharedApplication] openURL:URL]; } RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL callback:(RCTResponseSenderBlock)callback) { + // This can be expensive, so we deliberately don't call on main thread BOOL canOpen = [[UIApplication sharedApplication] canOpenURL:URL]; callback(@[@(canOpen)]); } diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index d98b997ca82a72..2b65671a9c30d8 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -34,7 +34,7 @@ type ReachabilityStateIOS = $Enum<{ * * ### reachabilityIOS * - * Asyncronously determine if the device is online and on a cellular network. + * Asynchronously determine if the device is online and on a cellular network. * * - `none` - device is offline * - `wifi` - device is online and connected via wifi, or is the iOS simulator @@ -60,7 +60,7 @@ type ReachabilityStateIOS = $Enum<{ * * ### isConnected * - * Available on all platforms. Asyncronously fetch a boolean to determine + * Available on all platforms. Asynchronously fetch a boolean to determine * internet connectivity. * * ``` diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m index 634d325e913359..1d0a793de8e454 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -61,7 +61,7 @@ @implementation RCTDataManager responseJSON = @{ @"status": @0, @"responseHeaders": @{}, - @"responseText": [connectionError localizedDescription] + @"responseText": [connectionError localizedDescription] ?: [NSNull null] }; } diff --git a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj index 652952994870b0..1dca7fe6de27bf 100644 --- a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj +++ b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj @@ -204,6 +204,7 @@ 58B511F01A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -212,6 +213,7 @@ LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTNetwork; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -219,6 +221,7 @@ 58B511F11A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/Network/RCTReachability.m b/Libraries/Network/RCTReachability.m index b5f30de30e7865..e0711571b9a3c3 100644 --- a/Libraries/Network/RCTReachability.m +++ b/Libraries/Network/RCTReachability.m @@ -25,6 +25,8 @@ @implementation RCTReachability @synthesize bridge = _bridge; +RCT_EXPORT_MODULE() + static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { RCTReachability *self = (__bridge id)info; @@ -53,8 +55,6 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC } } -RCT_EXPORT_MODULE() - #pragma mark - Lifecycle - (instancetype)initWithHost:(NSString *)host @@ -71,7 +71,7 @@ - (instancetype)initWithHost:(NSString *)host - (instancetype)init { - return [self initWithHost:@"http://apple.com"]; + return [self initWithHost:@"apple.com"]; } - (void)dealloc diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequestBase.js index d7619d07546f07..99a327b5f236eb 100644 --- a/Libraries/Network/XMLHttpRequestBase.js +++ b/Libraries/Network/XMLHttpRequestBase.js @@ -53,13 +53,23 @@ class XMLHttpRequestBase { } getAllResponseHeaders(): ?string { - /* Stub */ - return ''; + if (this.responseHeaders) { + var headers = []; + for (var headerName in this.responseHeaders) { + headers.push(headerName + ': ' + this.responseHeaders[headerName]); + } + return headers.join('\n'); + } + // according to the spec, return null <==> no response has been received + return null; } getResponseHeader(header: string): ?string { - /* Stub */ - return ''; + if (this.responseHeaders) { + var value = this.responseHeaders[header.toLowerCase()]; + return value !== undefined ? value : null; + } + return null; } setRequestHeader(header: string, value: any): void { @@ -122,7 +132,12 @@ class XMLHttpRequestBase { return; } this.status = status; - this.responseHeaders = responseHeaders; + // Headers should be case-insensitive + var lcResponseHeaders = {}; + for (var header in responseHeaders) { + lcResponseHeaders[header.toLowerCase()] = responseHeaders[header]; + } + this.responseHeaders = lcResponseHeaders; this.responseText = responseText; this._setReadyState(this.DONE); this._sendLoad(); diff --git a/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj/project.pbxproj b/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj/project.pbxproj index 10d8ce16900533..8bca741b4ad0bf 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj/project.pbxproj +++ b/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj/project.pbxproj @@ -143,10 +143,7 @@ GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -182,6 +179,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREPROCESSOR_DEFINITIONS = ""; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -198,6 +196,8 @@ 58B511F01A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -206,6 +206,7 @@ LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTPushNotification; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -213,6 +214,7 @@ 58B511F11A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index 4e791c68002c1e..ef1ba1496e8c62 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -9,12 +9,10 @@ #import -#import "../../React/Base/RCTBridgeModule.h" +#import "RCTBridgeModule.h" @interface RCTPushNotificationManager : NSObject -- (instancetype)initWithInitialNotification:(NSDictionary *)initialNotification NS_DESIGNATED_INITIALIZER; - + (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; + (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 90f2d878629092..4846c885e302e8 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -24,14 +24,8 @@ @implementation RCTPushNotificationManager @synthesize bridge = _bridge; - (instancetype)init -{ - return [self initWithInitialNotification:nil]; -} - -- (instancetype)initWithInitialNotification:(NSDictionary *)initialNotification { if ((self = [super init])) { - _initialNotification = [initialNotification copy]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRemoteNotificationReceived:) name:RCTRemoteNotificationReceived @@ -45,6 +39,12 @@ - (void)dealloc [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + _initialNotification = [bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]; +} + + (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { if ([application respondsToSelector:@selector(registerForRemoteNotifications)]) { @@ -113,7 +113,7 @@ - (void)handleRemoteNotificationReceived:(NSNotification *)notification #endif - NSUInteger types; + NSUInteger types = 0; if ([UIApplication instancesRespondToSelector:@selector(currentUserNotificationSettings)]) { types = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; } else { diff --git a/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj index d4945f460288a3..6ab58a8a2e3e06 100644 --- a/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj +++ b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj @@ -246,6 +246,7 @@ 580C37841AB104AF0015E709 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -257,6 +258,7 @@ XCTest, ); PRODUCT_NAME = "$(TARGET_NAME)"; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -264,6 +266,7 @@ 580C37851AB104AF0015E709 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/RCTTest/RCTTestModule.h b/Libraries/RCTTest/RCTTestModule.h index 21ed60c6b91f05..f248cbfca40818 100644 --- a/Libraries/RCTTest/RCTTestModule.h +++ b/Libraries/RCTTest/RCTTestModule.h @@ -10,19 +10,30 @@ #import #import "RCTBridgeModule.h" +#import "RCTDefines.h" @class FBSnapshotTestController; @interface RCTTestModule : NSObject -// This is typically polled while running the runloop until true -@property (nonatomic, readonly, getter=isDone) BOOL done; - -// This is used to give meaningful names to snapshot image files. -@property (nonatomic, assign) SEL testSelector; +/** + * The snapshot test controller for this module. + */ +@property (nonatomic, weak) FBSnapshotTestController *controller; +/** + * This is the view to be snapshotted. + */ @property (nonatomic, weak) UIView *view; -- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view; +/** + * This is used to give meaningful names to snapshot image files. + */ +@property (nonatomic, assign) SEL testSelector; + +/** + * This is typically polled while running the runloop until true. + */ +@property (nonatomic, readonly, getter=isDone) BOOL done; @end diff --git a/Libraries/RCTTest/RCTTestModule.m b/Libraries/RCTTest/RCTTestModule.m index 58b6572f8bfa32..33f562515d629b 100644 --- a/Libraries/RCTTest/RCTTestModule.m +++ b/Libraries/RCTTest/RCTTestModule.m @@ -12,21 +12,25 @@ #import "FBSnapshotTestController.h" #import "RCTAssert.h" #import "RCTLog.h" +#import "RCTUIManager.h" @implementation RCTTestModule { - __weak FBSnapshotTestController *_snapshotController; - __weak UIView *_view; NSMutableDictionary *_snapshotCounter; } +@synthesize bridge = _bridge; + RCT_EXPORT_MODULE() -- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view +- (dispatch_queue_t)methodQueue +{ + return _bridge.uiManager.methodQueue; +} + +- (instancetype)init { if ((self = [super init])) { - _snapshotController = controller; - _view = view; _snapshotCounter = [NSMutableDictionary new]; } return self; @@ -34,30 +38,29 @@ - (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controlle RCT_EXPORT_METHOD(verifySnapshot:(RCTResponseSenderBlock)callback) { - if (!_snapshotController) { - RCTLogWarn(@"No snapshot controller configured."); - callback(@[]); - return; - } + RCTAssert(_controller != nil, @"No snapshot controller configured."); + + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - dispatch_async(dispatch_get_main_queue(), ^{ NSString *testName = NSStringFromSelector(_testSelector); - _snapshotCounter[testName] = @([_snapshotCounter[testName] integerValue] + 1); + _snapshotCounter[testName] = [@([_snapshotCounter[testName] integerValue] + 1) stringValue]; + NSError *error = nil; - BOOL success = [_snapshotController compareSnapshotOfView:_view - selector:_testSelector - identifier:[_snapshotCounter[testName] stringValue] - error:&error]; + BOOL success = [_controller compareSnapshotOfView:_view + selector:_testSelector + identifier:_snapshotCounter[testName] + error:&error]; + RCTAssert(success, @"Snapshot comparison failed: %@", error); callback(@[]); - }); + }]; } RCT_EXPORT_METHOD(markTestCompleted) { - dispatch_async(dispatch_get_main_queue(), ^{ + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _done = YES; - }); + }]; } @end diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 9b3a7d3c89881b..9c0cacf709c3e1 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -17,9 +17,15 @@ #define TIMEOUT_SECONDS 240 +@interface RCTBridge (RCTTestRunner) + +@property (nonatomic, weak) RCTBridge *batchedBridge; + +@end + @implementation RCTTestRunner { - FBSnapshotTestController *_snapshotController; + FBSnapshotTestController *_testController; } - (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir @@ -27,8 +33,8 @@ - (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDi if ((self = [super init])) { NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"]; sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"]; - _snapshotController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName]; - _snapshotController.referenceImagesDirectory = referenceDir; + _testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName]; + _testController.referenceImagesDirectory = referenceDir; _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]]; } return self; @@ -36,12 +42,12 @@ - (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDi - (void)setRecordMode:(BOOL)recordMode { - _snapshotController.recordMode = recordMode; + _testController.recordMode = recordMode; } - (BOOL)recordMode { - return _snapshotController.recordMode; + return _testController.recordMode; } - (void)runTest:(SEL)test module:(NSString *)moduleName @@ -59,37 +65,30 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock { - UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; - if ([vc.view isKindOfClass:[RCTRootView class]]) { - [(RCTRootView *)vc.view invalidate]; // Make sure the normal app view doesn't interfere - } - vc.view = [[UIView alloc] init]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:_scriptURL + moduleName:moduleName + launchOptions:nil]; + rootView.initialProperties = initialProps; + rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices - RCTTestModule *testModule = [[RCTTestModule alloc] initWithSnapshotController:_snapshotController view:nil]; + NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]); + RCTTestModule *testModule = rootView.bridge.batchedBridge.modules[testModuleName]; + testModule.controller = _testController; testModule.testSelector = test; - RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL - moduleProvider:^(){ - return @[testModule]; - } - launchOptions:nil]; - - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge - moduleName:moduleName]; testModule.view = rootView; + + UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController; + vc.view = [[UIView alloc] init]; [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized - rootView.initialProperties = initialProps; - rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; while ([date timeIntervalSinceNow] > 0 && ![testModule isDone] && error == nil) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; error = [[RCTRedBox sharedInstance] currentErrorMessage]; } [rootView removeFromSuperview]; - [rootView.bridge invalidate]; - [rootView invalidate]; RCTAssert(vc.view.subviews.count == 0, @"There shouldn't be any other views: %@", vc.view); vc.view = nil; [[RCTRedBox sharedInstance] dismiss]; diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj index acb5daa3e736b2..38ac20c73c64d4 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */; }; - 00D277191AB8C35800DC1E48 /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277181AB8C35800DC1E48 /* SRWebSocket.m */; }; + 13AF20421AE707C5005F5298 /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 13AF20411AE707C5005F5298 /* SRWebSocket.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -26,8 +26,8 @@ /* Begin PBXFileReference section */ 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketExecutor.h; sourceTree = ""; }; 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = ""; }; - 00D277171AB8C35800DC1E48 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; - 00D277181AB8C35800DC1E48 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; + 13AF20401AE707C5005F5298 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; + 13AF20411AE707C5005F5298 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocketDebugger.a; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -45,8 +45,8 @@ 832C81771AAF6DEF007FA2F7 = { isa = PBXGroup; children = ( - 00D277171AB8C35800DC1E48 /* SRWebSocket.h */, - 00D277181AB8C35800DC1E48 /* SRWebSocket.m */, + 13AF20401AE707C5005F5298 /* SRWebSocket.h */, + 13AF20411AE707C5005F5298 /* SRWebSocket.m */, 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */, 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */, 832C81811AAF6DEF007FA2F7 /* Products */, @@ -119,7 +119,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00D277191AB8C35800DC1E48 /* SRWebSocket.m in Sources */, + 13AF20421AE707C5005F5298 /* SRWebSocket.m in Sources */, 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -214,6 +214,7 @@ 832C81951AAF6DF0007FA2F7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -224,6 +225,7 @@ "-llibicucore", ); PRODUCT_NAME = "$(TARGET_NAME)"; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -231,6 +233,7 @@ 832C81961AAF6DF0007FA2F7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h index 3fc062a37eb639..9993cbc5a3060d 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h @@ -7,6 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import "RCTDefines.h" + +#if RCT_DEV // Debug executors are only supported in dev mode + #import "RCTJavaScriptExecutor.h" @interface RCTWebSocketExecutor : NSObject @@ -14,3 +18,5 @@ - (instancetype)initWithURL:(NSURL *)URL; @end + +#endif diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m index 784c91e12768fa..16a378ccfb6496 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -7,6 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import "RCTDefines.h" + +#if RCT_DEV // Debug executors are only supported in dev mode + #import "RCTWebSocketExecutor.h" #import "RCTLog.h" @@ -62,7 +66,8 @@ - (instancetype)initWithURL:(NSURL *)URL retries--; } if (!runtimeIsReady) { - RCTLogError(@"Runtime is not ready. Do you have Chrome open?"); + RCTLogError(@"Runtime is not ready. Make sure Chrome is running and not " + "paused on a breakpoint or exception and try reloading again."); [self invalidate]; return nil; } @@ -82,7 +87,7 @@ - (BOOL)prepareJSRuntime { __block NSError *initError; dispatch_semaphore_t s = dispatch_semaphore_create(0); - [self sendMessage:@{@"method": @"prepareJSRuntime"} waitForReply:^(NSError *error, NSDictionary *reply) { + [self sendMessage:@{@"method": @"prepareJSRuntime"} context:nil waitForReply:^(NSError *error, NSDictionary *reply) { initError = error; dispatch_semaphore_signal(s); }]; @@ -111,7 +116,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error RCTLogError(@"WebSocket connection failed with error %@", error); } -- (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)callback +- (void)sendMessage:(NSDictionary *)message context:(NSNumber *)executorID waitForReply:(WSMessageCallback)callback { static NSUInteger lastID = 10000; @@ -122,6 +127,8 @@ - (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)call }]; callback(error, nil); return; + } else if (executorID && ![RCTGetExecutorID(self) isEqualToNumber:executorID]) { + return; } NSNumber *expectedID = @(lastID++); @@ -134,22 +141,22 @@ - (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)call - (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete { - NSDictionary *message = @{@"method": NSStringFromSelector(_cmd), @"url": [URL absoluteString], @"inject": _injectedObjects}; - [self sendMessage:message waitForReply:^(NSError *error, NSDictionary *reply) { + NSDictionary *message = @{@"method": @"executeApplicationScript", @"url": [URL absoluteString], @"inject": _injectedObjects}; + [self sendMessage:message context:nil waitForReply:^(NSError *error, NSDictionary *reply) { onComplete(error); }]; } -- (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments callback:(RCTJavaScriptCallback)onComplete +- (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete { RCTAssert(onComplete != nil, @"callback was missing for exec JS call"); NSDictionary *message = @{ - @"method": NSStringFromSelector(_cmd), + @"method": @"executeJSCall", @"moduleName": name, @"moduleMethod": method, @"arguments": arguments }; - [self sendMessage:message waitForReply:^(NSError *socketError, NSDictionary *reply) { + [self sendMessage:message context:executorID waitForReply:^(NSError *socketError, NSDictionary *reply) { if (socketError) { onComplete(nil, socketError); return; @@ -169,6 +176,11 @@ - (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)object }); } +- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block +{ + dispatch_async(dispatch_get_main_queue(), block); +} + - (void)invalidate { _socket.delegate = nil; @@ -187,3 +199,5 @@ - (void)dealloc } @end + +#endif diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.m b/Libraries/RCTWebSocketDebugger/SRWebSocket.m index 3fd675103367ce..e98e9e45637167 100644 --- a/Libraries/RCTWebSocketDebugger/SRWebSocket.m +++ b/Libraries/RCTWebSocketDebugger/SRWebSocket.m @@ -19,6 +19,8 @@ #import +#pragma clang diagnostic ignored "-Wshadow" + //NOTE: libicucore ins't actually needed for the socket to function //and by commenting this out, we avoid the need to import it into every app. @@ -1702,7 +1704,7 @@ static inline int32_t validate_dispatch_data_partial_string(NSData *data) { for (int i = 0; i < maxCodepointSize; i++) { NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO]; if (str) { - return data.length - i; + return (int32_t)(data.length - i); } } diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js index ec72a0b4f4696b..9d413e5c7ca7a1 100644 --- a/Libraries/ReactIOS/NativeMethodsMixin.js +++ b/Libraries/ReactIOS/NativeMethodsMixin.js @@ -19,6 +19,7 @@ var TextInputState = require('TextInputState'); var flattenStyle = require('flattenStyle'); var invariant = require('invariant'); var mergeFast = require('mergeFast'); +var precomputeStyle = require('precomputeStyle'); type MeasureOnSuccessCallback = ( x: number, @@ -93,7 +94,7 @@ var NativeMethodsMixin = { break; } } - var style = flattenStyle(nativeProps.style); + var style = precomputeStyle(flattenStyle(nativeProps.style)); var props = null; if (hasOnlyStyle) { diff --git a/Libraries/ReactIOS/ReactIOSComponentMixin.js b/Libraries/ReactIOS/ReactIOSComponentMixin.js index 94c708a433e863..03d7f5ad04fc3b 100644 --- a/Libraries/ReactIOS/ReactIOSComponentMixin.js +++ b/Libraries/ReactIOS/ReactIOSComponentMixin.js @@ -46,7 +46,7 @@ var ReactInstanceMap = require('ReactInstanceMap'); * * `mountImage`: A way to represent the potential to create lower level * resources whos `nodeHandle` can be discovered immediately by knowing the - * `rootNodeID`. Today's web react represents this with `innerHTML` annotated + * `rootNodeID`. Today's web React represents this with `innerHTML` annotated * with DOM ids that match the `rootNodeID`. * * Opaque name TodaysWebReact FutureWebWorkerReact ReactNative diff --git a/Libraries/ReactIOS/ReactIOSMount.js b/Libraries/ReactIOS/ReactIOSMount.js index 730031672f253d..9b1428fdd6d27a 100644 --- a/Libraries/ReactIOS/ReactIOSMount.js +++ b/Libraries/ReactIOS/ReactIOSMount.js @@ -191,10 +191,10 @@ var ReactIOSMount = { * that has been rendered and unmounting it. There should just be one child * component at this time. */ - unmountComponentAtNode: function(containerTag: number): bool { + unmountComponentAtNode: function(containerTag: number): boolean { if (!ReactIOSTagHandles.reactTagIsNativeTopRootID(containerTag)) { console.error('You cannot render into anything but a top root'); - return; + return false; } var containerID = ReactIOSTagHandles.tagToRootNodeID[containerTag]; diff --git a/Libraries/ReactIOS/ReactIOSNativeComponent.js b/Libraries/ReactIOS/ReactIOSNativeComponent.js index b9abd5965c5f07..7f27ae0ea7dae3 100644 --- a/Libraries/ReactIOS/ReactIOSNativeComponent.js +++ b/Libraries/ReactIOS/ReactIOSNativeComponent.js @@ -23,6 +23,7 @@ var styleDiffer = require('styleDiffer'); var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); var diffRawProperties = require('diffRawProperties'); var flattenStyle = require('flattenStyle'); +var precomputeStyle = require('precomputeStyle'); var warning = require('warning'); var registrationNames = ReactIOSEventEmitter.registrationNames; @@ -160,7 +161,7 @@ ReactIOSNativeComponent.Mixin = { // before actually doing the expensive flattening operation in order to // compute the diff. if (styleDiffer(nextProps.style, prevProps.style)) { - var nextFlattenedStyle = flattenStyle(nextProps.style); + var nextFlattenedStyle = precomputeStyle(flattenStyle(nextProps.style)); updatePayload = diffRawProperties( updatePayload, this.previousFlattenedStyle, diff --git a/Libraries/ReactIOS/ReactIOSViewAttributes.js b/Libraries/ReactIOS/ReactIOSViewAttributes.js index 069f00b2491925..489741b053b5ea 100644 --- a/Libraries/ReactIOS/ReactIOSViewAttributes.js +++ b/Libraries/ReactIOS/ReactIOSViewAttributes.js @@ -9,8 +9,7 @@ * @providesModule ReactIOSViewAttributes * @flow */ - -"use strict"; +'use strict'; var merge = require('merge'); @@ -21,6 +20,7 @@ ReactIOSViewAttributes.UIView = { accessible: true, accessibilityLabel: true, testID: true, + onLayout: true, }; ReactIOSViewAttributes.RCTView = merge( @@ -31,7 +31,7 @@ ReactIOSViewAttributes.RCTView = merge( // For this property to be effective, it must be applied to a view that contains // many subviews that extend outside its bound. The subviews must also have // overflow: hidden, as should the containing view (or one of its superviews). - removeClippedSubviews: true + removeClippedSubviews: true, }); module.exports = ReactIOSViewAttributes; diff --git a/Libraries/ReactIOS/WarningBox.js b/Libraries/ReactIOS/WarningBox.js new file mode 100644 index 00000000000000..37076ef5c299fe --- /dev/null +++ b/Libraries/ReactIOS/WarningBox.js @@ -0,0 +1,398 @@ +/** + * 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. + * + * @providesModule WarningBox + */ +'use strict'; + +var AsyncStorage = require('AsyncStorage'); +var EventEmitter = require('EventEmitter'); +var Map = require('Map'); +var PanResponder = require('PanResponder'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var TouchableOpacity = require('TouchableOpacity'); +var View = require('View'); + +var invariant = require('invariant'); +var rebound = require('rebound'); +var stringifySafe = require('stringifySafe'); + +var SCREEN_WIDTH = require('Dimensions').get('window').width; +var IGNORED_WARNINGS_KEY = '__DEV_WARNINGS_IGNORED'; + +var consoleWarn = console.warn.bind(console); + +var warningCounts = new Map(); +var ignoredWarnings: Array = []; +var totalWarningCount = 0; +var warningCountEvents = new EventEmitter(); + +/** + * WarningBox renders warnings on top of the app being developed. Warnings help + * guard against subtle yet significant issues that can impact the quality of + * your application, such as accessibility and memory leaks. This "in your + * face" style of warning allows developers to notice and correct these issues + * as quickly as possible. + * + * The warning box is currently opt-in. Set the following flag to enable it: + * + * `console.yellowBoxEnabled = true;` + * + * If "ignore" is tapped on a warning, the WarningBox will record that warning + * and will not display it again. This is useful for hiding errors that already + * exist or have been introduced elsewhere. To re-enable all of the errors, set + * the following: + * + * `console.yellowBoxResetIgnored = true;` + * + * This can also be set permanently, and ignore will only silence the warnings + * until the next refresh. + */ + +if (__DEV__) { + console.warn = function() { + consoleWarn.apply(null, arguments); + if (!console.yellowBoxEnabled) { + return; + } + var warning = Array.prototype.map.call(arguments, stringifySafe).join(' '); + if (!console.yellowBoxResetIgnored && + ignoredWarnings.indexOf(warning) !== -1) { + return; + } + var count = warningCounts.has(warning) ? warningCounts.get(warning) + 1 : 1; + warningCounts.set(warning, count); + totalWarningCount += 1; + warningCountEvents.emit('count', totalWarningCount); + }; +} + +function saveIgnoredWarnings() { + AsyncStorage.setItem( + IGNORED_WARNINGS_KEY, + JSON.stringify(ignoredWarnings), + function(err) { + if (err) { + console.warn('Could not save ignored warnings.', err); + } + } + ); +} + +AsyncStorage.getItem(IGNORED_WARNINGS_KEY, function(err, data) { + if (!err && data && !console.yellowBoxResetIgnored) { + ignoredWarnings = JSON.parse(data); + } +}); + +var WarningRow = React.createClass({ + componentWillMount: function() { + this.springSystem = new rebound.SpringSystem(); + this.dismissalSpring = this.springSystem.createSpring(); + this.dismissalSpring.setRestSpeedThreshold(0.05); + this.dismissalSpring.setCurrentValue(0); + this.dismissalSpring.addListener({ + onSpringUpdate: () => { + var val = this.dismissalSpring.getCurrentValue(); + this.text && this.text.setNativeProps({ + left: SCREEN_WIDTH * val, + }); + this.container && this.container.setNativeProps({ + opacity: 1 - val, + }); + this.closeButton && this.closeButton.setNativeProps({ + opacity: 1 - (val * 5), + }); + }, + onSpringAtRest: () => { + if (this.dismissalSpring.getCurrentValue()) { + this.collapseSpring.setEndValue(1); + } + }, + }); + this.collapseSpring = this.springSystem.createSpring(); + this.collapseSpring.setRestSpeedThreshold(0.05); + this.collapseSpring.setCurrentValue(0); + this.collapseSpring.getSpringConfig().friction = 20; + this.collapseSpring.getSpringConfig().tension = 200; + this.collapseSpring.addListener({ + onSpringUpdate: () => { + var val = this.collapseSpring.getCurrentValue(); + this.container && this.container.setNativeProps({ + height: Math.abs(46 - (val * 46)), + }); + }, + onSpringAtRest: () => { + this.props.onDismissed(); + }, + }); + this.panGesture = PanResponder.create({ + onStartShouldSetPanResponder: () => { + return !! this.dismissalSpring.getCurrentValue(); + }, + onMoveShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + this.isResponderOnlyToBlockTouches = + !! this.dismissalSpring.getCurrentValue(); + }, + onPanResponderMove: (e, gestureState) => { + if (this.isResponderOnlyToBlockTouches) { + return; + } + this.dismissalSpring.setCurrentValue(gestureState.dx / SCREEN_WIDTH); + }, + onPanResponderRelease: (e, gestureState) => { + if (this.isResponderOnlyToBlockTouches) { + return; + } + var gestureCompletion = gestureState.dx / SCREEN_WIDTH; + var doesGestureRelease = (gestureState.vx + gestureCompletion) > 0.5; + this.dismissalSpring.setEndValue(doesGestureRelease ? 1 : 0); + } + }); + }, + render: function() { + var countText; + if (warningCounts.get(this.props.warning) > 1) { + countText = ( + + ({warningCounts.get(this.props.warning)}){" "} + + ); + } + return ( + { this.container = container; }} + {...this.panGesture.panHandlers}> + + + { this.text = text; }}> + {countText} + {this.props.warning} + + + + { this.closeButton = closeButton; }} + style={styles.closeButton}> + { + this.dismissalSpring.setEndValue(1); + }}> + ✕ + + + + ); + } +}); + +var WarningBoxOpened = React.createClass({ + render: function() { + var countText; + if (warningCounts.get(this.props.warning) > 1) { + countText = ( + + ({warningCounts.get(this.props.warning)}){" "} + + ); + } + return ( + + + + {countText} + {this.props.warning} + + + + + + Dismiss + + + + + + + Ignore + + + + + + + ); + }, +}); + +var canMountWarningBox = true; + +var WarningBox = React.createClass({ + getInitialState: function() { + return { + totalWarningCount, + openWarning: null, + }; + }, + componentWillMount: function() { + if (console.yellowBoxResetIgnored) { + AsyncStorage.setItem(IGNORED_WARNINGS_KEY, '[]', function(err) { + if (err) { + console.warn('Could not reset ignored warnings.', err); + } + }); + ignoredWarnings = []; + } + }, + componentDidMount: function() { + invariant( + canMountWarningBox, + 'There can only be one WarningBox' + ); + canMountWarningBox = false; + warningCountEvents.addListener( + 'count', + this._onWarningCount + ); + }, + componentWillUnmount: function() { + warningCountEvents.removeAllListeners(); + canMountWarningBox = true; + }, + _onWarningCount: function(totalWarningCount) { + // Must use setImmediate because warnings often happen during render and + // state cannot be set while rendering + setImmediate(() => { + this.setState({ totalWarningCount, }); + }); + }, + _onDismiss: function(warning) { + warningCounts.delete(warning); + this.setState({ + openWarning: null, + }); + }, + render: function() { + if (warningCounts.size === 0) { + return ; + } + if (this.state.openWarning) { + return ( + { + this.setState({ openWarning: null }); + }} + onDismissed={this._onDismiss.bind(this, this.state.openWarning)} + onIgnored={() => { + ignoredWarnings.push(this.state.openWarning); + saveIgnoredWarnings(); + this._onDismiss(this.state.openWarning); + }} + /> + ); + } + var warningRows = []; + warningCounts.forEach((count, warning) => { + warningRows.push( + { + this.setState({ openWarning: warning }); + }} + onDismissed={this._onDismiss.bind(this, warning)} + warning={warning} + /> + ); + }); + return ( + + {warningRows} + + ); + }, +}); + +var styles = StyleSheet.create({ + bold: { + fontWeight: 'bold', + }, + closeButton: { + position: 'absolute', + right: 0, + height: 46, + width: 46, + }, + closeButtonText: { + color: 'white', + fontSize: 32, + position: 'relative', + left: 8, + }, + warningContainer: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0 + }, + warningBox: { + position: 'relative', + backgroundColor: 'rgba(171, 124, 36, 0.9)', + flex: 1, + height: 46, + }, + warningText: { + color: 'white', + position: 'absolute', + left: 0, + marginLeft: 15, + marginRight: 46, + top: 7, + }, + yellowBox: { + backgroundColor: 'rgba(171, 124, 36, 0.9)', + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + padding: 15, + paddingTop: 35, + }, + yellowBoxText: { + color: 'white', + fontSize: 20, + }, + yellowBoxButtons: { + flexDirection: 'row', + position: 'absolute', + bottom: 0, + }, + yellowBoxButton: { + flex: 1, + padding: 25, + }, + yellowBoxButtonText: { + color: 'white', + fontSize: 16, + } +}); + +module.exports = WarningBox; diff --git a/Libraries/ReactIOS/diffRawProperties.js b/Libraries/ReactIOS/diffRawProperties.js index 3a5de284f4910e..ddd6edbea05c71 100644 --- a/Libraries/ReactIOS/diffRawProperties.js +++ b/Libraries/ReactIOS/diffRawProperties.js @@ -42,6 +42,16 @@ function diffRawProperties( } prevProp = prevProps && prevProps[propKey]; nextProp = nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof prevProp === 'function') { + prevProp = true; + } + if (typeof nextProp === 'function') { + nextProp = true; + } + if (prevProp !== nextProp) { // If you want a property's diff to be detected, you must configure it // to be so - *or* it must be a scalar property. For now, we'll allow @@ -75,6 +85,16 @@ function diffRawProperties( } prevProp = prevProps[propKey]; nextProp = nextProps && nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof prevProp === 'function') { + prevProp = true; + } + if (typeof nextProp === 'function') { + nextProp = true; + } + if (prevProp !== nextProp) { if (nextProp === undefined) { nextProp = null; // null is a sentinel we explicitly send to native diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js index 084390ac50057d..16052c6fa69605 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.ios.js @@ -12,6 +12,9 @@ 'use strict'; var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); +var WarningBox = require('WarningBox'); var invariant = require('invariant'); @@ -24,12 +27,27 @@ function renderApplication( rootTag, 'Expect to have a valid rootTag, instead got ', rootTag ); + var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled; + var warningBox = shouldRenderWarningBox ? : null; React.render( - , + + + {warningBox} + , rootTag ); } +var styles = StyleSheet.create({ + appContainer: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, +}); + module.exports = renderApplication; diff --git a/Libraries/ReactIOS/requireNativeComponent.js b/Libraries/ReactIOS/requireNativeComponent.js index 55ad8a6b989e9e..4d9241853626a4 100644 --- a/Libraries/ReactIOS/requireNativeComponent.js +++ b/Libraries/ReactIOS/requireNativeComponent.js @@ -41,18 +41,19 @@ function requireNativeComponent( viewName: string, wrapperComponent: ?Function ): Function { - var viewConfig = RCTUIManager.viewConfigs && RCTUIManager.viewConfigs[viewName]; - if (!viewConfig) { + var viewConfig = RCTUIManager[viewName]; + if (!viewConfig || !viewConfig.nativeProps) { return UnimplementedView; } var nativeProps = { - ...RCTUIManager.viewConfigs.RCTView.nativeProps, + ...RCTUIManager.RCTView.nativeProps, ...viewConfig.nativeProps, }; + viewConfig.uiViewClassName = viewName; viewConfig.validAttributes = {}; for (var key in nativeProps) { // TODO: deep diff by default in diffRawProperties instead of setting it here - var differ = TypeToDifferMap[nativeProps[key].type] || deepDiffer; + var differ = TypeToDifferMap[nativeProps[key]] || deepDiffer; viewConfig.validAttributes[key] = {diff: differ}; } if (__DEV__) { diff --git a/Libraries/ReactIOS/verifyPropTypes.js b/Libraries/ReactIOS/verifyPropTypes.js index 032e572ece118d..ab1d61728895fd 100644 --- a/Libraries/ReactIOS/verifyPropTypes.js +++ b/Libraries/ReactIOS/verifyPropTypes.js @@ -23,7 +23,7 @@ function verifyPropTypes( return; // This happens for UnimplementedView. } var nativeProps = viewConfig.nativeProps; - for (var prop in viewConfig.nativeProps) { + for (var prop in nativeProps) { if (!component.propTypes[prop] && !View.propTypes[prop] && !ReactIOSStyleAttributes[prop] && diff --git a/Libraries/Settings/RCTSettings.xcodeproj/project.pbxproj b/Libraries/Settings/RCTSettings.xcodeproj/project.pbxproj new file mode 100644 index 00000000000000..e4a210466c66f4 --- /dev/null +++ b/Libraries/Settings/RCTSettings.xcodeproj/project.pbxproj @@ -0,0 +1,250 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13DBA45E1AEE749000A17CF8 /* RCTSettingsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DBA45D1AEE749000A17CF8 /* RCTSettingsManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libRCTSettings.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTSettings.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 13DBA45C1AEE749000A17CF8 /* RCTSettingsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSettingsManager.h; sourceTree = ""; }; + 13DBA45D1AEE749000A17CF8 /* RCTSettingsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSettingsManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + 13DBA45C1AEE749000A17CF8 /* RCTSettingsManager.h */, + 13DBA45D1AEE749000A17CF8 /* RCTSettingsManager.m */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RCTSettings */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTSettings" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTSettings; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRCTSettings.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTSettings" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RCTSettings */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13DBA45E1AEE749000A17CF8 /* RCTSettingsManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTSettings; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTSettings; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTSettings" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTSettings" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/Libraries/Settings/RCTSettingsManager.h b/Libraries/Settings/RCTSettingsManager.h new file mode 100644 index 00000000000000..274cc69aed4328 --- /dev/null +++ b/Libraries/Settings/RCTSettingsManager.h @@ -0,0 +1,18 @@ +/** + * 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 + +#import "RCTBridgeModule.h" + +@interface RCTSettingsManager : NSObject + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Libraries/Settings/RCTSettingsManager.m b/Libraries/Settings/RCTSettingsManager.m new file mode 100644 index 00000000000000..6f8b146c1dc1ef --- /dev/null +++ b/Libraries/Settings/RCTSettingsManager.m @@ -0,0 +1,103 @@ +/** + * 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 "RCTSettingsManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTUtils.h" + +@implementation RCTSettingsManager +{ + BOOL _ignoringUpdates; + NSUserDefaults *_defaults; +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (instancetype)init +{ + return [self initWithUserDefaults:[NSUserDefaults standardUserDefaults]]; +} + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults +{ + if ((self = [super init])) { + _defaults = defaults; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(userDefaultsDidChange:) + name:NSUserDefaultsDidChangeNotification + object:_defaults]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)userDefaultsDidChange:(NSNotification *)note +{ + if (_ignoringUpdates) { + return; + } + + [_bridge.eventDispatcher + sendDeviceEventWithName:@"settingsUpdated" + body:RCTJSONClean([_defaults dictionaryRepresentation])]; +} + +- (NSDictionary *)constantsToExport +{ + return @{ + @"settings": RCTJSONClean([_defaults dictionaryRepresentation]) + }; +} + +/** + * Set one or more values in the settings. + * TODO: would it be useful to have a callback for when this has completed? + */ +RCT_EXPORT_METHOD(setValues:(NSDictionary *)values) +{ + _ignoringUpdates = YES; + [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id json, BOOL *stop) { + id plist = [RCTConvert NSPropertyList:json]; + if (plist) { + [_defaults setObject:plist forKey:key]; + } else { + [_defaults removeObjectForKey:key]; + } + }]; + + [_defaults synchronize]; + _ignoringUpdates = NO; +} + +/** + * Remove some values from the settings. + */ +RCT_EXPORT_METHOD(deleteValues:(NSStringArray *)keys) +{ + _ignoringUpdates = YES; + for (NSString *key in keys) { + [_defaults removeObjectForKey:key]; + } + + [_defaults synchronize]; + _ignoringUpdates = NO; +} + +@end diff --git a/Libraries/Settings/Settings.android.js b/Libraries/Settings/Settings.android.js new file mode 100644 index 00000000000000..d13a32ba9218bc --- /dev/null +++ b/Libraries/Settings/Settings.android.js @@ -0,0 +1,34 @@ +/** + * 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. + * + * @providesModule Settings + * @flow + */ +'use strict'; + +var Settings = { + get(key: string): mixed { + console.warn('Settings is not yet supported on Android'); + return null; + }, + + set(settings: Object) { + console.warn('Settings is not yet supported on Android'); + }, + + watchKeys(keys: string | Array, callback: Function): number { + console.warn('Settings is not yet supported on Android'); + return -1; + }, + + clearWatch(watchId: number) { + console.warn('Settings is not yet supported on Android'); + }, +}; + +module.exports = Settings; diff --git a/Libraries/Settings/Settings.ios.js b/Libraries/Settings/Settings.ios.js new file mode 100644 index 00000000000000..c1099df93cbb4f --- /dev/null +++ b/Libraries/Settings/Settings.ios.js @@ -0,0 +1,77 @@ +/** + * 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. + * + * @providesModule Settings + * @flow + */ +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTSettingsManager = require('NativeModules').SettingsManager; + +var invariant = require('invariant'); + +var subscriptions: Array<{keys: Array; callback: ?Function}> = []; + +var Settings = { + _settings: RCTSettingsManager.settings, + + get(key: string): mixed { + return this._settings[key]; + }, + + set(settings: Object) { + this._settings = Object.assign(this._settings, settings); + RCTSettingsManager.setValues(settings); + }, + + watchKeys(keys: string | Array, callback: Function): number { + if (typeof keys == 'string') { + keys = [keys]; + } + + invariant( + Array.isArray(keys), + 'keys should be a string or array of strings' + ); + + var sid = subscriptions.length; + subscriptions.push({keys: keys, callback: callback}) + return sid; + }, + + clearWatch(watchId: number) { + if (watchId < subscriptions.length) { + subscriptions[watchId] = {keys: [], callback: null}; + } + }, + + _sendObservations(body: Object) { + var _this = this; + Object.keys(body).forEach((key) => { + var newValue = body[key]; + var didChange = _this._settings[key] !== newValue; + _this._settings[key] = newValue; + + if (didChange) { + subscriptions.forEach((sub) => { + if (~sub.keys.indexOf(key) && sub.callback) { + sub.callback(); + } + }); + } + }); + }, +}; + +RCTDeviceEventEmitter.addListener( + 'settingsUpdated', + Settings._sendObservations.bind(Settings) +); + +module.exports = Settings; diff --git a/Libraries/Storage/AsyncStorage.ios.js b/Libraries/Storage/AsyncStorage.ios.js index 5ee2dc5e355b1e..fe92f5c58c5b96 100644 --- a/Libraries/Storage/AsyncStorage.ios.js +++ b/Libraries/Storage/AsyncStorage.ios.js @@ -37,7 +37,7 @@ var AsyncStorage = { */ getItem: function( key: string, - callback: (error: ?Error, result: ?string) => void + callback?: ?(error: ?Error, result: ?string) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet([key], function(errors, result) { @@ -60,7 +60,7 @@ var AsyncStorage = { setItem: function( key: string, value: string, - callback: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet([[key,value]], function(errors) { @@ -78,7 +78,7 @@ var AsyncStorage = { */ removeItem: function( key: string, - callback: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove([key], function(errors) { @@ -100,7 +100,7 @@ var AsyncStorage = { mergeItem: function( key: string, value: string, - callback: ?(error: ?Error) => void + callback?: ?(error: ?Error) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge([[key,value]], function(errors) { @@ -119,7 +119,7 @@ var AsyncStorage = { * don't want to call this - use removeItem or multiRemove to clear only your * own keys instead. Returns a `Promise` object. */ - clear: function(callback: ?(error: ?Error) => void): Promise { + clear: function(callback?: ?(error: ?Error) => void): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.clear(function(error) { callback && callback(convertError(error)); @@ -135,7 +135,7 @@ var AsyncStorage = { /** * Gets *all* keys known to the system, for all callers, libraries, etc. Returns a `Promise` object. */ - getAllKeys: function(callback: (error: ?Error) => void): Promise { + getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.getAllKeys(function(error, keys) { callback && callback(convertError(error), keys); @@ -166,7 +166,7 @@ var AsyncStorage = { */ multiGet: function( keys: Array, - callback: (errors: ?Array, result: ?Array>) => void + callback?: ?(errors: ?Array, result: ?Array>) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiGet(keys, function(errors, result) { @@ -189,7 +189,7 @@ var AsyncStorage = { */ multiSet: function( keyValuePairs: Array>, - callback: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { @@ -209,7 +209,7 @@ var AsyncStorage = { */ multiRemove: function( keys: Array, - callback: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiRemove(keys, function(errors) { @@ -232,7 +232,7 @@ var AsyncStorage = { */ multiMerge: function( keyValuePairs: Array>, - callback: ?(errors: ?Array) => void + callback?: ?(errors: ?Array) => void ): Promise { return new Promise((resolve, reject) => { RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { diff --git a/Libraries/StyleSheet/TransformPropTypes.js b/Libraries/StyleSheet/TransformPropTypes.js new file mode 100644 index 00000000000000..c9f403f75fd515 --- /dev/null +++ b/Libraries/StyleSheet/TransformPropTypes.js @@ -0,0 +1,36 @@ +/** + * 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. + * + * @providesModule TransformPropTypes + * @flow + */ +'use strict'; + +var ReactPropTypes = require('ReactPropTypes'); + +var TransformPropTypes = { + transform: ReactPropTypes.arrayOf( + ReactPropTypes.oneOfType([ + ReactPropTypes.shape({rotate: ReactPropTypes.string}), + ReactPropTypes.shape({scaleX: ReactPropTypes.number}), + ReactPropTypes.shape({scaleY: ReactPropTypes.number}), + ReactPropTypes.shape({translateX: ReactPropTypes.number}), + ReactPropTypes.shape({translateY: ReactPropTypes.number}) + ]) + ), + transformMatrix: ReactPropTypes.arrayOf(ReactPropTypes.number), + + // DEPRECATED + rotation: ReactPropTypes.number, + scaleX: ReactPropTypes.number, + scaleY: ReactPropTypes.number, + translateX: ReactPropTypes.number, + translateY: ReactPropTypes.number, +}; + +module.exports = TransformPropTypes; diff --git a/Libraries/StyleSheet/precomputeStyle.js b/Libraries/StyleSheet/precomputeStyle.js new file mode 100644 index 00000000000000..ea2990b94e3049 --- /dev/null +++ b/Libraries/StyleSheet/precomputeStyle.js @@ -0,0 +1,161 @@ +/** + * 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. + * + * @providesModule precomputeStyle + * @flow + */ +'use strict'; + +var MatrixMath = require('MatrixMath'); +var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); +var invariant = require('invariant'); + +/** + * This method provides a hook where flattened styles may be precomputed or + * otherwise prepared to become better input data for native code. + */ +function precomputeStyle(style: ?Object): ?Object { + if (!style || !style.transform) { + return style; + } + invariant( + !style.transformMatrix, + 'transformMatrix and transform styles cannot be used on the same component' + ); + var newStyle = _precomputeTransforms({...style}); + deepFreezeAndThrowOnMutationInDev(newStyle); + return newStyle; +} + +/** + * Generate a transform matrix based on the provided transforms, and use that + * within the style object instead. + * + * This allows us to provide an API that is similar to CSS and to have a + * universal, singular interface to native code. + */ +function _precomputeTransforms(style: Object): Object { + var {transform} = style; + var result = MatrixMath.createIdentityMatrix(); + + transform.forEach(transformation => { + var key = Object.keys(transformation)[0]; + var value = transformation[key]; + if (__DEV__) { + _validateTransform(key, value, transformation); + } + + switch (key) { + case 'matrix': + MatrixMath.multiplyInto(result, result, value); + break; + case 'rotate': + _multiplyTransform(result, MatrixMath.reuseRotateZCommand, [_convertToRadians(value)]); + break; + case 'scale': + _multiplyTransform(result, MatrixMath.reuseScaleCommand, [value]); + break; + case 'scaleX': + _multiplyTransform(result, MatrixMath.reuseScaleXCommand, [value]); + break; + case 'scaleY': + _multiplyTransform(result, MatrixMath.reuseScaleYCommand, [value]); + break; + case 'translate': + _multiplyTransform(result, MatrixMath.reuseTranslate3dCommand, [value[0], value[1], value[2] || 0]); + break; + case 'translateX': + _multiplyTransform(result, MatrixMath.reuseTranslate2dCommand, [value, 0]); + break; + case 'translateY': + _multiplyTransform(result, MatrixMath.reuseTranslate2dCommand, [0, value]); + break; + default: + throw new Error('Invalid transform name: ' + key); + } + }); + + return { + ...style, + transformMatrix: result, + }; +} + +/** + * Performs a destructive operation on a transform matrix. + */ +function _multiplyTransform( + result: Array, + matrixMathFunction: Function, + args: Array +): void { + var matrixToApply = MatrixMath.createIdentityMatrix(); + var argsWithIdentity = [matrixToApply].concat(args); + matrixMathFunction.apply(this, argsWithIdentity); + MatrixMath.multiplyInto(result, result, matrixToApply); +} + +/** + * Parses a string like '0.5rad' or '60deg' into radians expressed in a float. + * Note that validation on the string is done in `_validateTransform()`. + */ +function _convertToRadians(value: string): number { + var floatValue = parseFloat(value, 10); + return value.indexOf('rad') > -1 ? floatValue : floatValue * Math.PI / 180; +} + +function _validateTransform(key, value, transformation) { + var multivalueTransforms = [ + 'matrix', + 'translate', + ]; + if (multivalueTransforms.indexOf(key) !== -1) { + invariant( + Array.isArray(value), + 'Transform with key of %s must have an array as the value: %s', + key, + JSON.stringify(transformation) + ); + } + switch (key) { + case 'matrix': + invariant( + value.length === 9 || value.length === 16, + 'Matrix transform must have a length of 9 (2d) or 16 (3d). ' + + 'Provided matrix has a length of %s: %s', + value.length, + JSON.stringify(transformation) + ); + break; + case 'translate': + break; + case 'rotate': + invariant( + typeof value === 'string', + 'Transform with key of "%s" must be a string: %s', + key, + JSON.stringify(transformation) + ); + invariant( + value.indexOf('deg') > -1 || value.indexOf('rad') > -1, + 'Rotate transform must be expressed in degrees (deg) or radians ' + + '(rad): %s', + JSON.stringify(transformation) + ); + break; + default: + invariant( + typeof value === 'number', + 'Transform with key of "%s" must be a number: %s', + key, + JSON.stringify(transformation) + ); + } +} + +module.exports = precomputeStyle; diff --git a/Libraries/Text/RCTRawTextManager.m b/Libraries/Text/RCTRawTextManager.m index 221b8daebb6b59..b6ad9b1101dbb0 100644 --- a/Libraries/Text/RCTRawTextManager.m +++ b/Libraries/Text/RCTRawTextManager.m @@ -17,7 +17,7 @@ @implementation RCTRawTextManager - (UIView *)view { - return nil; + return [[UIView alloc] init]; // TODO(#1102) Remove useless views. } - (RCTShadowView *)shadowView diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 4201b1b4e8d0dc..4ea9d3bd6fc093 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -22,15 +22,21 @@ static css_dim_t RCTMeasure(void *context, float width) RCTShadowText *shadowText = (__bridge RCTShadowText *)context; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[shadowText attributedString]]; + NSTextStorage *previousTextStorage = shadowText.layoutManager.textStorage; + if (previousTextStorage) { + [previousTextStorage removeLayoutManager:shadowText.layoutManager]; + } [textStorage addLayoutManager:shadowText.layoutManager]; shadowText.textContainer.size = CGSizeMake(isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX); - shadowText.layoutManager.textStorage = textStorage; [shadowText.layoutManager ensureLayoutForTextContainer:shadowText.textContainer]; CGSize computedSize = [shadowText.layoutManager usedRectForTextContainer:shadowText.textContainer].size; [textStorage removeLayoutManager:shadowText.layoutManager]; + if (previousTextStorage) { + [previousTextStorage addLayoutManager:shadowText.layoutManager]; + } css_dim_t result; result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width); diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 84f6b85e1f0882..9d2ade8e43082a 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -25,6 +25,7 @@ - (instancetype)initWithFrame:(CGRect)frame if ((self = [super initWithFrame:frame])) { _textStorage = [[NSTextStorage alloc] init]; + self.opaque = NO; self.contentMode = UIViewContentModeRedraw; } @@ -94,21 +95,18 @@ - (CGRect)textFrame return UIEdgeInsetsInsetRect(self.bounds, _contentInset); } -- (void)layoutSubviews +- (void)drawRect:(CGRect)rect { - [super layoutSubviews]; + CGRect textFrame = [self textFrame]; - // The header comment for `size` says that a height of 0.0 should be enough, - // but it isn't. - _textContainer.size = CGSizeMake([self textFrame].size.width, CGFLOAT_MAX); -} + // We reset the text container size every time because RCTShadowText's + // RCTMeasure overrides it. The header comment for `size` says that a height + // of 0.0 should be enough, but it isn't. + _textContainer.size = CGSizeMake(textFrame.size.width, CGFLOAT_MAX); -- (void)drawRect:(CGRect)rect -{ - CGPoint origin = [self textFrame].origin; NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer]; - [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:origin]; - [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:origin]; + [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin]; + [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin]; } - (NSNumber *)reactTagAtPoint:(CGPoint)point diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj index 0304ef851ea3cf..224c7e6b97fa7e 100644 --- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */; }; + 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */; }; 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */; }; 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */; }; 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; }; @@ -27,6 +29,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextView.h; sourceTree = ""; }; + 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextView.m; sourceTree = ""; }; + 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextViewManager.h; sourceTree = ""; }; + 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextViewManager.m; sourceTree = ""; }; 58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; }; 58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = ""; }; 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = ""; }; @@ -64,6 +70,10 @@ 58B512141A9E6EFF00147676 /* RCTText.m */, 58B511CC1A9E6C5C00147676 /* RCTTextManager.h */, 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */, + 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */, + 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */, + 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */, + 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */, 58B5119C1A9E6C1200147676 /* Products */, ); indentWidth = 2; @@ -135,8 +145,10 @@ buildActionMask = 2147483647; files = ( 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */, + 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */, 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */, 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, + 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */, 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */, 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */, ); @@ -222,6 +234,7 @@ 58B511B01A9E6C1300147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -229,6 +242,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -236,6 +250,7 @@ 58B511B11A9E6C1300147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/Text/RCTTextManager.h b/Libraries/Text/RCTTextManager.h index 13e8f854642da4..91ac87ba88fdbc 100644 --- a/Libraries/Text/RCTTextManager.h +++ b/Libraries/Text/RCTTextManager.h @@ -12,4 +12,3 @@ @interface RCTTextManager : RCTViewManager @end - diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index b8dabcf2c34f1c..ef518d20483d70 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -46,7 +46,7 @@ - (RCTShadowView *)shadowView RCT_EXPORT_SHADOW_PROPERTY(fontStyle, NSString) RCT_EXPORT_SHADOW_PROPERTY(isHighlighted, BOOL) RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat) -RCT_EXPORT_SHADOW_PROPERTY(maxNumberOfLines, NSInteger) +RCT_EXPORT_SHADOW_PROPERTY(maximumNumberOfLines, NSInteger) RCT_EXPORT_SHADOW_PROPERTY(shadowOffset, CGSize) RCT_EXPORT_SHADOW_PROPERTY(textAlign, NSTextAlignment) RCT_REMAP_SHADOW_PROPERTY(backgroundColor, textBackgroundColor, UIColor) @@ -123,7 +123,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowVie UIEdgeInsets padding = shadowView.paddingAsInsets; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - RCTText *text = (RCTText *)viewRegistry[reactTag]; + RCTText *text = viewRegistry[reactTag]; text.contentInset = padding; text.layoutManager = shadowView.layoutManager; text.textContainer = shadowView.textContainer; diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h new file mode 100644 index 00000000000000..19f2fea397b8c5 --- /dev/null +++ b/Libraries/Text/RCTTextView.h @@ -0,0 +1,29 @@ +/** + * 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 + +#import "RCTView.h" +#import "UIView+React.h" + +@class RCTEventDispatcher; + +@interface RCTTextView : RCTView + +@property (nonatomic, assign) BOOL autoCorrect; +@property (nonatomic, assign) BOOL clearTextOnFocus; +@property (nonatomic, assign) BOOL selectTextOnFocus; +@property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, strong) UIColor *placeholderTextColor; +@property (nonatomic, assign) UIFont *font; + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m new file mode 100644 index 00000000000000..c5947f317e0bd8 --- /dev/null +++ b/Libraries/Text/RCTTextView.m @@ -0,0 +1,216 @@ +/** + * 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 "RCTTextView.h" + +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTUtils.h" +#import "UIView+React.h" + +@implementation RCTTextView +{ + RCTEventDispatcher *_eventDispatcher; + BOOL _jsRequestingFirstResponder; + NSString *_placeholder; + UITextView *_placeholderView; + UITextView *_textView; +} + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _contentInset = UIEdgeInsetsZero; + _eventDispatcher = eventDispatcher; + _placeholderTextColor = [self defaultPlaceholderTextColor]; + + _textView = [[UITextView alloc] initWithFrame:self.bounds]; + _textView.backgroundColor = [UIColor clearColor]; + _textView.delegate = self; + [self addSubview:_textView]; + } + + return self; +} + +- (void)updateFrames +{ + // Adjust the insets so that they are as close as possible to single-line + // RCTTextField defaults + UIEdgeInsets adjustedInset = (UIEdgeInsets){ + _contentInset.top - 5, _contentInset.left - 4, + _contentInset.bottom, _contentInset.right + }; + + [_textView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; + [_placeholderView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; +} + +- (void)updatePlaceholder +{ + [_placeholderView removeFromSuperview]; + _placeholderView = nil; + + if (_placeholder) { + _placeholderView = [[UITextView alloc] initWithFrame:self.bounds]; + _placeholderView.backgroundColor = [UIColor clearColor]; + _placeholderView.scrollEnabled = false; + _placeholderView.attributedText = + [[NSAttributedString alloc] initWithString:_placeholder attributes:@{ + NSFontAttributeName : (_textView.font ? _textView.font : [self defaultPlaceholderFont]), + NSForegroundColorAttributeName : _placeholderTextColor + }]; + + [self insertSubview:_placeholderView belowSubview:_textView]; + [self _setPlaceholderVisibility]; + } +} + +- (void)setFont:(UIFont *)font +{ + _font = font; + _textView.font = _font; + [self updatePlaceholder]; +} + +- (void)setTextColor:(UIColor *)textColor +{ + _textView.textColor = textColor; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + _placeholder = placeholder; + [self updatePlaceholder]; +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + if (placeholderTextColor) { + _placeholderTextColor = placeholderTextColor; + } else { + _placeholderTextColor = [self defaultPlaceholderTextColor]; + } + [self updatePlaceholder]; +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + _contentInset = contentInset; + [self updateFrames]; +} + +- (void)setText:(NSString *)text +{ + if (![text isEqualToString:_textView.text]) { + [_textView setText:text]; + [self _setPlaceholderVisibility]; + } +} + +- (void)_setPlaceholderVisibility +{ + if (_textView.text.length > 0) { + [_placeholderView setHidden:YES]; + } else { + [_placeholderView setHidden:NO]; + } +} + +- (void)setAutoCorrect:(BOOL)autoCorrect +{ + _textView.autocorrectionType = (autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo); +} + +- (BOOL)autoCorrect +{ + return _textView.autocorrectionType == UITextAutocorrectionTypeYes; +} + +- (BOOL)textViewShouldBeginEditing:(UITextView *)textView +{ + if (_selectTextOnFocus) { + dispatch_async(dispatch_get_main_queue(), ^{ + [textView selectAll:nil]; + }); + } + return YES; +} + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + if (_clearTextOnFocus) { + [_textView setText:@""]; + _textView.text = @""; + [self _setPlaceholderVisibility]; + } + + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus + reactTag:self.reactTag + text:textView.text]; +} + +- (void)textViewDidChange:(UITextView *)textView +{ + [self _setPlaceholderVisibility]; + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange + reactTag:self.reactTag + text:textView.text]; + +} + +- (void)textViewDidEndEditing:(UITextView *)textView +{ + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd + reactTag:self.reactTag + text:textView.text]; +} + +- (BOOL)becomeFirstResponder +{ + _jsRequestingFirstResponder = YES; + BOOL result = [_textView becomeFirstResponder]; + _jsRequestingFirstResponder = NO; + return result; +} + +- (BOOL)resignFirstResponder +{ + [super resignFirstResponder]; + BOOL result = [_textView resignFirstResponder]; + if (result) { + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur + reactTag:self.reactTag + text:_textView.text]; + } + return result; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self updateFrames]; +} + +- (BOOL)canBecomeFirstResponder +{ + return _jsRequestingFirstResponder; +} + +- (UIFont *)defaultPlaceholderFont +{ + return [UIFont fontWithName:@"Helvetica" size:17]; +} + +- (UIColor *)defaultPlaceholderTextColor +{ + return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22]; +} + +@end diff --git a/Libraries/Text/RCTTextViewManager.h b/Libraries/Text/RCTTextViewManager.h new file mode 100644 index 00000000000000..fd2f2b44d3d334 --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.h @@ -0,0 +1,14 @@ +/** + * 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 RCTTextViewManager : RCTViewManager + +@end diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m new file mode 100644 index 00000000000000..570a511157bee5 --- /dev/null +++ b/Libraries/Text/RCTTextViewManager.m @@ -0,0 +1,65 @@ +/** + * 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 "RCTTextViewManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTShadowView.h" +#import "RCTSparseArray.h" +#import "RCTTextView.h" + +@implementation RCTTextViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) +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(clearTextOnFocus, BOOL) +RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) +RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType) +RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType) +RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL) +RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) +RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType) +RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; +} +RCT_CUSTOM_VIEW_PROPERTY(fontWeight, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withWeight:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontStyle, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withStyle:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView) +{ + view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName]; +} + +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView +{ + NSNumber *reactTag = shadowView.reactTag; + UIEdgeInsets padding = shadowView.paddingAsInsets; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + ((RCTTextView *)viewRegistry[reactTag]).contentInset = padding; + }; +} + +@end diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index ce7b4078e9acd4..a67abacb087ec5 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -33,7 +33,7 @@ var viewConfig = { }; /** - * A react component for displaying text which supports nesting, + * A React component for displaying text which supports nesting, * styling, and touch handling. In the following example, the nested title and * body text will inherit the `fontFamily` from `styles.baseText`, but the title * provides its own additional styles. The title and body will stack on top of diff --git a/Libraries/Text/TextUpdateTest.js b/Libraries/Text/TextUpdateTest.js new file mode 100644 index 00000000000000..c4218f73d5a40a --- /dev/null +++ b/Libraries/Text/TextUpdateTest.js @@ -0,0 +1,61 @@ +/** + * 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. + * + * @providesModule TextUpdateTest + * @flow + */ +'use strict'; + +var React = require('react-native'); +var TimerMixin = require('react-timer-mixin'); +var { + NativeModules, + StyleSheet, + Text, +} = React; + +var TestManager = NativeModules.TestManager || NativeModules.SnapshotTestManager; + +var TextUpdateTest = React.createClass({ + mixins: [TimerMixin], + getInitialState: function() { + return {seeMore: true}; + }, + componentDidMount: function() { + this.requestAnimationFrame( + () => this.setState( + {seeMore: false}, + TestManager.markTestCompleted + ) + ); + }, + render: function() { + return ( + this.setState({seeMore: !this.state.seeMore})}> + Tap to see more (bugs)... + {this.state.seeMore && 'raw text'} + + ); + }, +}); + +var styles = StyleSheet.create({ + container: { + margin: 10, + marginTop: 100, + }, +}); + +module.exports = TextUpdateTest; diff --git a/Libraries/Utilities/AlertIOS.js b/Libraries/Utilities/AlertIOS.js index de72d7b9f07306..0d1612bc9e0d9e 100644 --- a/Libraries/Utilities/AlertIOS.js +++ b/Libraries/Utilities/AlertIOS.js @@ -37,7 +37,7 @@ var DEFAULT_BUTTON = { * {text: 'Foo', onPress: () => console.log('Foo Pressed!')}, * {text: 'Bar', onPress: () => console.log('Bar Pressed!')}, * ] - * )} + * ) * ``` */ diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index fe28a7684c9d46..b93000a33a8f50 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -20,7 +20,7 @@ var dimensions = NativeModules.UIManager.Dimensions; // We calculate the window dimensions in JS so that we don't encounter loss of // precision in transferring the dimensions (which could be non-integers) over // the bridge. -if (dimensions.windowPhysicalPixels) { +if (dimensions && dimensions.windowPhysicalPixels) { // parse/stringify => Clone hack dimensions = JSON.parse(JSON.stringify(dimensions)); diff --git a/Libraries/Utilities/MatrixMath.js b/Libraries/Utilities/MatrixMath.js new file mode 100755 index 00000000000000..7f3d17c461b70a --- /dev/null +++ b/Libraries/Utilities/MatrixMath.js @@ -0,0 +1,131 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule MatrixMath + */ +'use strict'; + +/** + * Memory conservative (mutative) matrix math utilities. Uses "command" + * matrices, which are reusable. + */ +var MatrixMath = { + createIdentityMatrix: function() { + return [ + 1,0,0,0, + 0,1,0,0, + 0,0,1,0, + 0,0,0,1 + ]; + }, + + createCopy: function(m) { + return [ + m[0], m[1], m[2], m[3], + m[4], m[5], m[6], m[7], + m[8], m[9], m[10], m[11], + m[12], m[13], m[14], m[15], + ]; + }, + + createTranslate2d: function(x, y) { + var mat = MatrixMath.createIdentityMatrix(); + MatrixMath.reuseTranslate2dCommand(mat, x, y); + return mat; + }, + + reuseTranslate2dCommand: function(matrixCommand, x, y) { + matrixCommand[12] = x; + matrixCommand[13] = y; + }, + + reuseTranslate3dCommand: function(matrixCommand, x, y, z) { + matrixCommand[12] = x; + matrixCommand[13] = y; + matrixCommand[14] = z; + }, + + createScale: function(factor) { + var mat = MatrixMath.createIdentityMatrix(); + MatrixMath.reuseScaleCommand(mat, factor); + return mat; + }, + + reuseScaleCommand: function(matrixCommand, factor) { + matrixCommand[0] = factor; + matrixCommand[5] = factor; + }, + + reuseScale3dCommand: function(matrixCommand, x, y, z) { + matrixCommand[0] = x; + matrixCommand[5] = y; + matrixCommand[10] = z; + }, + + reuseScaleXCommand(matrixCommand, factor) { + matrixCommand[0] = factor; + }, + + reuseScaleYCommand(matrixCommand, factor) { + matrixCommand[5] = factor; + }, + + reuseScaleZCommand(matrixCommand, factor) { + matrixCommand[10] = factor; + }, + + reuseRotateYCommand: function(matrixCommand, amount) { + matrixCommand[0] = Math.cos(amount); + matrixCommand[2] = Math.sin(amount); + matrixCommand[8] = Math.sin(-amount); + matrixCommand[10] = Math.cos(amount); + }, + + createRotateZ: function(radians) { + var mat = MatrixMath.createIdentityMatrix(); + MatrixMath.reuseRotateZCommand(mat, radians); + return mat; + }, + + // http://www.w3.org/TR/css3-transforms/#recomposing-to-a-2d-matrix + reuseRotateZCommand: function(matrixCommand, radians) { + matrixCommand[0] = Math.cos(radians); + matrixCommand[1] = Math.sin(radians); + matrixCommand[4] = -Math.sin(radians); + matrixCommand[5] = Math.cos(radians); + }, + + multiplyInto: function(out, a, b) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; + + var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; + out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; + out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; + out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; + out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + } + +}; + +module.exports = MatrixMath; diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index c047d06deecf93..f15dd70e0d9b84 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -10,7 +10,9 @@ * @flow */ 'use strict'; + var ErrorUtils = require('ErrorUtils'); +var ReactUpdates = require('ReactUpdates'); var invariant = require('invariant'); var warning = require('warning'); @@ -19,6 +21,9 @@ var JSTimersExecution = require('JSTimersExecution'); var INTERNAL_ERROR = 'Error in MessageQueue implementation'; +// Prints all bridge traffic to console.log +var DEBUG_SPY_MODE = false; + type ModulesConfig = { [key:string]: { moduleID: number; @@ -261,6 +266,9 @@ var MessageQueueMixin = { 'both the success callback and the error callback.', cbID ); + if (DEBUG_SPY_MODE) { + console.log('N->JS: Callback#' + cbID + '(' + JSON.stringify(args) + ')'); + } cb.apply(scope, args); } catch(ie_requires_catch) { throw ie_requires_catch; @@ -290,6 +298,11 @@ var MessageQueueMixin = { var moduleName = this._localModuleIDToModuleName[moduleID]; var methodName = this._localModuleNameToMethodIDToName[moduleName][methodID]; + if (DEBUG_SPY_MODE) { + console.log( + 'N->JS: ' + moduleName + '.' + methodName + + '(' + JSON.stringify(params) + ')'); + } var ret = jsCall(this._requireFunc(moduleName), methodName, params); return ret; @@ -307,23 +320,26 @@ var MessageQueueMixin = { ); }, - processBatch: function (batch) { + processBatch: function(batch) { var self = this; - batch.forEach(function (call) { - invariant( - call.module === 'BatchedBridge', - 'All the calls should pass through the BatchedBridge module' - ); - if (call.method === 'callFunctionReturnFlushedQueue') { - self.callFunction.apply(self, call.args); - } else if (call.method === 'invokeCallbackAndReturnFlushedQueue') { - self.invokeCallback.apply(self, call.args); - } else { - throw new Error( - 'Unrecognized method called on BatchedBridge: ' + call.method); - } - }); - return this.flushedQueue(); + return guardReturn(function () { + ReactUpdates.batchedUpdates(function() { + batch.forEach(function(call) { + invariant( + call.module === 'BatchedBridge', + 'All the calls should pass through the BatchedBridge module' + ); + if (call.method === 'callFunctionReturnFlushedQueue') { + self._callFunction.apply(self, call.args); + } else if (call.method === 'invokeCallbackAndReturnFlushedQueue') { + self._invokeCallback.apply(self, call.args); + } else { + throw new Error( + 'Unrecognized method called on BatchedBridge: ' + call.method); + } + }); + }); + }, null, this._flushedQueueUnguarded, this); }, setLoggingEnabled: function(enabled) { @@ -456,6 +472,17 @@ var MessageQueueMixin = { var ret = currentOutgoingItems[REQUEST_MODULE_IDS].length || currentOutgoingItems[RESPONSE_RETURN_VALUES].length ? currentOutgoingItems : null; + if (DEBUG_SPY_MODE && ret) { + for (var i = 0; i < currentOutgoingItems[0].length; i++) { + var moduleName = this._remoteModuleIDToModuleName[currentOutgoingItems[0][i]]; + var methodName = + this._remoteModuleNameToMethodIDToName[moduleName][currentOutgoingItems[1][i]]; + console.log( + 'JS->N: ' + moduleName + '.' + methodName + + '(' + JSON.stringify(currentOutgoingItems[2][i]) + ')'); + } + } + return ret; }, diff --git a/Libraries/Utilities/PixelRatio.js b/Libraries/Utilities/PixelRatio.js index f8e23398a9c802..e6c96b7fa6b30e 100644 --- a/Libraries/Utilities/PixelRatio.js +++ b/Libraries/Utilities/PixelRatio.js @@ -36,8 +36,8 @@ var Dimensions = require('Dimensions'); * * ``` * var image = getImage({ - * width: 200 * PixelRatio.get(), - * height: 100 * PixelRatio.get() + * width: PixelRatio.getPixelSizeForLayoutSize(200), + * height: PixelRatio.getPixelSizeForLayoutSize(100), * }); * * ``` @@ -52,10 +52,21 @@ class PixelRatio { * - iPhone 6 * - PixelRatio.get() === 3 * - iPhone 6 plus + * - PixelRatio.get() === 3.5 + * - Nexus 6 */ static get(): number { return Dimensions.get('window').scale; } + + /** + * Converts a layout size (dp) to pixel size (px). + * + * Guaranteed to return an integer number. + */ + static getPixelSizeForLayoutSize(layoutSize: number): number { + return Math.round(layoutSize * PixelRatio.get()); + } } // No-op for iOS, but used on the web. Should not be documented. diff --git a/Libraries/Utilities/__mocks__/ErrorUtils.js b/Libraries/Utilities/__mocks__/ErrorUtils.js new file mode 100644 index 00000000000000..99db791774a441 --- /dev/null +++ b/Libraries/Utilities/__mocks__/ErrorUtils.js @@ -0,0 +1,23 @@ +// This mock only provides short-circuited methods of applyWithGuard and guard. +// A lot of modules rely on these two functions. This mock relieves their tests +// from depending on the real ErrorUtils module. If you need real error handling +// don't use this mock. +'use strict'; + +function execute(fun, context, args) { + return fun.apply(context, args); +}; + +function reportError(error) { + throw error; +} + +var ErrorUtils = { + apply: jest.genMockFunction().mockImplementation(execute), + applyWithGuard: jest.genMockFunction().mockImplementation(execute), + inGuard: jest.genMockFunction().mockReturnValue(true), + reportError: jest.genMockFunction().mockImplementation(reportError), + setGlobalHandler: jest.genMockFunction(), +}; + +module.exports = ErrorUtils; diff --git a/Libraries/Utilities/__mocks__/PixelRatio.js b/Libraries/Utilities/__mocks__/PixelRatio.js new file mode 100644 index 00000000000000..bcf04d7fe48a6f --- /dev/null +++ b/Libraries/Utilities/__mocks__/PixelRatio.js @@ -0,0 +1,20 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var PixelRatio = { + startDetecting: function () { + // noop for our implementation + }, + + get: function() { + return 2; + }, + + getPixelSizeForLayoutSize: function (layoutSize) { + return Math.round(layoutSize * PixelRatio.get()); + } +}; + +module.exports = PixelRatio; diff --git a/Libraries/Utilities/differ/__tests__/deepDiffer-test.js b/Libraries/Utilities/differ/__tests__/deepDiffer-test.js new file mode 100644 index 00000000000000..745b83ab093053 --- /dev/null +++ b/Libraries/Utilities/differ/__tests__/deepDiffer-test.js @@ -0,0 +1,102 @@ +/** + * 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. + */ +'use strict'; + +jest.dontMock('deepDiffer'); +var deepDiffer = require('deepDiffer'); + +describe('deepDiffer', function() { + it('should diff primitives of the same type', () => { + expect(deepDiffer(1, 2)).toBe(true); + expect(deepDiffer(42, 42)).toBe(false); + expect(deepDiffer('foo', 'bar')).toBe(true); + expect(deepDiffer('foo', 'foo')).toBe(false); + expect(deepDiffer(true, false)).toBe(true); + expect(deepDiffer(false, true)).toBe(true); + expect(deepDiffer(true, true)).toBe(false); + expect(deepDiffer(false, false)).toBe(false); + expect(deepDiffer(null, null)).toBe(false); + expect(deepDiffer(undefined, undefined)).toBe(false); + }); + it('should diff primitives of different types', () => { + expect(deepDiffer(1, '1')).toBe(true); + expect(deepDiffer(true, 'true')).toBe(true); + expect(deepDiffer(true, 1)).toBe(true); + expect(deepDiffer(false, 0)).toBe(true); + expect(deepDiffer(null, undefined)).toBe(true); + expect(deepDiffer(null, 0)).toBe(true); + expect(deepDiffer(null, false)).toBe(true); + expect(deepDiffer(null, '')).toBe(true); + expect(deepDiffer(undefined, 0)).toBe(true); + expect(deepDiffer(undefined, false)).toBe(true); + expect(deepDiffer(undefined, '')).toBe(true); + }); + it('should diff Objects', () => { + expect(deepDiffer({}, {})).toBe(false); + expect(deepDiffer({}, null)).toBe(true); + expect(deepDiffer(null, {})).toBe(true); + expect(deepDiffer({a: 1}, {a: 1})).toBe(false); + expect(deepDiffer({a: 1}, {a: 2})).toBe(true); + expect(deepDiffer({a: 1}, {a: 1, b: null})).toBe(true); + expect(deepDiffer({a: 1}, {a: 1, b: 1})).toBe(true); + expect(deepDiffer({a: 1, b: 1}, {a: 1})).toBe(true); + expect(deepDiffer({a: {A: 1}, b: 1}, {a: {A: 1}, b: 1})).toBe(false); + expect(deepDiffer({a: {A: 1}, b: 1}, {a: {A: 2}, b: 1})).toBe(true); + expect(deepDiffer( + {a: {A: {aA: 1, bB: 1}}, b: 1}, + {a: {A: {aA: 1, bB: 1}}, b: 1} + )).toBe(false); + expect(deepDiffer( + {a: {A: {aA: 1, bB: 1}}, b: 1}, + {a: {A: {aA: 1, cC: 1}}, b: 1} + )).toBe(true); + }); + it('should diff Arrays', () => { + expect(deepDiffer([], [])).toBe(false); + expect(deepDiffer([], null)).toBe(true); + expect(deepDiffer(null, [])).toBe(true); + expect(deepDiffer([42], [42])).toBe(false); + expect(deepDiffer([1], [2])).toBe(true); + expect(deepDiffer([1, 2, 3], [1, 2, 3])).toBe(false); + expect(deepDiffer([1, 2, 3], [1, 2, 4])).toBe(true); + expect(deepDiffer([1, 2, 3], [1, 4, 3])).toBe(true); + expect(deepDiffer([1, 2, 3, 4], [1, 2, 3])).toBe(true); + expect(deepDiffer([1, 2, 3], [1, 2, 3, 4])).toBe(true); + expect(deepDiffer([0, null, false, ''], [0, null, false, ''])).toBe(false); + expect(deepDiffer([0, null, false, ''], ['', false, null, 0])).toBe(true); + }); + it('should diff mixed types', () => { + expect(deepDiffer({}, [])).toBe(true); + expect(deepDiffer([], {})).toBe(true); + expect(deepDiffer( + {a: [{A: {aA: 1, bB: 1}}, 'bar'], c: [1, [false]]}, + {a: [{A: {aA: 1, bB: 1}}, 'bar'], c: [1, [false]]} + )).toBe(false); + expect(deepDiffer( + {a: [{A: {aA: 1, bB: 1}}, 'bar'], c: [1, [false]]}, + {a: [{A: {aA: 1, bB: 2}}, 'bar'], c: [1, [false]]} + )).toBe(true); + expect(deepDiffer( + {a: [{A: {aA: 1, bB: 1}}, 'bar'], c: [1, [false]]}, + {a: [{A: {aA: 1, bB: 1}}, 'bar'], c: [1, [false], null]} + )).toBe(true); + expect(deepDiffer( + {a: [{A: {aA: 1, bB: 1}}, 'bar'], c: [1, [false]]}, + {a: [{A: {aA: 1, bB: 1}}, ['bar']], c: [1, [false]]} + )).toBe(true); + }); + it('should distinguish between proper Array and Object', () => { + expect(deepDiffer(['a', 'b'], {0: 'a', 1: 'b', length: 2})).toBe(true); + expect(deepDiffer(['a', 'b'], {length: 2, 0: 'a', 1: 'b'})).toBe(true); + }); + it('should diff same object', () => { + var obj = [1,[2,3]]; + expect(deepDiffer(obj, obj)).toBe(false); + }); +}); diff --git a/Libraries/Utilities/differ/deepDiffer.js b/Libraries/Utilities/differ/deepDiffer.js index 66dc7fa95cf585..4dec6bd7bb892d 100644 --- a/Libraries/Utilities/differ/deepDiffer.js +++ b/Libraries/Utilities/differ/deepDiffer.js @@ -35,16 +35,29 @@ var deepDiffer = function(one: any, two: any): bool { if (one.constructor !== two.constructor) { return true; } - for (var key in one) { - if (deepDiffer(one[key], two[key])) { + if (Array.isArray(one)) { + // We know two is also an array because the constructors are equal + var len = one.length; + if (two.length !== len) { return true; } - } - for (var twoKey in two) { - // The only case we haven't checked yet is keys that are in two but aren't - // in one, which means they are different. - if (one[twoKey] === undefined && two[twoKey] !== undefined) { - return true; + for (var ii = 0; ii < len; ii++) { + if (deepDiffer(one[ii], two[ii])) { + return true; + } + } + } else { + for (var key in one) { + if (deepDiffer(one[key], two[key])) { + return true; + } + } + for (var twoKey in two) { + // The only case we haven't checked yet is keys that are in two but aren't + // in one, which means they are different. + if (one[twoKey] === undefined && two[twoKey] !== undefined) { + return true; + } } } return false; diff --git a/Libraries/Utilities/differ/sizesDiffer.js b/Libraries/Utilities/differ/sizesDiffer.js index 3bdc72acb23384..a431cd072273c8 100644 --- a/Libraries/Utilities/differ/sizesDiffer.js +++ b/Libraries/Utilities/differ/sizesDiffer.js @@ -5,14 +5,14 @@ */ 'use strict'; -var dummySize = {w: undefined, h: undefined}; +var dummySize = {width: undefined, height: undefined}; var sizesDiffer = function(one, two) { one = one || dummySize; two = two || dummySize; return one !== two && ( - one.w !== two.w || - one.h !== two.h + one.width !== two.width || + one.height !== two.height ); }; diff --git a/Libraries/Utilities/stringifySafe.js b/Libraries/Utilities/stringifySafe.js new file mode 100644 index 00000000000000..750a932b644970 --- /dev/null +++ b/Libraries/Utilities/stringifySafe.js @@ -0,0 +1,49 @@ +/** + * 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. + * + * @providesModule stringifySafe + * @flow + */ +'use strict'; + +/** + * Tries to stringify with JSON.stringify and toString, but catches exceptions + * (e.g. from circular objects) and always returns a string and never throws. + */ +function stringifySafe(arg: any): string { + var ret; + var type = typeof arg; + if (arg === undefined) { + ret = 'undefined'; + } else if (arg === null) { + ret = 'null'; + } else if (type === 'string') { + ret = '"' + arg + '"'; + } else if (type === 'function') { + try { + ret = arg.toString(); + } catch (e) { + ret = '[function unknown]'; + } + } else { + // Perform a try catch, just in case the object has a circular + // reference or stringify throws for some other reason. + try { + ret = JSON.stringify(arg); + } catch (e) { + if (typeof arg.toString === 'function') { + try { + ret = arg.toString(); + } catch (E) {} + } + } + } + return ret || '["' + type + '" failed to stringify]'; +} + +module.exports = stringifySafe; diff --git a/Libraries/Vibration/RCTVibration.xcodeproj/project.pbxproj b/Libraries/Vibration/RCTVibration.xcodeproj/project.pbxproj index bc8a47cbf7c9ad..f8aec3fedde4d0 100644 --- a/Libraries/Vibration/RCTVibration.xcodeproj/project.pbxproj +++ b/Libraries/Vibration/RCTVibration.xcodeproj/project.pbxproj @@ -208,6 +208,7 @@ 832C81951AAF6DF0007FA2F7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, @@ -215,6 +216,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -222,6 +224,7 @@ 832C81961AAF6DF0007FA2F7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, diff --git a/Libraries/react-native/react-native-interface.js b/Libraries/react-native/react-native-interface.js index 6408526b812e6a..b76518387826eb 100644 --- a/Libraries/react-native/react-native-interface.js +++ b/Libraries/react-native/react-native-interface.js @@ -16,3 +16,8 @@ declare var __DEV__: boolean; declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{ inject: ?((stuff: Object) => void) };*/ + +declare var fetch: any; +declare var Headers: any; +declare var Request: any; +declare var Response: any; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 01bff7eae5dfb0..b94b172f37de61 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -27,6 +27,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), Navigator: require('Navigator'), + SegmentedControlIOS: require('SegmentedControlIOS'), ScrollView: require('ScrollView'), SliderIOS: require('SliderIOS'), SwitchIOS: require('SwitchIOS'), diff --git a/Libraries/vendor/react/platform/NodeHandle.js b/Libraries/vendor/react/platform/NodeHandle.js index 19f74c6ece0b73..c7c93545bf0317 100644 --- a/Libraries/vendor/react/platform/NodeHandle.js +++ b/Libraries/vendor/react/platform/NodeHandle.js @@ -19,7 +19,7 @@ * worker thread. * * The only other requirement of a platform/environment is that it always be - * possible to extract the react rootNodeID in a blocking manner (see + * possible to extract the React rootNodeID in a blocking manner (see * `getRootNodeID`). * * +------------------+ +------------------+ +------------------+ diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index 020731fde52cbe..37c42382712627 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -436,6 +436,7 @@ var TouchableMixin = { if (isTouchWithinActive) { this._receiveSignal(Signals.ENTER_PRESS_RECT, e); } else { + this._cancelLongPressDelayTimeout(); this._receiveSignal(Signals.LEAVE_PRESS_RECT, e); } }, diff --git a/React.podspec b/React.podspec index 2aa8bc93ba6ba8..7cb6c1c1f8ffc5 100644 --- a/React.podspec +++ b/React.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "React" - s.version = "0.4.0" + s.version = "0.4.2" s.summary = "Build high quality mobile apps using React." s.description = <<-DESC React Native apps are built using the React JS diff --git a/React/Base/RCTAssert.h b/React/Base/RCTAssert.h index 7e73aed7d6ab33..b0a3c5c52f553f 100644 --- a/React/Base/RCTAssert.h +++ b/React/Base/RCTAssert.h @@ -9,21 +9,7 @@ #import -#ifdef __cplusplus -extern "C" { -#endif - -/** - * By default, only raise an NSAssertion in debug mode - * (custom assert functions will still be called). - */ -#ifndef RCT_ASSERT -#if DEBUG -#define RCT_ASSERT 1 -#else -#define RCT_ASSERT 0 -#endif -#endif +#import "RCTDefines.h" /** * The default error domain to be used for React errors. @@ -44,13 +30,14 @@ typedef void (^RCTAssertFunction)( /** * Private logging function - ignore this. */ -void _RCTAssertFormat(BOOL, const char *, int, const char *, NSString *, ...) NS_FORMAT_FUNCTION(5,6); +RCT_EXTERN void _RCTAssertFormat( + BOOL, const char *, int, const char *, NSString *, ...) NS_FORMAT_FUNCTION(5,6); /** * This is the main assert macro that you should use. */ #define RCTAssert(condition, ...) do { BOOL pass = ((condition) != 0); \ -if (RCT_ASSERT && !pass) { [[NSAssertionHandler currentHandler] handleFailureInFunction:@(__func__) \ +if (RCT_NSASSERT && !pass) { [[NSAssertionHandler currentHandler] handleFailureInFunction:@(__func__) \ file:@(__FILE__) lineNumber:__LINE__ description:__VA_ARGS__]; } \ _RCTAssertFormat(pass, __FILE__, __LINE__, __func__, __VA_ARGS__); \ } while (false) @@ -66,16 +53,12 @@ _RCTAssertFormat(pass, __FILE__, __LINE__, __func__, __VA_ARGS__); \ * macros. You can use these to replace the standard behavior with custom log * functionality. */ -void RCTSetAssertFunction(RCTAssertFunction assertFunction); -RCTAssertFunction RCTGetAssertFunction(void); +RCT_EXTERN void RCTSetAssertFunction(RCTAssertFunction assertFunction); +RCT_EXTERN RCTAssertFunction RCTGetAssertFunction(void); /** * This appends additional code to the existing assert function, without * replacing the existing functionality. Useful if you just want to forward * assert info to an extra service without changing the default behavior. */ -void RCTAddAssertFunction(RCTAssertFunction assertFunction); - -#ifdef __cplusplus -} -#endif +RCT_EXTERN void RCTAddAssertFunction(RCTAssertFunction assertFunction); diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index ab853851ca31e6..7c8af00cce73a3 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -10,6 +10,7 @@ #import #import "RCTBridgeModule.h" +#import "RCTDefines.h" #import "RCTFrameUpdate.h" #import "RCTInvalidating.h" #import "RCTJavaScriptExecutor.h" @@ -27,6 +28,11 @@ extern NSString *const RCTReloadNotification; */ extern NSString *const RCTJavaScriptDidLoadNotification; +/** + * This notification fires when the bridge failed to load. + */ +extern NSString *const RCTJavaScriptDidFailToLoadNotification; + /** * This block can be used to instantiate modules that require additional * init parameters, or additional configuration prior to being used. @@ -40,7 +46,7 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); /** * This function returns the module name for a given class. */ -extern NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); +RCT_EXTERN NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); /** * Async batched bridge used to communicate with the JavaScript application. @@ -62,9 +68,9 @@ extern NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); /** * This method is used to call functions in the JavaScript application context. * It is primarily intended for use by modules that require two-way communication - * with the JavaScript code. Method should be regsitered using the + * with the JavaScript code. Method should be registered using the * RCT_IMPORT_METHOD macro below. Attempting to call a method that has not been - * registered will result in an error. + * registered will result in an error. Safe to call from any thread. */ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args; @@ -80,13 +86,9 @@ __attribute__((used, section("__DATA,RCTImport"))) \ static const char *__rct_import_##module##_##method##__ = #module"."#method; /** - * This method is used to execute a new application script. It is called - * internally whenever a JS application bundle is loaded/reloaded, but should - * probably not be used at any other time. + * URL of the script that was loaded into the bridge. */ -- (void)enqueueApplicationScript:(NSString *)script - url:(NSURL *)url - onComplete:(RCTJavaScriptCompleteBlock)onComplete; +@property (nonatomic, copy) NSURL *bundleURL; @property (nonatomic, strong) Class executorClass; @@ -101,13 +103,6 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method; */ @property (nonatomic, copy, readonly) NSDictionary *modules; -/** - * The shadow queue is used to execute callbacks from the JavaScript code. All - * native hooks (e.g. exported module methods) will be executed on the shadow - * queue. - */ -@property (nonatomic, readonly) dispatch_queue_t shadowQueue; - /** * The launch options that were used to initialize the bridge. */ @@ -119,18 +114,8 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method; @property (nonatomic, readonly, getter=isLoading) BOOL loading; /** - * Reload the bundle and reset executor and modules. + * Reload the bundle and reset executor & modules. Safe to call from any thread. */ - (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 5c6bcf7cb35a5b..c98e3648b56b8b 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -22,13 +22,18 @@ #import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTProfile.h" #import "RCTRedBox.h" #import "RCTRootView.h" +#import "RCTSourceCode.h" #import "RCTSparseArray.h" #import "RCTUtils.h" NSString *const RCTReloadNotification = @"RCTReloadNotification"; NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; + +dispatch_queue_t const RCTJSThread = nil; /** * Must be kept in sync with `MessageQueue.js`. @@ -42,45 +47,6 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; -/** - * Temporarily allow to turn on and off the call batching in case someone wants - * to profile both - */ -#define BATCHED_BRIDGE 1 - -#ifdef DEBUG - -#define RCT_PROFILE_START() \ -_Pragma("clang diagnostic push") \ -_Pragma("clang diagnostic ignored \"-Wshadow\"") \ -NSTimeInterval __rct_profile_start = CACurrentMediaTime() \ -_Pragma("clang diagnostic pop") - -#define RCT_PROFILE_END(cat, args, profileName...) \ -do { \ -if (_profile) { \ - [_profileLock lock]; \ - [_profile addObject:@{ \ - @"name": [@[profileName] componentsJoinedByString: @"_"], \ - @"cat": @ #cat, \ - @"ts": @((NSUInteger)((__rct_profile_start - _startingTime) * 1e6)), \ - @"dur": @((NSUInteger)((CACurrentMediaTime() - __rct_profile_start) * 1e6)), \ - @"ph": @"X", \ - @"pid": @([[NSProcessInfo processInfo] processIdentifier]), \ - @"tid": [[NSThread currentThread] description], \ - @"args": args ?: [NSNull null], \ - }]; \ - [_profileLock unlock]; \ -} \ -} while(0) - -#else - -#define RCT_PROFILE_START(...) -#define RCT_PROFILE_END(...) - -#endif - #ifdef __LP64__ typedef uint64_t RCTHeaderValue; typedef struct section_64 RCTHeaderSection; @@ -91,6 +57,11 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { #define RCTGetSectByNameFromHeader getsectbynamefromheader #endif +#define RCTAssertJSThread() \ + RCTAssert(![NSStringFromClass([_javaScriptExecutor class]) isEqualToString:@"RCTContextExecutor"] || \ + [[[NSThread currentThread] name] isEqualToString:@"com.facebook.React.JavaScript"], \ + @"This method must be called on JS thread") + NSString *const RCTEnqueueNotification = @"RCTEnqueueNotification"; NSString *const RCTDequeueNotification = @"RCTDequeueNotification"; @@ -152,6 +123,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { * RTCBridgeModule protocol to ensure they've been exported. This scanning * functionality is disabled in release mode to improve startup performance. */ +static NSDictionary *RCTModuleIDsByName; static NSArray *RCTModuleNamesByID; static NSArray *RCTModuleClassesByID; static NSArray *RCTBridgeModuleClassesByModuleID(void) @@ -159,8 +131,9 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - RCTModuleNamesByID = [NSMutableArray array]; - RCTModuleClassesByID = [NSMutableArray array]; + RCTModuleIDsByName = [[NSMutableDictionary alloc] init]; + RCTModuleNamesByID = [[NSMutableArray alloc] init]; + RCTModuleClassesByID = [[NSMutableArray alloc] init]; Dl_info info; dladdr(&RCTBridgeModuleClassesByModuleID, &info); @@ -175,7 +148,8 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { // Get data entry NSString *entry = @(*(const char **)(mach_header + addr)); - NSArray *parts = [[entry substringWithRange:(NSRange){2, entry.length - 3}] componentsSeparatedByString:@" "]; + NSArray *parts = [[entry substringWithRange:(NSRange){2, entry.length - 3}] + componentsSeparatedByString:@" "]; // Parse class name NSString *moduleClassName = parts[0]; @@ -191,54 +165,70 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { NSStringFromClass(cls)); // Register module - [(NSMutableArray *)RCTModuleNamesByID addObject:RCTBridgeModuleNameForClass(cls)]; + NSString *moduleName = RCTBridgeModuleNameForClass(cls); + ((NSMutableDictionary *)RCTModuleIDsByName)[moduleName] = @(RCTModuleNamesByID.count); + [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; [(NSMutableArray *)RCTModuleClassesByID addObject:cls]; } } -#if DEBUG + if (RCT_DEBUG) { - // We may be able to get rid of this check in future, once people - // get used to the new registration system. That would potentially - // allow you to create modules that are not automatically registered + // We may be able to get rid of this check in future, once people + // get used to the new registration system. That would potentially + // allow you to create modules that are not automatically registered - static unsigned int classCount; - Class *classes = objc_copyClassList(&classCount); - for (unsigned int i = 0; i < classCount; i++) - { - Class cls = classes[i]; - Class superclass = cls; - while (superclass) + static unsigned int classCount; + Class *classes = objc_copyClassList(&classCount); + for (unsigned int i = 0; i < classCount; i++) { - if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule))) + Class cls = classes[i]; + Class superclass = cls; + while (superclass) { - if (![RCTModuleClassesByID containsObject:cls]) { - RCTLogError(@"Class %@ was not exported. Did you forget to use RCT_EXPORT_MODULE()?", NSStringFromClass(cls)); + if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule))) + { + if (![RCTModuleClassesByID containsObject:cls]) { + RCTLogError(@"Class %@ was not exported. Did you forget to use RCT_EXPORT_MODULE()?", NSStringFromClass(cls)); + } + break; } - break; + superclass = class_getSuperclass(superclass); } - superclass = class_getSuperclass(superclass); } + free(classes); } -#endif - }); return RCTModuleClassesByID; } +@class RCTBatchedBridge; + @interface RCTBridge () -@property (nonatomic, copy, readonly) NSArray *profile; +@property (nonatomic, strong) RCTBatchedBridge *batchedBridge; +@property (nonatomic, strong) RCTBridgeModuleProviderBlock moduleProvider; +@property (nonatomic, strong, readwrite) RCTEventDispatcher *eventDispatcher; - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method - arguments:(NSArray *)args; + arguments:(NSArray *)args + context:(NSNumber *)context; + +@end + +@interface RCTBatchedBridge : RCTBridge + +@property (nonatomic, weak) RCTBridge *parentBridge; + +- (instancetype)initWithParentBridge:(RCTBridge *)bridge; - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method - arguments:(NSArray *)args; + arguments:(NSArray *)args + context:(NSNumber *)context; @end @@ -249,6 +239,7 @@ @interface RCTModuleMethod : NSObject @property (nonatomic, copy, readonly) NSString *moduleClassName; @property (nonatomic, copy, readonly) NSString *JSMethodName; +@property (nonatomic, assign, readonly) SEL selector; @end @@ -260,10 +251,9 @@ @implementation RCTModuleMethod NSMethodSignature *_methodSignature; NSArray *_argumentBlocks; NSString *_methodName; + dispatch_block_t _methodQueue; } -static Class _globalExecutorClass; - static NSString *RCTStringUpToFirstArgument(NSString *methodName) { NSRange colonRange = [methodName rangeOfString:@":"]; @@ -320,13 +310,13 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName _isClassMethod = [reactMethodName characterAtIndex:0] == '+'; _moduleClass = NSClassFromString(_moduleClassName); -#if DEBUG + if (RCT_DEBUG) { - // Sanity check - RCTAssert([_moduleClass conformsToProtocol:@protocol(RCTBridgeModule)], - @"You are attempting to export the method %@, but %@ does not \ - conform to the RCTBridgeModule Protocol", objCMethodName, _moduleClassName); -#endif + // Sanity check + RCTAssert([_moduleClass conformsToProtocol:@protocol(RCTBridgeModule)], + @"You are attempting to export the method %@, but %@ does not \ + conform to the RCTBridgeModule Protocol", objCMethodName, _moduleClassName); + } // Get method signature _methodSignature = _isClassMethod ? @@ -338,14 +328,15 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; #define RCT_ARG_BLOCK(_logic) \ - [argumentBlocks addObject:^(RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { \ + [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { \ _logic \ [invocation setArgument:&value atIndex:index]; \ }]; \ void (^addBlockArgument)(void) = ^{ RCT_ARG_BLOCK( - if (json && ![json isKindOfClass:[NSNumber class]]) { + + if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); return; @@ -355,7 +346,8 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName __autoreleasing id value = (json ? ^(NSArray *args) { [bridge _invokeAndProcessModule:@"BatchedBridge" method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, args]]; + arguments:@[json, args] + context:context]; } : ^(NSArray *unused) {}); ) }; @@ -382,7 +374,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName #define RCT_CONVERT_CASE(_value, _type) \ case _value: { \ - _type (*convert)(id, SEL, id) = (typeof(convert))[RCTConvert methodForSelector:selector]; \ + _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ break; \ } @@ -405,9 +397,26 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName RCT_CONVERT_CASE('@', id) RCT_CONVERT_CASE('^', void *) + case '{': { + [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { + NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; + void *returnValue = malloc(methodSignature.methodReturnLength); + NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [_invocation setTarget:[RCTConvert class]]; + [_invocation setSelector:selector]; + [_invocation setArgument:&json atIndex:2]; + [_invocation invoke]; + [_invocation getReturnValue:returnValue]; + + [invocation setArgument:returnValue atIndex:index]; + + free(returnValue); + }]; + break; + } + default: defaultCase(argumentType); - break; } } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { addBlockArgument(); @@ -421,7 +430,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName #define RCT_CASE(_value, _class, _logic) \ case _value: { \ RCT_ARG_BLOCK( \ - if (json && ![json isKindOfClass:[_class class]]) { \ + if (RCT_DEBUG && json && ![json isKindOfClass:[_class class]]) { \ RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \ json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \ return; \ @@ -437,7 +446,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName #define RCT_SIMPLE_CASE(_value, _type, _selector) \ case _value: { \ RCT_ARG_BLOCK( \ - if (json && ![json respondsToSelector:@selector(_selector)]) { \ + if (RCT_DEBUG && json && ![json respondsToSelector:@selector(_selector)]) { \ RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \ index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \ return; \ @@ -461,9 +470,12 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName RCT_SIMPLE_CASE('d', double, doubleValue) RCT_SIMPLE_CASE('B', BOOL, boolValue) + case '{': + RCTLogMustFix(@"Cannot convert JSON to struct %s", argumentType); + break; + default: defaultCase(argumentType); - break; } } } @@ -477,21 +489,21 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName - (void)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments + context:(NSNumber *)context { + if (RCT_DEBUG) { -#if DEBUG - - // Sanity check - RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \ - %@ on a module of class %@", _methodName, [module class]); -#endif - - // Safety check - if (arguments.count != _argumentBlocks.count) { - RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", - RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, - arguments.count, _argumentBlocks.count); - return; + // Sanity check + RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \ + %@ on a module of class %@", _methodName, [module class]); + + // Safety check + if (arguments.count != _argumentBlocks.count) { + RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", + RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, + arguments.count, _argumentBlocks.count); + return; + } } // Create invocation (we can't re-use this as it wouldn't be thread-safe) @@ -503,8 +515,8 @@ - (void)invokeWithBridge:(RCTBridge *)bridge NSUInteger index = 0; for (id json in arguments) { id arg = (json == [NSNull null]) ? nil : json; - void (^block)(RCTBridge *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; - block(bridge, invocation, index + 2, arg); + void (^block)(RCTBridge *, NSNumber *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; + block(bridge, context, invocation, index + 2, arg); index++; } @@ -653,7 +665,6 @@ - (NSString *)description return moduleConfig; } - /** * As above, but for local modules/methods, which represent JS classes * and methods that will be called by the native code via the bridge. @@ -677,6 +688,8 @@ - (NSString *)description */ static NSMutableDictionary *RCTLocalModuleIDs; static NSMutableDictionary *RCTLocalMethodIDs; +static NSMutableArray *RCTLocalModuleNames; +static NSMutableArray *RCTLocalMethodNames; static NSDictionary *RCTLocalModulesConfig() { static NSMutableDictionary *localModules; @@ -685,6 +698,8 @@ - (NSString *)description RCTLocalModuleIDs = [[NSMutableDictionary alloc] init]; RCTLocalMethodIDs = [[NSMutableDictionary alloc] init]; + RCTLocalModuleNames = [[NSMutableArray alloc] init]; + RCTLocalMethodNames = [[NSMutableArray alloc] init]; localModules = [[NSMutableDictionary alloc] init]; for (NSString *moduleDotMethod in RCTJSMethods()) { @@ -701,6 +716,7 @@ - (NSString *)description @"methods": [[NSMutableDictionary alloc] init] }; localModules[moduleName] = module; + [RCTLocalModuleNames addObject:moduleName]; } // Add method if it doesn't already exist @@ -711,6 +727,7 @@ - (NSString *)description @"methodID": @(methods.count), @"type": @"local" }; + [RCTLocalMethodNames addObject:methodName]; } // Add module and method lookup @@ -722,123 +739,263 @@ - (NSString *)description return localModules; } -@interface RCTDisplayLink : NSObject +@interface RCTFrameUpdate (Private) -- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; @end -@interface RCTBridge (RCTDisplayLink) +@implementation RCTFrameUpdate -- (void)_update:(CADisplayLink *)displayLink; +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink +{ + if ((self = [super init])) { + _timestamp = displayLink.timestamp; + _deltaTime = displayLink.duration; + } + return self; +} @end -@implementation RCTDisplayLink -{ - __weak RCTBridge *_bridge; - CADisplayLink *_displayLink; -} +@implementation RCTBridge + +static id _latestJSExecutor; -- (instancetype)initWithBridge:(RCTBridge *)bridge +- (instancetype)initWithBundleURL:(NSURL *)bundleURL + moduleProvider:(RCTBridgeModuleProviderBlock)block + launchOptions:(NSDictionary *)launchOptions { + RCTAssertMainThread(); + if ((self = [super init])) { - _bridge = bridge; - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; - [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + /** + * Pre register modules + */ + RCTLocalModulesConfig(); + + _bundleURL = bundleURL; + _moduleProvider = block; + _launchOptions = [launchOptions copy]; + [self bindKeys]; + [self setUp]; } return self; } -- (BOOL)isValid +- (void)dealloc { - return _displayLink != nil; + /** + * This runs only on the main thread, but crashes the subclass + * RCTAssertMainThread(); + */ + [self invalidate]; } -- (void)invalidate +- (void)bindKeys { - if (self.isValid) { - [_displayLink invalidate]; - _displayLink = nil; - } + RCTAssertMainThread(); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reload) + name:RCTReloadNotification + object:nil]; + +#if TARGET_IPHONE_SIMULATOR + + __weak RCTBridge *weakSelf = self; + RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; + + // reload in current mode + [commands registerKeyCommandWithInput:@"r" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [weakSelf reload]; + }]; + +#endif + } -- (void)_update:(CADisplayLink *)displayLink +- (void)reload { - [_bridge _update:displayLink]; +/** + * AnyThread + */ + dispatch_async(dispatch_get_main_queue(), ^{ + [self invalidate]; + [self setUp]; + }); } -@end +- (void)setUp +{ + RCTAssertMainThread(); -@interface RCTFrameUpdate (Private) + _batchedBridge = [[RCTBatchedBridge alloc] initWithParentBridge:self]; +} -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; +- (BOOL)isValid +{ + return _batchedBridge.isValid; +} -@end +- (void)invalidate +{ + RCTAssertMainThread(); -@implementation RCTFrameUpdate + [_batchedBridge invalidate]; + _batchedBridge = nil; +} -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink ++ (void)logMessage:(NSString *)message level:(NSString *)level { - if ((self = [super init])) { - _timestamp = displayLink.timestamp; - _deltaTime = displayLink.duration; - } - return self; + dispatch_async(dispatch_get_main_queue(), ^{ + if (!_latestJSExecutor.isValid) { + return; + } + + [_latestJSExecutor executeJSCall:@"RCTLog" + method:@"logIfNoNativeHook" + arguments:@[level, message] + context:RCTGetExecutorID(_latestJSExecutor) + callback:^(id json, NSError *error) {}]; + }); +} + +- (NSDictionary *)modules +{ + return _batchedBridge.modules; } +- (RCTEventDispatcher *)eventDispatcher +{ + return _eventDispatcher ?: _batchedBridge.eventDispatcher; +} + +#define RCT_BRIDGE_WARN(...) \ +- (void)__VA_ARGS__ \ +{ \ + RCTLogMustFix(@"Called method \"%@\" on top level bridge. This method should \ + only be called from bridge instance in a bridge module", @(__func__)); \ +} + +RCT_BRIDGE_WARN(enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args) +RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context) + @end -@implementation RCTBridge +@implementation RCTBatchedBridge { + BOOL _loading; + id _javaScriptExecutor; RCTSparseArray *_modulesByID; + RCTSparseArray *_queuesByID; + dispatch_queue_t _methodQueue; NSDictionary *_modulesByName; - id _javaScriptExecutor; - Class _executorClass; - NSURL *_bundleURL; - RCTBridgeModuleProviderBlock _moduleProvider; - RCTDisplayLink *_displayLink; + CADisplayLink *_mainDisplayLink; + CADisplayLink *_jsDisplayLink; NSMutableSet *_frameUpdateObservers; NSMutableArray *_scheduledCalls; - NSMutableArray *_scheduledCallbacks; - BOOL _loading; - - NSUInteger _startingTime; - NSMutableArray *_profile; - NSLock *_profileLock; + RCTSparseArray *_scheduledCallbacks; } -static id _latestJSExecutor; +@synthesize valid = _valid; -- (instancetype)initWithBundleURL:(NSURL *)bundleURL - moduleProvider:(RCTBridgeModuleProviderBlock)block - launchOptions:(NSDictionary *)launchOptions +- (instancetype)initWithParentBridge:(RCTBridge *)bridge { - if ((self = [super init])) { - _bundleURL = bundleURL; - _moduleProvider = block; - _launchOptions = [launchOptions copy]; - [self setUp]; - [self bindKeys]; - } + if (self = [super init]) { + RCTAssertMainThread(); + + _parentBridge = bridge; + + /** + * Set Initial State + */ + _valid = YES; + _loading = YES; + _frameUpdateObservers = [[NSMutableSet alloc] init]; + _scheduledCalls = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; + _queuesByID = [[RCTSparseArray alloc] init]; + _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + + /** + * Initialize executor to allow enqueueing calls + */ + Class executorClass = self.executorClass ?: [RCTContextExecutor class]; + _javaScriptExecutor = RCTCreateExecutor(executorClass); + _latestJSExecutor = _javaScriptExecutor; + + /** + * Setup event dispatcher before initializing modules to allow init calls + */ + self.eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; + + /** + * Initialize and register bridge modules *before* adding the display link + * so we don't have threading issues + */ + _methodQueue = dispatch_queue_create("com.facebook.React.BridgeMethodQueue", DISPATCH_QUEUE_SERIAL); + [self registerModules]; + /** + * Start the application script + */ + [self initJS]; + } return self; } -- (void)setUp + +- (NSURL *)bundleURL +{ + return _parentBridge.bundleURL; +} + +- (NSDictionary *)launchOptions +{ + return _parentBridge.launchOptions; +} + +/** + * Override to ensure that we won't create another nested bridge + */ +- (void)setUp {} + +- (void)reload +{ + [_parentBridge reload]; +} + +- (Class)executorClass { - Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class]; - _javaScriptExecutor = [[executorClass alloc] init]; - _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]; - _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[NSMutableArray alloc] init]; + return _parentBridge.executorClass; +} + +- (void)setExecutorClass:(Class)executorClass +{ + RCTAssertMainThread(); + + _parentBridge.executorClass = executorClass; +} + +- (BOOL)isLoading +{ + return _loading; +} + +- (BOOL)isValid +{ + return _valid; +} + +- (void)registerModules +{ + RCTAssertMainThread(); // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; - for (id module in _moduleProvider ? _moduleProvider() : nil) { + for (id module in _parentBridge.moduleProvider ? _parentBridge.moduleProvider() : nil) { preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module; } @@ -848,21 +1005,29 @@ - (void)setUp [RCTBridgeModuleClassesByModuleID() enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) { NSString *moduleName = RCTModuleNamesByID[moduleID]; // Check if module instance has already been registered for this name - if ((_modulesByID[moduleID] = modulesByName[moduleName])) { + id module = modulesByName[moduleName]; + if (module) { // Preregistered instances takes precedence, no questions asked if (!preregisteredModules[moduleName]) { // It's OK to have a name collision as long as the second instance is nil RCTAssert([[moduleClass alloc] init] == nil, - @"Attempted to register RCTBridgeModule class %@ for the name '%@', \ - but name was already registered by class %@", moduleClass, + @"Attempted to register RCTBridgeModule class %@ for the name " + "'%@', but name was already registered by class %@", moduleClass, moduleName, [modulesByName[moduleName] class]); } + if ([module class] != moduleClass) { + RCTLogInfo(@"RCTBridgeModule of class %@ with name '%@' was encountered " + "in the project, but name was already registered by class %@." + "That's fine if it's intentional - just letting you know.", + moduleClass, moduleName, [modulesByName[moduleName] class]); + } } else { // Module name hasn't been used before, so go ahead and instantiate - id module = [[moduleClass alloc] init]; - if (module) { - _modulesByID[moduleID] = modulesByName[moduleName] = module; - } + module = [[moduleClass alloc] init]; + } + if (module) { + // Store module instance + _modulesByID[moduleID] = modulesByName[moduleName] = module; } }]; @@ -876,6 +1041,27 @@ - (void)setUp } } + // Get method queues + [_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, BOOL *stop) { + if ([module respondsToSelector:@selector(methodQueue)]) { + dispatch_queue_t queue = [module methodQueue]; + if (queue) { + _queuesByID[moduleID] = queue; + } else { + _queuesByID[moduleID] = [NSNull null]; + } + } + + if ([module conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { + [_frameUpdateObservers addObject:module]; + } + }]; +} + +- (void)initJS +{ + RCTAssertMainThread(); + // Inject module data into JS context NSString *configJSON = RCTJSONStringify(@{ @"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName), @@ -887,7 +1073,9 @@ - (void)setUp dispatch_semaphore_signal(semaphore); }]; - _loading = YES; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW); + + NSURL *bundleURL = _parentBridge.bundleURL; if (_javaScriptExecutor == nil) { /** @@ -896,13 +1084,22 @@ - (void)setUp */ _loading = NO; - } else if (_bundleURL) { // Allow testing without a script + } else if (bundleURL) { // Allow testing without a script RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; - [loader loadBundleAtURL:_bundleURL onComplete:^(NSError *error) { + [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { + _loading = NO; - if (error != nil) { - NSArray *stack = [[error userInfo] objectForKey:@"stack"]; + if (!self.isValid) { + return; + } + + RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = bundleURL; + sourceCodeModule.scriptText = script; + if (error) { + + NSArray *stack = [error userInfo][@"stack"]; if (stack) { [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withStack:stack]; @@ -910,71 +1107,35 @@ - (void)setUp [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withDetails:[error localizedFailureReason]]; } - } else { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification - object:self]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reload) - name:RCTReloadNotification - object:nil]; - } - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reload) - name:RCTReloadNotification - object:nil]; - }]; - } -} + NSDictionary *userInfo = @{@"error": error}; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification + object:self + userInfo:userInfo]; -- (void)bindKeys -{ + } else { -#if TARGET_IPHONE_SIMULATOR + [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { - __weak RCTBridge *weakSelf = self; - RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; + if (!loadError) { - // Workaround around the first cmd+R not working: http://openradar.appspot.com/19613391 - // You can register just the cmd key and do nothing. This will trigger the bug and cmd+R - // will work like a charm! - [commands registerKeyCommandWithInput:@"" - modifierFlags:UIKeyModifierCommand - action:NULL]; - // reload in current mode - [commands registerKeyCommandWithInput:@"r" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - [weakSelf reload]; - }]; - // reset to normal mode - [commands registerKeyCommandWithInput:@"n" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - __strong RCTBridge *strongSelf = weakSelf; - strongSelf.executorClass = Nil; - [strongSelf reload]; - }]; - // reload in debug mode - [commands registerKeyCommandWithInput:@"d" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - __strong RCTBridge *strongSelf = weakSelf; - strongSelf.executorClass = NSClassFromString(@"RCTWebSocketExecutor"); - if (!strongSelf.executorClass) { - strongSelf.executorClass = NSClassFromString(@"RCTWebViewExecutor"); - } - if (!strongSelf.executorClass) { - RCTLogError(@"WebSocket debugger is not available. " - "Did you forget to include RCTWebSocketExecutor?"); - } - [strongSelf reload]; - }]; -#endif + /** + * Register the display link to start sending js calls after everything + * is setup + */ + NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTContextExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop]; + [_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:_parentBridge + userInfo:@{ @"bridge": self }]; + } + }]; + } + }]; + } } - - (NSDictionary *)modules { RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. " @@ -983,51 +1144,65 @@ - (NSDictionary *)modules return _modulesByName; } -- (void)dealloc -{ - [self invalidate]; -} - #pragma mark - RCTInvalidating -- (BOOL)isValid -{ - return _javaScriptExecutor != nil; -} - - (void)invalidate { - if (!self.isValid && _modulesByID == nil) { - return; - } - - if (![NSThread isMainThread]) { - [self performSelectorOnMainThread:@selector(invalidate) withObject:nil waitUntilDone:YES]; + if (!self.isValid) { return; } - [[NSNotificationCenter defaultCenter] removeObserver:self]; + RCTAssertMainThread(); - // Release executor + _valid = NO; if (_latestJSExecutor == _javaScriptExecutor) { _latestJSExecutor = nil; } - [_javaScriptExecutor invalidate]; - _javaScriptExecutor = nil; - [_displayLink invalidate]; - _frameUpdateObservers = nil; + void (^mainThreadInvalidate)(void) = ^{ - // Invalidate modules - for (id target in _modulesByID.allObjects) { - if ([target respondsToSelector:@selector(invalidate)]) { - [(id)target invalidate]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_mainDisplayLink invalidate]; + _mainDisplayLink = nil; + + // Invalidate modules + for (id target in _modulesByID.allObjects) { + if ([target respondsToSelector:@selector(invalidate)]) { + [(id)target invalidate]; + } } + + // Release modules (breaks retain cycle if module has strong bridge reference) + _frameUpdateObservers = nil; + _modulesByID = nil; + _queuesByID = nil; + _modulesByName = nil; + }; + + if (!_javaScriptExecutor) { + + // No JS thread running + mainThreadInvalidate(); + return; } - // Release modules (breaks retain cycle if module has strong bridge reference) - _modulesByID = nil; - _modulesByName = nil; + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + + /** + * JS Thread deallocations + */ + [_javaScriptExecutor invalidate]; + _javaScriptExecutor = nil; + + [_jsDisplayLink invalidate]; + _jsDisplayLink = nil; + + /** + * Main Thread deallocations + */ + mainThreadInvalidate(); + + }]; } /** @@ -1040,7 +1215,7 @@ - (void)invalidate #pragma mark - RCTBridge methods /** - * Like JS::call, for objective-c. + * Public. Can be invoked from any thread. */ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args { @@ -1051,11 +1226,10 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); - if (!_loading) { - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, args ?: @[]]]; - } + [self _invokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID ?: @0, methodID ?: @0, args ?: @[]] + context:RCTGetExecutorID(_javaScriptExecutor)]; } /** @@ -1063,6 +1237,8 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args */ - (void)_immediatelyCallTimer:(NSNumber *)timer { + RCTAssertJSThread(); + NSString *moduleDotMethod = @"RCTJSTimers.callTimers"; NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod]; RCTAssert(moduleID != nil, @"Module '%@' not registered.", @@ -1071,41 +1247,49 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); - if (!_loading) { -#if BATCHED_BRIDGE + dispatch_block_t block = ^{ [self _actuallyInvokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]]]; - -#else + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + }; - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]]]; -#endif + if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { + [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; + } else { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; } } - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); - RCT_PROFILE_START(); + + RCTProfileBeginEvent(); + [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { - RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script"); + RCTAssertJSThread(); + + RCTProfileEndEvent(@"ApplicationScript", @"js_call,init", scriptLoadError); if (scriptLoadError) { onComplete(scriptLoadError); return; } - RCT_PROFILE_START(); + RCTProfileBeginEvent(); + NSNumber *context = RCTGetExecutorID(_javaScriptExecutor); [_javaScriptExecutor executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] + context:context callback:^(id json, NSError *error) { - RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue"); - RCT_PROFILE_START(); - [self _handleBuffer:json]; - RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); + RCTProfileEndEvent(@"FetchApplicationScriptCallbacks", @"js_call,init", @{ + @"json": json ?: [NSNull null], + @"error": error ?: [NSNull null], + }); + + [self _handleBuffer:json context:context]; + onComplete(error); }]; }]; @@ -1113,72 +1297,101 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete: #pragma mark - Payload Generation -- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args +- (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID { -#if BATCHED_BRIDGE - RCT_PROFILE_START(); + RCTAssertJSThread(); - if ([module isEqualToString:@"RCTEventEmitter"]) { - for (NSDictionary *call in _scheduledCalls) { - if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) { - [_scheduledCalls removeObject:call]; - } - } + id queue = nil; + if (moduleID) { + queue = _queuesByID[moduleID]; } - id call = @{ - @"module": module, - @"method": method, - @"args": args, - }; - - if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { - [_scheduledCallbacks addObject:call]; + if (queue == [NSNull null]) { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; } else { - [_scheduledCalls addObject:call]; + dispatch_async(queue ?: _methodQueue, block); } +} - RCT_PROFILE_END(js_call, args, @"schedule", module, method); +/** + * Called by enqueueJSCall from any thread, or from _immediatelyCallTimer, + * on the JS thread, but only in non-batched mode. + */ +- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context +{ + /** + * AnyThread + */ + + __weak RCTBatchedBridge *weakSelf = self; + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileBeginEvent(); + + RCTBatchedBridge *strongSelf = weakSelf; + if (!strongSelf.isValid || !strongSelf->_scheduledCallbacks || !strongSelf->_scheduledCalls) { + return; + } + + id call = @{ + @"module": module, + @"method": method, + @"args": args, + @"context": context ?: @0, + }; + + if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { + strongSelf->_scheduledCallbacks[args[0]] = call; + } else { + [strongSelf->_scheduledCalls addObject:call]; + } + + RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); + }]; } -- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args +- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { -#endif + RCTAssertJSThread(); + [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; - NSString *moduleDotMethod = [NSString stringWithFormat:@"%@.%@", module, method]; - RCT_PROFILE_START(); RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { + if (!self.isValid) { + return; + } [[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil]; - RCT_PROFILE_END(js_call, args, moduleDotMethod); - - RCT_PROFILE_START(); - [self _handleBuffer:json]; - RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); + [self _handleBuffer:json context:context]; }; [_javaScriptExecutor executeJSCall:module method:method arguments:args + context:context callback:processResponse]; } #pragma mark - Payload Processing -- (void)_handleBuffer:(id)buffer +- (void)_handleBuffer:(id)buffer context:(NSNumber *)context { + RCTAssertJSThread(); + if (buffer == nil || buffer == (id)kCFNull) { return; } + NSArray *requestsArray = [RCTConvert NSArray:buffer]; + +#if RCT_DEBUG + if (![buffer isKindOfClass:[NSArray class]]) { RCTLogError(@"Buffer must be an instance of NSArray, got %@", NSStringFromClass([buffer class])); return; } - NSArray *requestsArray = (NSArray *)buffer; NSUInteger bufferRowCount = [requestsArray count]; NSUInteger expectedFieldsCount = RCTBridgeFieldResponseReturnValues + 1; + if (bufferRowCount != expectedFieldsCount) { RCTLogError(@"Must pass all fields to buffer - expected %zd, saw %zd", expectedFieldsCount, bufferRowCount); return; @@ -1192,57 +1405,79 @@ - (void)_handleBuffer:(id)buffer } } +#endif + NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; NSUInteger numRequests = [moduleIDs count]; - BOOL allSame = numRequests == [methodIDs count] && numRequests == [paramsArrays count]; - if (!allSame) { + + if (RCT_DEBUG && (numRequests != methodIDs.count || numRequests != paramsArrays.count)) { RCTLogError(@"Invalid data message - all must be length: %zd", numRequests); return; } + // TODO: if we sort the requests by module, we could dispatch once per + // module instead of per request, which would reduce the call overhead. for (NSUInteger i = 0; i < numRequests; i++) { @autoreleasepool { [self _handleRequestNumber:i moduleID:[moduleIDs[i] integerValue] methodID:[methodIDs[i] integerValue] - params:paramsArrays[i]]; + params:paramsArrays[i] + context:context]; } } - // TODO: only used by RCTUIManager - can we eliminate this special case? - dispatch_async(self.shadowQueue, ^{ - for (id module in _modulesByID.allObjects) { - if ([module respondsToSelector:@selector(batchDidComplete)]) { + // TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case? + [_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, BOOL *stop) { + if ([module respondsToSelector:@selector(batchDidComplete)]) { + [self dispatchBlock:^{ [module batchDidComplete]; - } + } forModule:moduleID]; } - }); + }]; } - (BOOL)_handleRequestNumber:(NSUInteger)i moduleID:(NSUInteger)moduleID methodID:(NSUInteger)methodID params:(NSArray *)params + context:(NSNumber *)context { - if (![params isKindOfClass:[NSArray class]]) { + RCTAssertJSThread(); + + if (!self.isValid) { + return NO; + } + + if (RCT_DEBUG && ![params isKindOfClass:[NSArray class]]) { RCTLogError(@"Invalid module/method/params tuple for request #%zd", i); return NO; } // Look up method NSArray *methods = RCTExportedMethodsByModuleID()[moduleID]; - if (methodID >= methods.count) { + + if (RCT_DEBUG && methodID >= methods.count) { RCTLogError(@"Unknown methodID: %zd for module: %zd (%@)", methodID, moduleID, RCTModuleNamesByID[moduleID]); return NO; } + RCTModuleMethod *method = methods[methodID]; - __weak RCTBridge *weakSelf = self; - dispatch_async(self.shadowQueue, ^{ - __strong RCTBridge *strongSelf = weakSelf; + // Look up module + id module = self->_modulesByID[moduleID]; + if (RCT_DEBUG && !module) { + RCTLogError(@"No module found for name '%@'", RCTModuleNamesByID[moduleID]); + return NO; + } + + __weak RCTBatchedBridge *weakSelf = self; + [self dispatchBlock:^{ + RCTProfileBeginEvent(); + RCTBatchedBridge *strongSelf = weakSelf; if (!strongSelf.isValid) { // strongSelf has been invalidated since the dispatch_async call and this @@ -1250,118 +1485,92 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i return; } - // Look up module - id module = strongSelf->_modulesByID[moduleID]; - if (!module) { - RCTLogError(@"No module found for name '%@'", RCTModuleNamesByID[moduleID]); - return; - } - @try { - [method invokeWithBridge:strongSelf module:module arguments:params]; + [method invokeWithBridge:strongSelf module:module arguments:params context:context]; } @catch (NSException *exception) { RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); - if ([exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { - @throw; + if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + @throw exception; } } - }); + + RCTProfileEndEvent(@"Invoke callback", @"objc_call", @{ + @"module": method.moduleClassName, + @"method": method.JSMethodName, + @"selector": NSStringFromSelector(method.selector), + }); + } forModule:@(moduleID)]; return YES; } -- (void)_update:(CADisplayLink *)displayLink +- (void)_jsThreadUpdate:(CADisplayLink *)displayLink { - RCT_PROFILE_START(); + RCTAssertJSThread(); + + RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); + + RCTProfileBeginEvent(); RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; for (id observer in _frameUpdateObservers) { if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { - [observer didUpdateFrame:frameUpdate]; + [self dispatchBlock:^{ + [observer didUpdateFrame:frameUpdate]; + } forModule:RCTModuleIDsByName[RCTBridgeModuleNameForClass([observer class])]]; } } - [self _runScheduledCalls]; - - RCT_PROFILE_END(display_link, nil, @"main_thread"); -} - -- (void)_runScheduledCalls -{ -#if BATCHED_BRIDGE - - NSArray *calls = [_scheduledCallbacks arrayByAddingObjectsFromArray:_scheduledCalls]; + NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls]; + NSNumber *currentExecutorID = RCTGetExecutorID(_javaScriptExecutor); + calls = [calls filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *call, NSDictionary *bindings) { + return [call[@"context"] isEqualToNumber:currentExecutorID]; + }]]; if (calls.count > 0) { _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"processBatch" - arguments:@[calls]]; + method:@"processBatch" + arguments:@[calls] + context:RCTGetExecutorID(_javaScriptExecutor)]; } -#endif -} - -- (void)addFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers addObject:observer]; -} - -- (void)removeFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers removeObject:observer]; -} - -- (void)reload -{ - if (!_loading) { - // If the bridge has not loaded yet, the context will be already invalid at - // the time the javascript gets executed. - // It will crash the javascript, and even the next `load` won't render. - [self invalidate]; - [self setUp]; - } + RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); } -+ (void)logMessage:(NSString *)message level:(NSString *)level +- (void)_mainThreadUpdate:(CADisplayLink *)displayLink { - if (![_latestJSExecutor isValid]) { - return; - } + RCTAssertMainThread(); - // Note: the js executor could get invalidated while we're trying to call - // this...need to watch out for that. - [_latestJSExecutor executeJSCall:@"RCTLog" - method:@"logIfNoNativeHook" - arguments:@[level, message] - callback:^(id json, NSError *error) {}]; + RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); } - (void)startProfiling { - if (![_bundleURL.scheme isEqualToString:@"http"]) { + RCTAssertMainThread(); + + if (![_parentBridge.bundleURL.scheme isEqualToString:@"http"]) { RCTLogError(@"To run the profiler you must be running from the dev server"); return; } - _profileLock = [[NSLock alloc] init]; - _startingTime = CACurrentMediaTime(); - [_profileLock lock]; - _profile = [[NSMutableArray alloc] init]; - [_profileLock unlock]; + [_mainDisplayLink invalidate]; + _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; + [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + + RCTProfileInit(); } - (void)stopProfiling { - [_profileLock lock]; - NSArray *profile = _profile; - _profile = nil; - [_profileLock unlock]; - _profileLock = nil; - - NSString *log = RCTJSONStringify(profile, NULL); - NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", _bundleURL.scheme, _bundleURL.host, _bundleURL.port]; + RCTAssertMainThread(); + + [_mainDisplayLink invalidate]; + + NSString *log = RCTProfileEnd(); + NSURL *bundleURL = _parentBridge.bundleURL; + NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; NSURL *URL = [NSURL URLWithString:URLString]; NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; URLRequest.HTTPMethod = @"POST"; diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 70d5c76d092138..34b861ff3f8f10 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -17,6 +17,16 @@ */ typedef void (^RCTResponseSenderBlock)(NSArray *response); +/** + * This constant can be returned from +methodQueue to force module + * methods to be called on the JavaScript thread. This can have serious + * implications for performance, so only use this if you're sure it's what + * you need. + * + * NOTE: RCTJSThread is not a real libdispatch queue + */ +extern const dispatch_queue_t RCTJSThread; + /** * Provides the interface needed to register a bridge module. */ @@ -73,12 +83,67 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response); * { ... } */ #define RCT_REMAP_METHOD(js_name, method) \ + RCT_EXTERN_REMAP_METHOD(js_name, method) \ + - (void)method + +/** + * Use this macro in a private Objective-C implementation file to automatically + * register an external module with the bridge when it loads. This allows you to + * register Swift or private Objective-C classes with the bridge. + * + * For example if one wanted to export a Swift class to the bridge: + * + * MyModule.swift: + * + * @objc(MyModule) class MyModule: NSObject { + * + * @objc func doSomething(string: String! withFoo a: Int, bar b: Int) { ... } + * + * } + * + * MyModuleExport.m: + * + * #import "RCTBridgeModule.h" + * + * @interface RCT_EXTERN_MODULE(MyModule, NSObject) + * + * RCT_EXTERN_METHOD(doSomething:(NSString *)string withFoo:(NSInteger)a bar:(NSInteger)b) + * + * @end + * + * This will now expose MyModule and the method to JavaScript via + * `NativeModules.MyModule.doSomething` + */ +#define RCT_EXTERN_MODULE(objc_name, objc_supername) \ + RCT_EXTERN_REMAP_MODULE(, objc_name, objc_supername) + +/** + * Similar to RCT_EXTERN_MODULE but allows setting a custom JavaScript name + */ +#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \ + objc_name : objc_supername \ + @end \ + @interface objc_name (RCTExternModule) \ + @end \ + @implementation objc_name (RCTExternModule) \ + RCT_EXPORT_MODULE(js_name) + +/** + * Use this macro in accordance with RCT_EXTERN_MODULE to export methods + * of an external module. + */ +#define RCT_EXTERN_METHOD(method) \ + RCT_EXTERN_REMAP_METHOD(, method) + +/** + * Similar to RCT_EXTERN_REMAP_METHOD but allows setting a custom JavaScript name + */ +#define RCT_EXTERN_REMAP_METHOD(js_name, method) \ - (void)__rct_export__##method { \ __attribute__((used, section("__DATA,RCTExport"))) \ __attribute__((__aligned__(1))) \ static const char *__rct_export_entry__[] = { __func__, #method, #js_name }; \ - } \ - - (void)method + } /** * Deprecated, do not use. @@ -89,6 +154,37 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response); __attribute__((__aligned__(1))) \ static const char *__rct_export_entry__[] = { __func__, #js_name, NULL } +/** + * The queue that will be used to call all exported methods. If omitted, this + * will call on the default background queue, which is avoids blocking the main + * thread. + * + * If the methods in your module need to interact with UIKit methods, they will + * probably need to call those on the main thread, as most of UIKit is main- + * thread-only. You can tell React Native to call your module methods on the + * main thread by returning a reference to the main queue, like this: + * + * - (dispatch_queue_t)methodQueue + * { + * return dispatch_get_main_queue(); + * } + * + * If your methods perform heavy work such as synchronous filesystem or network + * access, you probably don't want to block the default background queue, as + * this will stall other methods. Instead, you should return a custom serial + * queue, like this: + * + * - (dispatch_queue_t)methodQueue + * { + * return dispatch_queue_create("com.mydomain.FileQueue", DISPATCH_QUEUE_SERIAL); + * } + * + * Alternatively, if only some methods of the module should be executed on a + * particular queue you can leave this method unimplemented, and simply + * dispatch_async() to the required queue within the method itself. + */ +- (dispatch_queue_t)methodQueue; + /** * Injects constants into JS. These constants are made accessible via * NativeModules.ModuleName.X. This method is called when the module is diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index e3f5e85980a0c5..dd99ac9fbedaa1 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -7,16 +7,14 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import - #import #import -#import "../Layout/Layout.h" -#import "../Views/RCTAnimationType.h" -#import "../Views/RCTPointerEvents.h" - +#import "Layout.h" +#import "RCTAnimationType.h" +#import "RCTDefines.h" #import "RCTLog.h" +#import "RCTPointerEvents.h" /** * This class provides a collection of conversion functions for mapping @@ -41,6 +39,7 @@ + (NSString *)NSString:(id)json; + (NSNumber *)NSNumber:(id)json; + (NSData *)NSData:(id)json; ++ (NSIndexSet *)NSIndexSet:(id)json; + (NSURL *)NSURL:(id)json; + (NSURLRequest *)NSURLRequest:(id)json; @@ -78,6 +77,7 @@ + (UIImage *)UIImage:(id)json; + (CGImageRef)CGImage:(id)json; ++ (UIFont *)UIFont:(id)json; + (UIFont *)UIFont:(UIFont *)font withSize:(id)json; + (UIFont *)UIFont:(UIFont *)font withWeight:(id)json; + (UIFont *)UIFont:(UIFont *)font withStyle:(id)json; @@ -103,6 +103,12 @@ typedef NSArray UIColorArray; typedef NSArray CGColorArray; + (CGColorArray *)CGColorArray:(id)json; +/** + * Convert a JSON object to a Plist-safe equivalent by stripping null values. + */ +typedef id NSPropertyList; ++ (NSPropertyList)NSPropertyList:(id)json; + typedef BOOL css_overflow; + (css_overflow)css_overflow:(id)json; + (css_flex_direction_t)css_flex_direction_t:(id)json; @@ -116,33 +122,27 @@ typedef BOOL css_overflow; @end -#ifdef __cplusplus -extern "C" { -#endif - /** * This function will attempt to set a property using a json value by first * inferring the correct type from all available information, and then * applying an appropriate conversion method. If the property does not * exist, or the type cannot be inferred, the function will return NO. */ -BOOL RCTSetProperty(id target, NSString *keyPath, SEL type, id json); +RCT_EXTERN BOOL RCTSetProperty(id target, NSString *keyPath, SEL type, id json); /** * This function attempts to copy a property from the source object to the * destination object using KVC. If the property does not exist, or cannot * be set, it will do nothing and return NO. */ -BOOL RCTCopyProperty(id target, id source, NSString *keyPath); +RCT_EXTERN BOOL RCTCopyProperty(id target, id source, NSString *keyPath); /** - * Underlying implementation of RCT_ENUM_CONVERTER macro. Ignore this. + * Underlying implementations of RCT_XXX_CONVERTER macros. Ignore these. */ -NSNumber *RCTConverterEnumValue(const char *, NSDictionary *, NSNumber *, id); - -#ifdef __cplusplus -} -#endif +RCT_EXTERN NSNumber *RCTConvertEnumValue(const char *, NSDictionary *, NSNumber *, id); +RCT_EXTERN NSArray *RCTConvertArrayValue(SEL, id); +RCT_EXTERN void RCTLogConvertError(id, const char *); /** * This macro is used for creating simple converter functions that just call @@ -157,18 +157,19 @@ RCT_CUSTOM_CONVERTER(type, name, [json getter]) #define RCT_CUSTOM_CONVERTER(type, name, code) \ + (type)name:(id)json \ { \ - if (json == [NSNull null]) { \ - json = nil; \ - } \ - @try { \ + json = (json == (id)kCFNull) ? nil : json; \ + if (!RCT_DEBUG) { \ return code; \ + } else { \ + @try { \ + return code; \ + } \ + @catch (__unused NSException *e) { \ + RCTLogConvertError(json, #type); \ + json = nil; \ + return code; \ + } \ } \ - @catch (__unused NSException *e) { \ - RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \ - json, [json classForCoder], #type); \ - json = nil; \ - return code; \ - } \ } /** @@ -190,22 +191,14 @@ RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter]) dispatch_once(&onceToken, ^{ \ mapping = values; \ }); \ - NSNumber *converted = RCTConverterEnumValue(#type, mapping, @(default), json); \ - return ((type(*)(id, SEL))objc_msgSend)(converted, @selector(getter)); \ + return [RCTConvertEnumValue(#type, mapping, @(default), json) getter]; \ } /** * This macro is used for creating converter functions for typed arrays. */ -#define RCT_ARRAY_CONVERTER(type) \ -+ (type##Array *)type##Array:(id)json \ -{ \ - NSMutableArray *values = [[NSMutableArray alloc] init]; \ - for (id jsonValue in [self NSArray:json]) { \ - id value = [self type:jsonValue]; \ - if (value) { \ - [values addObject:value]; \ - } \ - } \ - return values; \ +#define RCT_ARRAY_CONVERTER(type) \ ++ (NSArray *)type##Array:(id)json \ +{ \ + return RCTConvertArrayValue(@selector(type:), json); \ } diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 25de29656dc98a..06b5aa023bbae0 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -11,8 +11,16 @@ #import +#import "RCTDefines.h" + @implementation RCTConvert +void RCTLogConvertError(id json, const char *type) +{ + RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to %s", + json, [json classForCoder], type); +} + RCT_CONVERTER(BOOL, BOOL, boolValue) RCT_NUMBER_CONVERTER(double, doubleValue) RCT_NUMBER_CONVERTER(float, floatValue) @@ -41,11 +49,11 @@ + (NSNumber *)NSNumber:(id)json }); NSNumber *number = [formatter numberFromString:json]; if (!number) { - RCTLogError(@"JSON String '%@' could not be interpreted as a number", json); + RCTLogConvertError(json, "a number"); } return number; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]); + RCTLogConvertError(json, "a number"); } return nil; } @@ -56,28 +64,54 @@ + (NSData *)NSData:(id)json return [[self NSString:json] dataUsingEncoding:NSUTF8StringEncoding]; } ++ (NSIndexSet *)NSIndexSet:(id)json +{ + json = [self NSNumberArray:json]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + for (NSNumber *number in json) { + NSInteger index = number.integerValue; + if (RCT_DEBUG && index < 0) { + RCTLogError(@"Invalid index value %zd. Indices must be positive.", index); + } + [indexSet addIndex:index]; + } + return indexSet; +} + + (NSURL *)NSURL:(id)json { - if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json); + NSString *path = [self NSString:json]; + if (!path.length) { return nil; } - NSString *path = json; - if ([path isAbsolutePath]) - { + @try { // NSURL has a history of crashing with bad input, so let's be safe + + NSURL *URL = [NSURL URLWithString:path]; + if (URL.scheme) { // Was a well-formed absolute URL + return URL; + } + + // Check if it has a scheme + if ([path rangeOfString:@"[a-zA-Z][a-zA-Z._-]+:" options:NSRegularExpressionSearch].location == 0) { + path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + URL = [NSURL URLWithString:path]; + if (URL) { + return URL; + } + } + + // Assume that it's a local path + path = [path stringByRemovingPercentEncoding]; + if (![path isAbsolutePath]) { + path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path]; + } return [NSURL fileURLWithPath:path]; } - else if ([path length]) - { - NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]]; - if ([URL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) { - RCTLogWarn(@"The file '%@' does not exist", URL); - return nil; - } - return URL; + @catch (__unused NSException *e) { + RCTLogConvertError(json, "a valid URL"); + return nil; } - return nil; } + (NSURLRequest *)NSURLRequest:(id)json @@ -100,11 +134,12 @@ + (NSDate *)NSDate:(id)json }); NSDate *date = [formatter dateFromString:json]; if (!date) { - RCTLogError(@"JSON String '%@' could not be interpreted as a date. Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); + RCTLogError(@"JSON String '%@' could not be interpreted as a date. " + "Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); } return date; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]); + RCTLogConvertError(json, "a date"); } return nil; } @@ -115,7 +150,7 @@ + (NSDate *)NSDate:(id)json // JS standard for time zones is minutes. RCT_CUSTOM_CONVERTER(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[self double:json] * 60.0]) -NSNumber *RCTConverterEnumValue(const char *typeName, NSDictionary *mapping, NSNumber *defaultValue, id json) +NSNumber *RCTConvertEnumValue(const char *typeName, NSDictionary *mapping, NSNumber *defaultValue, id json) { if (!json || json == (id)kCFNull) { return defaultValue; @@ -224,57 +259,52 @@ + (NSDate *)NSDate:(id)json }), UIBarStyleDefault, integerValue) // TODO: normalise the use of w/width so we can do away with the alias values (#6566645) +static void RCTConvertCGStructValue(const char *type, NSArray *fields, NSDictionary *aliases, CGFloat *result, id json) +{ + NSUInteger count = fields.count; + if ([json isKindOfClass:[NSArray class]]) { + if (RCT_DEBUG && [json count] != count) { + RCTLogError(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); + } else { + for (NSUInteger i = 0; i < count; i++) { + result[i] = [RCTConvert CGFloat:json[i]]; + } + } + } else if ([json isKindOfClass:[NSDictionary class]]) { + if (aliases.count) { + json = [json mutableCopy]; + for (NSString *alias in aliases) { + NSString *key = aliases[alias]; + NSNumber *number = json[alias]; + if (number) { + RCTLogWarn(@"Using deprecated '%@' property for '%s'. Use '%@' instead.", alias, type, key); + ((NSMutableDictionary *)json)[key] = number; + } + } + } + for (NSUInteger i = 0; i < count; i++) { + result[i] = [RCTConvert CGFloat:json[fields[i]]]; + } + } else if (RCT_DEBUG && json && json != (id)kCFNull) { + RCTLogConvertError(json, type); + } +} + /** * This macro is used for creating converter functions for structs that consist * of a number of CGFloat properties, such as CGPoint, CGRect, etc. */ -#define RCT_CGSTRUCT_CONVERTER(type, values, _aliases) \ -+ (type)type:(id)json \ -{ \ - @try { \ - static NSArray *fields; \ - static NSUInteger count; \ - static dispatch_once_t onceToken; \ - dispatch_once(&onceToken, ^{ \ - fields = values; \ - count = [fields count]; \ - }); \ - type result; \ - if ([json isKindOfClass:[NSArray class]]) { \ - if ([json count] != count) { \ - RCTLogError(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); \ - } else { \ - for (NSUInteger i = 0; i < count; i++) { \ - ((CGFloat *)&result)[i] = [self CGFloat:json[i]]; \ - } \ - } \ - } else if ([json isKindOfClass:[NSDictionary class]]) { \ - NSDictionary *aliases = _aliases; \ - if (aliases.count) { \ - json = [json mutableCopy]; \ - for (NSString *alias in aliases) { \ - NSString *key = aliases[alias]; \ - NSNumber *number = json[alias]; \ - if (number) { \ - RCTLogWarn(@"Using deprecated '%@' property for '%s'. Use '%@' instead.", alias, #type, key); \ - ((NSMutableDictionary *)json)[key] = number; \ - } \ - } \ - } \ - for (NSUInteger i = 0; i < count; i++) { \ - ((CGFloat *)&result)[i] = [self CGFloat:json[fields[i]]]; \ - } \ - } else if (json && json != [NSNull null]) { \ - RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", \ - #type, [json classForCoder], json); \ - } \ - return result; \ - } \ - @catch (__unused NSException *e) { \ - RCTLogError(@"JSON value '%@' cannot be converted to '%s'", json, #type); \ - type result; \ - return result; \ - } \ +#define RCT_CGSTRUCT_CONVERTER(type, values, aliases) \ ++ (type)type:(id)json \ +{ \ + static NSArray *fields; \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + fields = values; \ + }); \ + type result; \ + RCTConvertCGStructValue(#type, fields, aliases, (CGFloat *)&result, json); \ + return result; \ } RCT_CUSTOM_CONVERTER(CGFloat, CGFloat, [self double:json]) @@ -521,9 +551,7 @@ + (UIColor *)UIColor:(id)json } else if ([json isKindOfClass:[NSArray class]]) { if ([json count] < 3 || [json count] > 4) { - RCTLogError(@"Expected array with count 3 or 4, but count is %zd: %@", [json count], json); - } else { // Color array @@ -541,10 +569,9 @@ + (UIColor *)UIColor:(id)json blue:[self double:json[@"b"]] alpha:[self double:json[@"a"] ?: @1]]; - } else if (json && ![json isKindOfClass:[NSNull class]]) { - - RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, received %@: %@", - [json classForCoder], json); + } + else if (RCT_DEBUG && json && json != (id)kCFNull) { + RCTLogConvertError(json, "a color"); } // Default color @@ -569,8 +596,12 @@ + (UIImage *)UIImage:(id)json // TODO: we might as well cache the result of these checks (and possibly the // image itself) so as to reduce overhead on subsequent checks of the same input - if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json classForCoder], json); + if (!json || json == (id)kCFNull) { + return nil; + } + + if (RCT_DEBUG && ![json isKindOfClass:[NSString class]]) { + RCTLogConvertError(json, "an image"); return nil; } @@ -662,6 +693,16 @@ static BOOL RCTFontIsCondensed(UIFont *font) return (symbolicTraits & UIFontDescriptorTraitCondensed) != 0; } ++ (UIFont *)UIFont:(id)json +{ + json = [self NSDictionary:json]; + return [self UIFont:nil + withFamily:json[@"fontFamily"] + size:json[@"fontSize"] + weight:json[@"fontWeight"] + style:json[@"fontStyle"]]; +} + + (UIFont *)UIFont:(UIFont *)font withSize:(id)json { return [self UIFont:font withFamily:nil size:json weight:nil style:nil]; @@ -690,35 +731,38 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family const RCTFontWeight RCTDefaultFontWeight = UIFontWeightRegular; const CGFloat RCTDefaultFontSize = 14; - // Get existing properties + // Initialize properties to defaults + CGFloat fontSize = RCTDefaultFontSize; + RCTFontWeight fontWeight = RCTDefaultFontWeight; + NSString *familyName = RCTDefaultFontFamily; BOOL isItalic = NO; BOOL isCondensed = NO; - RCTFontWeight fontWeight = RCTDefaultFontWeight; + if (font) { - family = font.familyName; + familyName = font.familyName ?: RCTDefaultFontFamily; + fontSize = font.pointSize ?: RCTDefaultFontSize; fontWeight = RCTWeightOfFont(font); isItalic = RCTFontIsItalic(font); isCondensed = RCTFontIsCondensed(font); } - // Get font style - if (style) { - isItalic = [self RCTFontStyle:style]; - } - // Get font size - CGFloat fontSize = [self CGFloat:size] ?: RCTDefaultFontSize; + fontSize = [self CGFloat:size] ?: fontSize; // Get font family - NSString *familyName = [self NSString:family] ?: RCTDefaultFontFamily; + familyName = [self NSString:family] ?: familyName; + + // Gracefully handle being given a font name rather than font family, for + // example: "Helvetica Light Oblique" rather than just "Helvetica". if ([UIFont fontNamesForFamilyName:familyName].count == 0) { font = [UIFont fontWithName:familyName size:fontSize]; if (font) { // It's actually a font name, not a font family name, // but we'll do what was meant, not what was said. familyName = font.familyName; - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - fontWeight = [traits[UIFontWeightTrait] doubleValue]; + fontWeight = RCTWeightOfFont(font); + isItalic = RCTFontIsItalic(font); + isCondensed = RCTFontIsCondensed(font); } else { // Not a valid font or family RCTLogError(@"Unrecognized font family '%@'", familyName); @@ -726,14 +770,20 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family } } + // Get font style + if (style) { + isItalic = [self RCTFontStyle:style]; + } + // Get font weight if (weight) { fontWeight = [self RCTFontWeight:weight]; } - // Get closest match - UIFont *bestMatch = font; - CGFloat closestWeight = font ? RCTWeightOfFont(font) : INFINITY; + // Get the closest font that matches the given weight for the fontFamily + UIFont *bestMatch = [UIFont fontWithName:font.fontName size: fontSize]; + CGFloat closestWeight = INFINITY; + for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) { UIFont *match = [UIFont fontWithName:name size:fontSize]; if (isItalic == RCTFontIsItalic(match) && @@ -757,6 +807,31 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family return bestMatch; } +NSArray *RCTConvertArrayValue(SEL type, id json) +{ + __block BOOL copy = NO; + __block NSArray *values = json = [RCTConvert NSArray:json]; + [json enumerateObjectsUsingBlock:^(id jsonValue, NSUInteger idx, BOOL *stop) { + id value = ((id(*)(Class, SEL, id))objc_msgSend)([RCTConvert class], type, jsonValue); + if (copy) { + if (value) { + [(NSMutableArray *)values addObject:value]; + } + } else if (value != jsonValue) { + // Converted value is different, so we'll need to copy the array + values = [[NSMutableArray alloc] initWithCapacity:values.count]; + for (NSInteger i = 0; i < idx; i++) { + [(NSMutableArray *)values addObject:json[i]]; + } + if (value) { + [(NSMutableArray *)values addObject:value]; + } + copy = YES; + } + }]; + return values; +} + RCT_ARRAY_CONVERTER(NSString) RCT_ARRAY_CONVERTER(NSDictionary) RCT_ARRAY_CONVERTER(NSURL) @@ -773,6 +848,58 @@ + (NSArray *)CGColorArray:(id)json return colors; } +static id RCTConvertPropertyListValue(id json) +{ + if (!json || json == (id)kCFNull) { + return nil; + } + + if ([json isKindOfClass:[NSDictionary class]]) { + __block BOOL copy = NO; + NSMutableDictionary *values = [[NSMutableDictionary alloc] initWithCapacity:[json count]]; + [json enumerateKeysAndObjectsUsingBlock:^(NSString *key, id jsonValue, BOOL *stop) { + id value = RCTConvertPropertyListValue(jsonValue); + if (value) { + values[key] = value; + } + copy |= value != jsonValue; + }]; + return copy ? values : json; + } + + if ([json isKindOfClass:[NSArray class]]) { + __block BOOL copy = NO; + __block NSArray *values = json; + [json enumerateObjectsUsingBlock:^(id jsonValue, NSUInteger idx, BOOL *stop) { + id value = RCTConvertPropertyListValue(jsonValue); + if (copy) { + if (value) { + [(NSMutableArray *)values addObject:value]; + } + } else if (value != jsonValue) { + // Converted value is different, so we'll need to copy the array + values = [[NSMutableArray alloc] initWithCapacity:values.count]; + for (NSInteger i = 0; i < idx; i++) { + [(NSMutableArray *)values addObject:json[i]]; + } + if (value) { + [(NSMutableArray *)values addObject:value]; + } + copy = YES; + } + }]; + return values; + } + + // All other JSON types are supported by property lists + return json; +} + ++ (NSPropertyList)NSPropertyList:(id)json +{ + return RCTConvertPropertyListValue(json); +} + RCT_ENUM_CONVERTER(css_overflow, (@{ @"hidden": @NO, @"visible": @YES diff --git a/React/Base/RCTDefines.h b/React/Base/RCTDefines.h new file mode 100644 index 00000000000000..71550a30d1c6c4 --- /dev/null +++ b/React/Base/RCTDefines.h @@ -0,0 +1,55 @@ +/** + * 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 + +/** + * Make global functions usable in C++ + */ +#if defined(__cplusplus) +#define RCT_EXTERN extern "C" __attribute__((visibility("default"))) +#else +#define RCT_EXTERN extern __attribute__((visibility("default"))) +#endif + +/** + * The RCT_DEBUG macro can be used to exclude error checking and logging code + * from release builds to improve performance and reduce binary size. + */ +#ifndef RCT_DEBUG +#if DEBUG +#define RCT_DEBUG 1 +#else +#define RCT_DEBUG 0 +#endif +#endif + +/** + * The RCT_DEV macro can be used to enable or disable development tools + * such as the debug executors, dev menu, red box, etc. + */ +#ifndef RCT_DEV +#if DEBUG +#define RCT_DEV 1 +#else +#define RCT_DEV 0 +#endif +#endif + +/** + * By default, only raise an NSAssertion in debug mode + * (custom assert functions will still be called). + */ +#ifndef RCT_NSASSERT +#if RCT_DEBUG +#define RCT_NSASSERT 1 +#else +#define RCT_NSASSERT 0 +#endif +#endif diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index a49e076e621743..bb80ac208d7ffc 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -9,11 +9,51 @@ #import -@class RCTBridge; +#import "RCTBridge.h" +#import "RCTBridgeModule.h" +#import "RCTInvalidating.h" +/** + * Developer menu, useful for exposing extra functionality when debugging. + */ @interface RCTDevMenu : NSObject -- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; +/** + * Is the menu enabled. The menu is enabled by default if RCT_DEV=1, but + * you may wish to disable it so that you can provide your own shake handler. + */ +@property (nonatomic, assign) BOOL shakeToShow; + +/** + * Enables performance profiling. + */ +@property (nonatomic, assign) BOOL profilingEnabled; + +/** + * Enables automatic polling for JS code changes. Only applicable when + * running the app from a server. + */ +@property (nonatomic, assign) BOOL liveReloadEnabled; + +/** + * Manually show the dev menu (can be called from JS). + */ - (void)show; +/** + * Manually reload the application. Equivalent to calling [bridge reload] + * directly, but can be called from JS. + */ +- (void)reload; + +@end + +/** + * This category makes the developer menu instance available via the + * RCTBridge, which is useful for any class that needs to access the menu. + */ +@interface RCTBridge (RCTDevMenu) + +@property (nonatomic, readonly) RCTDevMenu *devMenu; + @end diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 7621b1955f3db8..f7e688df560258 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -9,101 +9,397 @@ #import "RCTDevMenu.h" -#import "RCTRedBox.h" +#import "RCTBridge.h" +#import "RCTDefines.h" +#import "RCTKeyCommands.h" +#import "RCTLog.h" +#import "RCTProfile.h" #import "RCTRootView.h" #import "RCTSourceCode.h" -#import "RCTWebViewExecutor.h" +#import "RCTUtils.h" -@interface RCTBridge (RCTDevMenu) +#if RCT_DEV -@property (nonatomic, copy, readonly) NSArray *profile; +@interface RCTBridge (Profiling) - (void)startProfiling; - (void)stopProfiling; @end -@interface RCTDevMenu () +static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification"; +static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu"; + +@implementation UIWindow (RCTDevMenu) + +- (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event +{ + if (event.subtype == UIEventSubtypeMotionShake) { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil]; + } +} + +@end + +@interface RCTDevMenu () + +@property (nonatomic, strong) Class executorClass; @end @implementation RCTDevMenu { - BOOL _liveReload; - __weak RCTBridge *_bridge; + UIActionSheet *_actionSheet; + NSUserDefaults *_defaults; + NSMutableDictionary *_settings; + NSURLSessionDataTask *_updateTask; + NSURL *_liveReloadURL; + BOOL _jsLoaded; +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + ++ (void)initialize +{ + // We're swizzling here because it's poor form to override methods in a category, + // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's + // no need to call the original implementation. + RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:)); } -- (instancetype)initWithBridge:(RCTBridge *)bridge +- (instancetype)init { - if (self = [super init]) { - _bridge = bridge; + if ((self = [super init])) { + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter addObserver:self + selector:@selector(showOnShake) + name:RCTShowDevMenuNotification + object:nil]; + + [notificationCenter addObserver:self + selector:@selector(updateSettings) + name:NSUserDefaultsDidChangeNotification + object:nil]; + + [notificationCenter addObserver:self + selector:@selector(jsLoaded) + name:RCTJavaScriptDidLoadNotification + object:nil]; + + _defaults = [NSUserDefaults standardUserDefaults]; + _settings = [[NSMutableDictionary alloc] init]; + + // Delay setup until after Bridge init + __weak RCTDevMenu *weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateSettings]; + }); + +#if TARGET_IPHONE_SIMULATOR + + RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; + + // Toggle debug menu + [commands registerKeyCommandWithInput:@"d" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [weakSelf toggle]; + }]; + + // Reload in normal mode + [commands registerKeyCommandWithInput:@"n" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + weakSelf.executorClass = Nil; + }]; +#endif + } return self; } -- (void)show +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +- (void)updateSettings +{ + NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey]; + if ([settings isEqualToDictionary:_settings]) { + return; + } + + [_settings setDictionary:settings]; + self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; + self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; + self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + self.executorClass = NSClassFromString(_settings[@"executorClass"]); +} + +- (void)jsLoaded +{ + _jsLoaded = YES; + + // Check if live reloading is available + _liveReloadURL = nil; + RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; + if (!sourceCodeModule.scriptURL) { + if (!sourceCodeModule) { + RCTLogWarn(@"RCTSourceCode module not found"); + } else { + RCTLogWarn(@"RCTSourceCode module scriptURL has not been set"); + } + } else if (![sourceCodeModule.scriptURL isFileURL]) { + // Live reloading is disabled when running from bundled JS file + _liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + // Hit these setters again after bridge has finished loading + self.profilingEnabled = _profilingEnabled; + self.liveReloadEnabled = _liveReloadEnabled; + self.executorClass = _executorClass; + }); +} + +- (BOOL)isValid +{ + return NO; +} + +- (void)dealloc +{ + [_updateTask cancel]; + [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)updateSetting:(NSString *)name value:(id)value +{ + id currentValue = _settings[name]; + if (currentValue == value || [currentValue isEqual:value]) { + return; + } + if (value) { + _settings[name] = value; + } else { + [_settings removeObjectForKey:name]; + } + [_defaults setObject:_settings forKey:RCTDevMenuSettingsKey]; + [_defaults synchronize]; +} + +- (void)showOnShake +{ + if (_shakeToShow) { + [self show]; + } +} + +- (void)toggle +{ + if (_actionSheet) { + [_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES]; + _actionSheet = nil; + } else { + [self show]; + } +} + +RCT_EXPORT_METHOD(show) { - NSString *debugTitleChrome = _bridge.executorClass != Nil && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging"; - NSString *debugTitleSafari = _bridge.executorClass == [RCTWebViewExecutor class] ? @"Disable Safari Debugging" : @"Enable Safari Debugging"; - NSString *liveReloadTitle = _liveReload ? @"Disable Live Reload" : @"Enable Live Reload"; - NSString *profilingTitle = _bridge.profile ? @"Stop Profiling" : @"Start Profiling"; - UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development" - delegate:self - cancelButtonTitle:@"Cancel" - destructiveButtonTitle:nil - otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil]; + if (_actionSheet || !_bridge) { + return; + } + + NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome"; + NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari"; + + UIActionSheet *actionSheet = + [[UIActionSheet alloc] initWithTitle:@"React Native: Development" + delegate:self + cancelButtonTitle:nil + destructiveButtonTitle:nil + otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, nil]; + + if (_liveReloadURL) { + + NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; + NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling"; + + [actionSheet addButtonWithTitle:liveReloadTitle]; + [actionSheet addButtonWithTitle:profilingTitle]; + } + + [actionSheet addButtonWithTitle:@"Cancel"]; + actionSheet.cancelButtonIndex = [actionSheet numberOfButtons] - 1; + actionSheet.actionSheetStyle = UIBarStyleBlack; - [actionSheet showInView:[[[[UIApplication sharedApplication] keyWindow] rootViewController] view]]; + [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; + _actionSheet = actionSheet; +} + +RCT_EXPORT_METHOD(reload) +{ + _jsLoaded = NO; + _liveReloadURL = nil; + [_bridge reload]; } - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { - if (buttonIndex == 0) { - [_bridge reload]; - } else if (buttonIndex == 1) { - Class cls = NSClassFromString(@"RCTWebSocketExecutor"); - _bridge.executorClass = (_bridge.executorClass != cls) ? cls : nil; - [_bridge reload]; - } else if (buttonIndex == 2) { - Class cls = [RCTWebViewExecutor class]; - _bridge.executorClass = (_bridge.executorClass != cls) ? cls : Nil; - [_bridge reload]; - } else if (buttonIndex == 3) { - _liveReload = !_liveReload; - [self _pollAndReload]; - } else if (buttonIndex == 4) { - if (_bridge.profile) { - [_bridge stopProfiling]; - } else { - [_bridge startProfiling]; + _actionSheet = nil; + if (buttonIndex == actionSheet.cancelButtonIndex) { + return; + } + + switch (buttonIndex) { + case 0: { + [self reload]; + break; + } + case 1: { + Class cls = NSClassFromString(@"RCTWebSocketExecutor"); + self.executorClass = (_executorClass == cls) ? Nil : cls; + break; } + case 2: { + Class cls = NSClassFromString(@"RCTWebViewExecutor"); + self.executorClass = (_executorClass == cls) ? Nil : cls; + break; + } + case 3: { + self.liveReloadEnabled = !_liveReloadEnabled; + break; + } + case 4: { + self.profilingEnabled = !_profilingEnabled; + break; + } + default: + break; } } -- (void)_pollAndReload +- (void)setShakeToShow:(BOOL)shakeToShow { - if (_liveReload) { - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - NSURL *url = sourceCodeModule.scriptURL; - NSURL *longPollURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:url]; - [self performSelectorInBackground:@selector(_checkForUpdates:) withObject:longPollURL]; + if (_shakeToShow != shakeToShow) { + _shakeToShow = shakeToShow; + [self updateSetting:@"shakeToShow" value: @(_shakeToShow)]; } } -- (void)_checkForUpdates:(NSURL *)URL +- (void)setProfilingEnabled:(BOOL)enabled { - NSMutableURLRequest *longPollRequest = [NSMutableURLRequest requestWithURL:URL]; - longPollRequest.timeoutInterval = 30; - NSHTTPURLResponse *response; - [NSURLConnection sendSynchronousRequest:longPollRequest returningResponse:&response error:nil]; + if (_profilingEnabled != enabled) { + _profilingEnabled = enabled; + [self updateSetting:@"profilingEnabled" value: @(_profilingEnabled)]; + } - dispatch_async(dispatch_get_main_queue(), ^{ - if (_liveReload && response.statusCode == 205) { - [[RCTRedBox sharedInstance] dismiss]; - [_bridge reload]; + if (_liveReloadURL && enabled != RCTProfileIsProfiling()) { + if (enabled) { + [_bridge startProfiling]; + } else { + [_bridge stopProfiling]; } - [self _pollAndReload]; - }); + } +} + +- (void)setLiveReloadEnabled:(BOOL)enabled +{ + if (_liveReloadEnabled != enabled) { + _liveReloadEnabled = enabled; + [self updateSetting:@"liveReloadEnabled" value: @(_liveReloadEnabled)]; + } + + if (_liveReloadEnabled) { + [self checkForUpdates]; + } else { + [_updateTask cancel]; + _updateTask = nil; + } +} + +- (void)setExecutorClass:(Class)executorClass +{ + if (_executorClass != executorClass) { + _executorClass = executorClass; + [self updateSetting:@"executorClass" value: NSStringFromClass(executorClass)]; + } + + if (_bridge.executorClass != executorClass) { + + // TODO (6929129): we can remove this special case test once we have better + // support for custom executors in the dev menu. But right now this is + // needed to prevent overriding a custom executor with the default if a + // custom executor has been set directly on the bridge + if (executorClass == Nil && + (_bridge.executorClass != NSClassFromString(@"RCTWebSocketExecutor") && + _bridge.executorClass != NSClassFromString(@"RCTWebViewExecutor"))) { + return; + } + + _bridge.executorClass = executorClass; + [self reload]; + } +} + +- (void)checkForUpdates +{ + if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) { + return; + } + + if (_updateTask) { + [_updateTask cancel]; + _updateTask = nil; + return; + } + + __weak RCTDevMenu *weakSelf = self; + _updateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + dispatch_async(dispatch_get_main_queue(), ^{ + __strong RCTDevMenu *strongSelf = weakSelf; + if (strongSelf && strongSelf->_liveReloadEnabled) { + NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; + if (!error && HTTPResponse.statusCode == 205) { + [strongSelf reload]; + } else { + strongSelf->_updateTask = nil; + [strongSelf checkForUpdates]; + } + } + }); + + }]; + + [_updateTask resume]; +} + +@end + +#else // Unavailable when not in dev mode + +@implementation RCTDevMenu + +- (void)show {} +- (void)reload {} + +@end + +#endif + +@implementation RCTBridge (RCTDevMenu) + +- (RCTDevMenu *)devMenu +{ + return self.modules[RCTBridgeModuleNameForClass([RCTDevMenu class])]; } @end diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index dd6bd8ed6f8aa2..15cb180210f6e6 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -50,7 +50,7 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { /** * Send a user input event. The body dictionary must contain a "target" - * parameter, representing the react tag of the view sending the event + * parameter, representing the React tag of the view sending the event */ - (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body; diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index fb4e02eae30f75..8487556e576a53 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -44,7 +44,7 @@ - (void)sendDeviceEventWithName:(NSString *)name body:(id)body - (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body { RCTAssert([body[@"target"] isKindOfClass:[NSNumber class]], - @"Event body dictionary must include a 'target' property containing a react tag"); + @"Event body dictionary must include a 'target' property containing a React tag"); [_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent" args:body ? @[body[@"target"], name, body] : @[body[@"target"], name]]; diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h index 57dff78e756700..8f1eb8a98c076a 100644 --- a/React/Base/RCTJavaScriptExecutor.h +++ b/React/Base/RCTJavaScriptExecutor.h @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import #import "RCTInvalidating.h" @@ -27,6 +29,7 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error); - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments + context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete; /** @@ -39,4 +42,36 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error); - (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)objectName callback:(RCTJavaScriptCompleteBlock)onComplete; + +/** + * Enqueue a block to run in the executors JS thread. Fallback to `dispatch_async` + * on the main queue if the executor doesn't own a thread. + */ +- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block; + +@optional + +/** + * Special case for Timers + ContextExecutor - instead of the default + * if jsthread then call else dispatch call on jsthread + * ensure the call is made async on the jsthread + */ +- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block; + @end + +static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID"; +__used static id RCTCreateExecutor(Class executorClass) +{ + static NSUInteger executorID = 0; + id executor = [[executorClass alloc] init]; + if (executor) { + objc_setAssociatedObject(executor, RCTJavaScriptExecutorID, @(++executorID), OBJC_ASSOCIATION_RETAIN); + } + return executor; +} + +__used static NSNumber *RCTGetExecutorID(id executor) +{ + return executor ? objc_getAssociatedObject(executor, RCTJavaScriptExecutorID) : @0; +} diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h index bdc551b4d22194..01f51d7e99e05c 100755 --- a/React/Base/RCTJavaScriptLoader.h +++ b/React/Base/RCTJavaScriptLoader.h @@ -1,4 +1,11 @@ -// Copyright 2004-present Facebook. All Rights Reserved. +/** + * 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 @@ -15,6 +22,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; -- (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(RCTJavaScriptCompleteBlock)onComplete; +- (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(void (^)(NSError *, NSString *))onComplete; @end diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index baf2ca34455476..0210986dceab03 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -1,49 +1,24 @@ -// Copyright 2004-present Facebook. All Rights Reserved. +/** + * 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 "RCTJavaScriptLoader.h" #import "RCTBridge.h" -#import "RCTInvalidating.h" -#import "RCTLog.h" -#import "RCTRedBox.h" +#import "RCTConvert.h" #import "RCTSourceCode.h" #import "RCTUtils.h" -#define NO_REMOTE_MODULE @"Could not fetch module bundle %@. Ensure node server is running.\n\nIf it timed out, try reloading." -#define NO_LOCAL_BUNDLE @"Could not load local bundle %@. Ensure file exists." - -#define CACHE_DIR @"RCTJSBundleCache" - -#pragma mark - Application Engine - -/** - * TODO: - * - Add window resize rotation events matching the DOM API. - * - Device pixel ration hooks. - * - Source maps. - */ @implementation RCTJavaScriptLoader { __weak RCTBridge *_bridge; } -/** - * `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore - * engine in its own dedicated thread. - * - * TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one - * additional GCD dispatch per frame and likely makes it so that other UIThread - * operations don't delay the dispatch (so we can begin working in JS much - * faster.) Event handling must still be sent via a GCD dispatch, of course. - * - * We must add the display link to two runloops in order to get setTimeouts to - * fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`) - * TODO: We can invent a `requestAnimationFrame` and - * `requestAvailableAnimationFrame` to control if callbacks can be fired during - * an animation. - * http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink - * - */ - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super init])) { @@ -52,93 +27,80 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge return self; } -- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete +- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *, NSString *))onComplete { - if (scriptURL == nil) { + // Sanitize the script URL + scriptURL = [RCTConvert NSURL:scriptURL.absoluteString]; + + if (!scriptURL || + ([scriptURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) { NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{ - NSLocalizedDescriptionKey: @"No script URL provided" + NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided" }]; - onComplete(error); + onComplete(error, nil); return; } - if ([scriptURL isFileURL]) { - NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath]; - NSString *localPath = [scriptURL.absoluteString substringFromIndex:@"file://".length]; - - if (![localPath hasPrefix:bundlePath]) { - NSString *absolutePath = [NSString stringWithFormat:@"%@/%@", bundlePath, localPath]; - scriptURL = [NSURL fileURLWithPath:absolutePath]; - } - } - NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { - // Handle general request errors - if (error) { - if ([[error domain] isEqualToString:NSURLErrorDomain]) { - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: @"Could not connect to development server. Ensure node server is running - run 'npm start' from ReactKit root", - NSLocalizedFailureReasonErrorKey: [error localizedDescription], - NSUnderlyingErrorKey: error, - }; - error = [NSError errorWithDomain:@"JSServer" - code:error.code - userInfo:userInfo]; - } - onComplete(error); - return; - } - - // Parse response as text - NSStringEncoding encoding = NSUTF8StringEncoding; - if (response.textEncodingName != nil) { - CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); - if (cfEncoding != kCFStringEncodingInvalidId) { - encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); - } - } - NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; - - // Handle HTTP errors - if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { - NSDictionary *userInfo; - NSDictionary *errorDetails = RCTJSONParse(rawText, nil); - if ([errorDetails isKindOfClass:[NSDictionary class]] && - [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { - NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; - for (NSDictionary *err in errorDetails[@"errors"]) { - [fakeStack addObject: @{ - @"methodName": err[@"description"] ?: @"", - @"file": err[@"filename"] ?: @"", - @"lineNumber": err[@"lineNumber"] ?: @0 - }]; - } - userInfo = @{ - NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", - @"stack": fakeStack, - }; - } else { - userInfo = @{NSLocalizedDescriptionKey: rawText}; - } - error = [NSError errorWithDomain:@"JSServer" - code:[(NSHTTPURLResponse *)response statusCode] - userInfo:userInfo]; - - onComplete(error); - return; - } - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - sourceCodeModule.scriptURL = scriptURL; - sourceCodeModule.scriptText = rawText; + // Handle general request errors + if (error) { + if ([[error domain] isEqualToString:NSURLErrorDomain]) { + NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]]; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: desc, + NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSUnderlyingErrorKey: error, + }; + error = [NSError errorWithDomain:@"JSServer" + code:error.code + userInfo:userInfo]; + } + onComplete(error, nil); + return; + } - [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *_error) { - dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(_error); - }); - }]; - }]; + // Parse response as text + NSStringEncoding encoding = NSUTF8StringEncoding; + if (response.textEncodingName != nil) { + CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + if (cfEncoding != kCFStringEncodingInvalidId) { + encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); + } + } + NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; + + // Handle HTTP errors + if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { + NSDictionary *userInfo; + NSDictionary *errorDetails = RCTJSONParse(rawText, nil); + if ([errorDetails isKindOfClass:[NSDictionary class]] && + [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { + NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; + for (NSDictionary *err in errorDetails[@"errors"]) { + [fakeStack addObject: @{ + @"methodName": err[@"description"] ?: @"", + @"file": err[@"filename"] ?: @"", + @"lineNumber": err[@"lineNumber"] ?: @0 + }]; + } + userInfo = @{ + NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", + @"stack": fakeStack, + }; + } else { + userInfo = @{NSLocalizedDescriptionKey: rawText}; + } + error = [NSError errorWithDomain:@"JSServer" + code:[(NSHTTPURLResponse *)response statusCode] + userInfo:userInfo]; + + onComplete(error, nil); + return; + } + onComplete(nil, rawText); + }]; [task resume]; } diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 9141dd31d9348b..823acb2418659e 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -90,6 +90,17 @@ - (void)registerKeyCommandWithInput:(NSString *)input { RCTAssertMainThread(); + if (input.length && flags) { + + // Workaround around the first cmd not working: http://openradar.appspot.com/19613391 + // You can register just the cmd key and do nothing. This ensures that + // command-key modified commands will work first time. + + [self registerKeyCommandWithInput:@"" + modifierFlags:flags + action:nil]; + } + UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input modifierFlags:flags action:@selector(RCT_handleKeyCommand:)]; diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 7ffd860068d74e..75cbe722e09b43 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -10,10 +10,7 @@ #import #import "RCTAssert.h" - -#ifdef __cplusplus -extern "C" { -#endif +#import "RCTDefines.h" /** * Thresholds for logs to raise an assertion, or display redbox, respectively. @@ -45,11 +42,16 @@ typedef void (^RCTLogFunction)( NSString *message ); +/** + * Get a given thread's name (or the current queue, if in debug mode) + */ +RCT_EXTERN NSString *RCTThreadName(NSThread *); + /** * A method to generate a string from a collection of log data. To omit any * particular data from the log, just pass nil or zero for the argument. */ -NSString *RCTFormatLog( +RCT_EXTERN NSString *RCTFormatLog( NSDate *timestamp, NSThread *thread, RCTLogLevel level, @@ -68,35 +70,35 @@ extern RCTLogFunction RCTDefaultLogFunction; * below which logs will be ignored. Default is RCTLogLevelInfo for debug and * RCTLogLevelError for production. */ -void RCTSetLogThreshold(RCTLogLevel threshold); -RCTLogLevel RCTGetLogThreshold(void); +RCT_EXTERN void RCTSetLogThreshold(RCTLogLevel threshold); +RCT_EXTERN RCTLogLevel RCTGetLogThreshold(void); /** * These methods get and set the current logging function called by the RCTLogXX * macros. You can use these to replace the standard behavior with custom log * functionality. */ -void RCTSetLogFunction(RCTLogFunction logFunction); -RCTLogFunction RCTGetLogFunction(void); +RCT_EXTERN void RCTSetLogFunction(RCTLogFunction logFunction); +RCT_EXTERN RCTLogFunction RCTGetLogFunction(void); /** * This appends additional code to the existing log function, without replacing * the existing functionality. Useful if you just want to forward logs to an * extra service without changing the default behavior. */ -void RCTAddLogFunction(RCTLogFunction logFunction); +RCT_EXTERN void RCTAddLogFunction(RCTLogFunction logFunction); /** * This method adds a conditional prefix to any messages logged within the scope * of the passed block. This is useful for adding additional context to log * messages. The block will be performed synchronously on the current thread. */ -void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix); +RCT_EXTERN void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix); /** * Private logging functions - ignore these. */ -void _RCTLogFormat(RCTLogLevel, const char *, int, NSString *, ...) NS_FORMAT_FUNCTION(4,5); +RCT_EXTERN void _RCTLogFormat(RCTLogLevel, const char *, int, NSString *, ...) NS_FORMAT_FUNCTION(4,5); #define _RCTLog(lvl, ...) do { \ if (lvl >= RCTLOG_FATAL_LEVEL) { RCTAssert(NO, __VA_ARGS__); } \ _RCTLogFormat(lvl, __FILE__, __LINE__, __VA_ARGS__); \ @@ -111,7 +113,3 @@ void _RCTLogFormat(RCTLogLevel, const char *, int, NSString *, ...) NS_FORMAT_FU #define RCTLogWarn(...) _RCTLog(RCTLogLevelWarning, __VA_ARGS__) #define RCTLogError(...) _RCTLog(RCTLogLevelError, __VA_ARGS__) #define RCTLogMustFix(...) _RCTLog(RCTLogLevelMustFix, __VA_ARGS__) - -#ifdef __cplusplus -} -#endif diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index 1770a20a2a6fb0..fb70fe6d3053df 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -11,6 +11,7 @@ #import "RCTAssert.h" #import "RCTBridge.h" +#import "RCTDefines.h" #import "RCTRedBox.h" @interface RCTBridge (Logging) @@ -31,12 +32,12 @@ + (void)logMessage:(NSString *)message level:(NSString *)level; static RCTLogFunction RCTCurrentLogFunction; static RCTLogLevel RCTCurrentLogThreshold; -void RCTLogSetup(void) __attribute__((constructor)); -void RCTLogSetup() +__attribute__((constructor)) +static void RCTLogSetup() { RCTCurrentLogFunction = RCTDefaultLogFunction; -#if DEBUG +#if RCT_DEBUG RCTCurrentLogThreshold = RCTLogLevelInfo - 1; #else RCTCurrentLogThreshold = RCTLogLevelError; @@ -98,6 +99,22 @@ void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix) [prefixStack removeLastObject]; } +NSString *RCTThreadName(NSThread *thread) +{ + NSString *threadName = [thread isMainThread] ? @"main" : thread.name; + if (threadName.length == 0) { +#if DEBUG // This is DEBUG not RCT_DEBUG because it *really* must not ship in RC +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + threadName = @(dispatch_queue_get_label(dispatch_get_current_queue())); +#pragma clang diagnostic pop +#else + threadName = [NSString stringWithFormat:@"%p", thread]; +#endif + } + return threadName; +} + NSString *RCTFormatLog( NSDate *timestamp, NSThread *thread, @@ -121,18 +138,7 @@ void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix) [log appendFormat:@"[%s]", RCTLogLevels[level - 1]]; } if (thread) { - NSString *threadName = [thread isMainThread] ? @"main" : thread.name; - if (threadName.length == 0) { -#if DEBUG -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - threadName = @(dispatch_queue_get_label(dispatch_get_current_queue())); -#pragma clang diagnostic pop -#else - threadName = [NSString stringWithFormat:@"%p", thread]; -#endif - } - [log appendFormat:@"[tid:%@]", threadName]; + [log appendFormat:@"[tid:%@]", RCTThreadName(thread)]; } if (fileName) { fileName = [fileName lastPathComponent]; @@ -156,12 +162,7 @@ void _RCTLogFormat( NSString *format, ...) { -#if DEBUG - BOOL log = YES; -#else - BOOL log = (RCTCurrentLogFunction != nil); -#endif - + BOOL log = RCT_DEBUG || (RCTCurrentLogFunction != nil); if (log && level >= RCTCurrentLogThreshold) { // Get message @@ -183,15 +184,15 @@ void _RCTLogFormat( level, fileName ? @(fileName) : nil, (lineNumber >= 0) ? @(lineNumber) : nil, message ); -#if DEBUG +#if RCT_DEBUG // Red box is only available in debug mode - // Log to red box - if (level >= RCTLOG_REDBOX_LEVEL) { - [[RCTRedBox sharedInstance] showErrorMessage:message]; - } + // Log to red box + if (level >= RCTLOG_REDBOX_LEVEL) { + [[RCTRedBox sharedInstance] showErrorMessage:message]; + } - // Log to JS executor - [RCTBridge logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"]; + // Log to JS executor + [RCTBridge logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"]; #endif diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h new file mode 100644 index 00000000000000..0c254c80a15c0d --- /dev/null +++ b/React/Base/RCTProfile.h @@ -0,0 +1,105 @@ +/** + * 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 + +#import "RCTDefines.h" + +/** + * RCTProfile + * + * This file provides a set of functions and macros for performance profiling + * + * NOTE: This API is a work in a work in progress, please consider carefully + * before before using it. + */ + +#if RCT_DEV + +/** + * Returns YES if the profiling information is currently being collected + */ +RCT_EXTERN BOOL RCTProfileIsProfiling(void); + +/** + * Start collecting profiling information + */ +RCT_EXTERN void RCTProfileInit(void); + +/** + * Stop profiling and return a JSON string of the collected data - The data + * returned is compliant with google's trace event format - the format used + * as input to trace-viewer + */ +RCT_EXTERN NSString *RCTProfileEnd(void); + +/** + * Collects the initial event information for the event and returns a reference ID + */ +RCT_EXTERN NSNumber *_RCTProfileBeginEvent(void); + +/** + * The ID returned by BeginEvent should then be passed into EndEvent, with the + * rest of the event information. Just at this point the event will actually be + * registered + */ +RCT_EXTERN void _RCTProfileEndEvent(NSNumber *, NSString *, NSString *, id); + +/** + * This pair of macros implicitly handle the event ID when beginning and ending + * an event, for both simplicity and performance reasons, this method is preferred + * + * NOTE: The EndEvent call has to be either, in the same scope of BeginEvent, + * or in a sub-scope, otherwise the ID stored by BeginEvent won't be accessible + * for EndEvent, in this case you may want to use the actual C functions. + */ +#define RCTProfileBeginEvent() \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +NSNumber *__rct_profile_id = _RCTProfileBeginEvent(); \ +_Pragma("clang diagnostic pop") + +#define RCTProfileEndEvent(name, category, args...) \ +_RCTProfileEndEvent(__rct_profile_id, name, category, args) + +/** + * An event that doesn't have a duration (i.e. Notification, VSync, etc) + */ +RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString *); + +/** + * Helper to profile the duration of the execution of a block. This method uses + * self and _cmd to name this event for simplicity sake. + * + * NOTE: The block can't expect any argument + */ +#define RCTProfileBlock(block, category, arguments) \ +^{ \ + RCTProfileBeginEvent(); \ + block(); \ + RCTProfileEndEvent([NSString stringWithFormat:@"[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd)], category, arguments); \ +} + +#else + +#define RCTProfileIsProfiling(...) NO +#define RCTProfileInit(...) +#define RCTProfileEnd(...) @"" + +#define _RCTProfileBeginEvent(...) @0 +#define RCTProfileBeginEvent(...) + +#define _RCTProfileEndEvent(...) +#define RCTProfileEndEvent(...) + +#define RCTProfileImmediateEvent(...) + +#define RCTProfileBlock(block, ...) block + +#endif diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m new file mode 100644 index 00000000000000..19c6900c73abbc --- /dev/null +++ b/React/Base/RCTProfile.m @@ -0,0 +1,174 @@ +/** + * 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 "RCTProfile.h" + +#import + +#import + +#import "RCTDefines.h" +#import "RCTLog.h" +#import "RCTUtils.h" + +#if RCT_DEV + +#pragma mark - Prototypes + +NSNumber *RCTProfileTimestamp(NSTimeInterval); +NSString *RCTProfileMemory(vm_size_t); +NSDictionary *RCTProfileGetMemoryUsage(void); + +#pragma mark - Constants + +NSString const *RCTProfileTraceEvents = @"traceEvents"; +NSString const *RCTProfileSamples = @"samples"; + +#pragma mark - Variables + +NSDictionary *RCTProfileInfo; +NSUInteger RCTProfileEventID = 0; +NSMutableDictionary *RCTProfileOngoingEvents; +NSTimeInterval RCTProfileStartTime; +NSLock *_RCTProfileLock; + +#pragma mark - Macros + +#define RCTProfileAddEvent(type, props...) \ +[RCTProfileInfo[type] addObject:@{ \ + @"pid": @([[NSProcessInfo processInfo] processIdentifier]), \ + @"tid": RCTThreadName([NSThread currentThread]), \ + props \ +}]; + +#define CHECK(...) \ +if (!RCTProfileIsProfiling()) { \ + return __VA_ARGS__; \ +} + +#define RCTProfileLock(...) \ +[_RCTProfileLock lock]; \ +__VA_ARGS__ \ +[_RCTProfileLock unlock] + +#pragma mark - Private Helpers + +NSNumber *RCTProfileTimestamp(NSTimeInterval timestamp) +{ + return @((timestamp - RCTProfileStartTime) * 1e6); +} + +NSString *RCTProfileMemory(vm_size_t memory) +{ + double mem = ((double)memory) / 1024 / 1024; + return [NSString stringWithFormat:@"%.2lfmb", mem]; +} + +NSDictionary *RCTProfileGetMemoryUsage(void) +{ + struct task_basic_info info; + mach_msg_type_number_t size = sizeof(info); + kern_return_t kerr = task_info(mach_task_self(), + TASK_BASIC_INFO, + (task_info_t)&info, + &size); + if( kerr == KERN_SUCCESS ) { + return @{ + @"suspend_count": @(info.suspend_count), + @"virtual_size": RCTProfileMemory(info.virtual_size), + @"resident_size": RCTProfileMemory(info.resident_size), + }; + } else { + return @{}; + } +} + +#pragma mark - Public Functions + +BOOL RCTProfileIsProfiling(void) +{ + RCTProfileLock( + BOOL profiling = RCTProfileInfo != nil; + ); + return profiling; +} + +void RCTProfileInit(void) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _RCTProfileLock = [[NSLock alloc] init]; + }); + RCTProfileLock( + RCTProfileStartTime = CACurrentMediaTime(); + RCTProfileOngoingEvents = [[NSMutableDictionary alloc] init]; + RCTProfileInfo = @{ + RCTProfileTraceEvents: [[NSMutableArray alloc] init], + RCTProfileSamples: [[NSMutableArray alloc] init], + }; + ); +} + +NSString *RCTProfileEnd(void) +{ + RCTProfileLock( + NSString *log = RCTJSONStringify(RCTProfileInfo, NULL); + RCTProfileEventID = 0; + RCTProfileInfo = nil; + RCTProfileOngoingEvents = nil; + ); + return log; +} + +NSNumber *_RCTProfileBeginEvent(void) +{ + CHECK(@0); + RCTProfileLock( + NSNumber *eventID = @(++RCTProfileEventID); + RCTProfileOngoingEvents[eventID] = RCTProfileTimestamp(CACurrentMediaTime()); + ); + return eventID; +} + +void _RCTProfileEndEvent(NSNumber *eventID, NSString *name, NSString *categories, id args) +{ + CHECK(); + RCTProfileLock( + NSNumber *startTimestamp = RCTProfileOngoingEvents[eventID]; + if (startTimestamp) { + NSNumber *endTimestamp = RCTProfileTimestamp(CACurrentMediaTime()); + + RCTProfileAddEvent(RCTProfileTraceEvents, + @"name": name, + @"cat": categories, + @"ph": @"X", + @"ts": startTimestamp, + @"dur": @(endTimestamp.doubleValue - startTimestamp.doubleValue), + @"args": args ?: @[], + ); + [RCTProfileOngoingEvents removeObjectForKey:eventID]; + } + ); +} + +void RCTProfileImmediateEvent(NSString *name, NSTimeInterval timestamp, NSString *scope) +{ + CHECK(); + RCTProfileLock( + RCTProfileAddEvent(RCTProfileTraceEvents, + @"name": name, + @"ts": RCTProfileTimestamp(timestamp), + @"scope": scope, + @"ph": @"i", + @"args": RCTProfileGetMemoryUsage(), + ); + ); +} + +#endif diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index 3bed3150578c6a..9c5b6d3dd3bf0e 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -10,8 +10,11 @@ #import "RCTRedBox.h" #import "RCTBridge.h" +#import "RCTDefines.h" #import "RCTUtils.h" +#if RCT_DEBUG + @interface RCTRedBoxWindow : UIWindow @property (nonatomic, copy) NSString *lastErrorMessage; @@ -73,10 +76,27 @@ - (id)initWithFrame:(CGRect)frame reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); [_rootView addSubview:dismissButton]; [_rootView addSubview:reloadButton]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter addObserver:self + selector:@selector(dismiss) + name:RCTReloadNotification + object:nil]; + + [notificationCenter addObserver:self + selector:@selector(dismiss) + name:RCTJavaScriptDidLoadNotification + object:nil]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void)openStackFrameInEditor:(NSDictionary *)stackFrame { NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, nil) dataUsingEncoding:NSUTF8StringEncoding]; @@ -88,7 +108,7 @@ - (void)openStackFrameInEditor:(NSDictionary *)stackFrame [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - [NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:nil]; + [[[NSURLSession sharedSession] dataTaskWithRequest:request] resume]; } - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow @@ -122,7 +142,6 @@ - (void)dismiss - (void)reload { [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil]; - [self dismiss]; } #pragma mark - TableView @@ -282,23 +301,12 @@ - (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow { - -#if DEBUG - - dispatch_block_t block = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ if (!_window) { _window = [[RCTRedBoxWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; } [_window showErrorMessage:message withStack:stack showIfHidden:shouldShow]; - }; - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_async(dispatch_get_main_queue(), block); - } - -#endif - + }); } - (NSString *)currentErrorMessage @@ -316,3 +324,20 @@ - (void)dismiss } @end + +#else // Disabled + +@implementation RCTRedBox + ++ (instancetype)sharedInstance { return nil; } +- (void)showErrorMessage:(NSString *)message {} +- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {} +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack {} +- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack {} +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow {} +- (NSString *)currentErrorMessage { return nil; } +- (void)dismiss {} + +@end + +#endif diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index 1227eba9473ba5..d55094c3727328 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -11,7 +11,7 @@ #import "RCTBridge.h" -@interface RCTRootView : UIView +@interface RCTRootView : UIView /** * - Designated initializer - @@ -57,12 +57,6 @@ */ @property (nonatomic, strong) Class executorClass; -/** - * If YES will watch for shake gestures and show development menu - * with options like "Reload", "Enable Debugging", etc. - */ -@property (nonatomic, assign) BOOL enableDevMenu; - /** * The backing view controller of the root view. */ diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index c9a97dfd134e6a..9ee09d495ee447 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -13,7 +13,6 @@ #import "RCTBridge.h" #import "RCTContextExecutor.h" -#import "RCTDevMenu.h" #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" @@ -21,16 +20,13 @@ #import "RCTTouchHandler.h" #import "RCTUIManager.h" #import "RCTUtils.h" +#import "RCTView.h" #import "RCTWebViewExecutor.h" #import "UIView+React.h" -/** - * HACK(t6568049) This should be removed soon, hiding to prevent people from - * relying on it - */ @interface RCTBridge (RCTRootView) -- (void)setJavaScriptExecutor:(id)executor; +@property (nonatomic, weak, readonly) RCTBridge *batchedBridge; @end @@ -40,17 +36,21 @@ - (NSNumber *)allocateRootTag; @end +@interface RCTRootContentView : RCTView + +- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge; + +@end + @implementation RCTRootView { - RCTDevMenu *_devMenu; RCTBridge *_bridge; - RCTTouchHandler *_touchHandler; NSString *_moduleName; NSDictionary *_launchOptions; - UIView *_contentView; + RCTRootContentView *_contentView; } -- (instancetype)initWithBridge:(RCTBridge *)bridge + - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName { RCTAssert(bridge, @"A bridge instance is required to create an RCTRootView"); @@ -60,21 +60,15 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge self.backgroundColor = [UIColor whiteColor]; -#ifdef DEBUG - - _enableDevMenu = YES; - -#endif - _bridge = bridge; _moduleName = moduleName; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(bundleFinishedLoading) + selector:@selector(javaScriptDidLoad:) name:RCTJavaScriptDidLoadNotification object:_bridge]; - if (!_bridge.loading) { - [self bundleFinishedLoading]; + if (!_bridge.batchedBridge.isLoading) { + [self bundleFinishedLoading:_bridge.batchedBridge]; } } return self; @@ -91,25 +85,6 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL return [self initWithBridge:bridge moduleName:moduleName]; } -- (BOOL)isValid -{ - return _contentView.userInteractionEnabled; -} - -- (void)invalidate -{ - _contentView.userInteractionEnabled = NO; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - if (_contentView) { - [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" - args:@[_contentView.reactTag]]; - } -} - - (UIViewController *)backingViewController { return _backingViewController ?: [super backingViewController]; @@ -120,46 +95,41 @@ - (BOOL)canBecomeFirstResponder return YES; } -- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event -{ - if (motion == UIEventSubtypeMotionShake && self.enableDevMenu) { - if (!_devMenu) { - _devMenu = [[RCTDevMenu alloc] initWithBridge:_bridge]; - } - [_devMenu show]; - } else { - [super motionEnded:motion withEvent:event]; - } -} - RCT_IMPORT_METHOD(AppRegistry, runApplication) RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) -- (void)bundleFinishedLoading + +- (void)javaScriptDidLoad:(NSNotification *)notification +{ + RCTBridge *bridge = notification.userInfo[@"bridge"]; + [self bundleFinishedLoading:bridge]; +} + +- (void)bundleFinishedLoading:(RCTBridge *)bridge { dispatch_async(dispatch_get_main_queue(), ^{ + if (!bridge.isValid) { + return; + } /** - * Every root view that is created must have a unique react tag. + * Every root view that is created must have a unique React tag. * Numbering of these tags goes from 1, 11, 21, 31, etc * * NOTE: Since the bridge persists, the RootViews might be reused, so now - * the react tag is assigned every time we load new content. + * the React tag is assigned every time we load new content. */ [_contentView removeFromSuperview]; - _contentView = [[UIView alloc] initWithFrame:self.bounds]; - _contentView.reactTag = [_bridge.uiManager allocateRootTag]; - _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; - [_contentView addGestureRecognizer:_touchHandler]; + _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds + bridge:bridge]; [self addSubview:_contentView]; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": _contentView.reactTag, - @"initialProps": self.initialProperties ?: @{}, + @"initialProps": _initialProperties ?: @{}, }; - [_bridge.uiManager registerRootView:_contentView]; - [_bridge enqueueJSCall:@"AppRegistry.runApplication" + [bridge enqueueJSCall:@"AppRegistry.runApplication" args:@[moduleName, appParameters]]; }); } @@ -169,7 +139,6 @@ - (void)layoutSubviews [super layoutSubviews]; if (_contentView) { _contentView.frame = self.bounds; - [_bridge.uiManager setFrame:self.frame forRootView:_contentView]; } } @@ -178,6 +147,12 @@ - (NSNumber *)reactTag return _contentView.reactTag; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_contentView removeFromSuperview]; +} + @end @implementation RCTUIManager (RCTRootView) @@ -190,3 +165,60 @@ - (NSNumber *)allocateRootTag } @end + +@implementation RCTRootContentView +{ + __weak RCTBridge *_bridge; + RCTTouchHandler *_touchHandler; +} + +- (instancetype)initWithFrame:(CGRect)frame + bridge:(RCTBridge *)bridge +{ + if ((self = [super init])) { + _bridge = bridge; + [self setUp]; + self.frame = frame; + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + if (self.reactTag && _bridge.isValid) { + [_bridge.uiManager setFrame:self.bounds forRootView:self]; + } +} + +- (void)setUp +{ + /** + * Every root view that is created must have a unique react tag. + * Numbering of these tags goes from 1, 11, 21, 31, etc + * + * NOTE: Since the bridge persists, the RootViews might be reused, so now + * the react tag is assigned every time we load new content. + */ + self.reactTag = [_bridge.uiManager allocateRootTag]; + [self addGestureRecognizer:[[RCTTouchHandler alloc] initWithBridge:_bridge]]; + [_bridge.uiManager registerRootView:self]; +} + +- (BOOL)isValid +{ + return self.userInteractionEnabled; +} + +- (void)invalidate +{ + self.userInteractionEnabled = NO; +} + +- (void)dealloc +{ + [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" + args:@[self.reactTag]]; +} + +@end diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index bd731a99844715..2af5c428c3de74 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -26,7 +26,7 @@ @implementation RCTTouchHandler /** * Arrays managed in parallel tracking native touch object along with the - * native view that was touched, and the react touch data dictionary. + * native view that was touched, and the React touch data dictionary. * This must be kept track of because `UIKit` destroys the touch targets * if touches are canceled and we have no other way to recover this information. */ @@ -55,8 +55,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _pendingTouches = [[NSMutableArray alloc] init]; _bridgeInteractionTiming = [[NSMutableArray alloc] init]; - // `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower - // level components not build using RCT, will fail to recognize gestures. + // `cancelsTouchesInView` is needed in order to be used as a top level + // event delegated recognizer. Otherwise, lower-level components not built + // using RCT, will fail to recognize gestures. self.cancelsTouchesInView = NO; } return self; @@ -165,7 +166,9 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex * (start/end/move/cancel) and the indices that represent "changed" `Touch`es * from that array. */ -- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime +- (void)_updateAndDispatchTouches:(NSSet *)touches + eventName:(NSString *)eventName + originatingTime:(CFTimeInterval)originatingTime { // Update touches NSMutableArray *changedIndexes = [[NSMutableArray alloc] init]; @@ -196,15 +199,39 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventNa #pragma mark - Gesture Recognizer Delegate Callbacks +static BOOL RCTAllTouchesAreCancelldOrEnded(NSSet *touches) +{ + for (UITouch *touch in touches) { + if (touch.phase == UITouchPhaseBegan || + touch.phase == UITouchPhaseMoved || + touch.phase == UITouchPhaseStationary) { + return NO; + } + } + return YES; +} + +static BOOL RCTAnyTouchesChanged(NSSet *touches) +{ + for (UITouch *touch in touches) { + if (touch.phase == UITouchPhaseBegan || + touch.phase == UITouchPhaseMoved) { + return YES; + } + } + return NO; +} + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; - self.state = UIGestureRecognizerStateBegan; // "start" has to record new touches before extracting the event. // "end"/"cancel" needs to remove the touch *after* extracting the event. [self _recordNewTouches:touches]; [self _updateAndDispatchTouches:touches eventName:@"topTouchStart" originatingTime:event.timestamp]; + + self.state = UIGestureRecognizerStateBegan; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event @@ -213,7 +240,12 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event if (self.state == UIGestureRecognizerStateFailed) { return; } + [self _updateAndDispatchTouches:touches eventName:@"topTouchMove" originatingTime:event.timestamp]; + + if (self.state == UIGestureRecognizerStateBegan) { + self.state = UIGestureRecognizerStateChanged; + } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event @@ -221,6 +253,12 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event [super touchesEnded:touches withEvent:event]; [self _updateAndDispatchTouches:touches eventName:@"topTouchEnd" originatingTime:event.timestamp]; [self _recordRemovedTouches:touches]; + + if (RCTAllTouchesAreCancelldOrEnded(event.allTouches)) { + self.state = UIGestureRecognizerStateEnded; + } else if (RCTAnyTouchesChanged(event.allTouches)) { + self.state = UIGestureRecognizerStateChanged; + } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event @@ -228,6 +266,12 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event [super touchesCancelled:touches withEvent:event]; [self _updateAndDispatchTouches:touches eventName:@"topTouchCancel" originatingTime:event.timestamp]; [self _recordRemovedTouches:touches]; + + if (RCTAllTouchesAreCancelldOrEnded(event.allTouches)) { + self.state = UIGestureRecognizerStateCancelled; + } else if (RCTAnyTouchesChanged(event.allTouches)) { + self.state = UIGestureRecognizerStateChanged; + } } - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index d20ba8a5faf47e..6c4b91464e6390 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -13,43 +13,42 @@ #import #import "RCTAssert.h" - -#ifdef __cplusplus -extern "C" { -#endif +#import "RCTDefines.h" // Utility functions for JSON object <-> string serialization/deserialization -NSString *RCTJSONStringify(id jsonObject, NSError **error); -id RCTJSONParse(NSString *jsonString, NSError **error); +RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error); +RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error); + +// Strip non JSON-safe values from an object graph +RCT_EXTERN id RCTJSONClean(id object); // Get MD5 hash of a string (TODO: currently unused. Remove?) -NSString *RCTMD5Hash(NSString *string); +RCT_EXTERN NSString *RCTMD5Hash(NSString *string); // Get screen metrics in a thread-safe way -CGFloat RCTScreenScale(void); -CGSize RCTScreenSize(void); +RCT_EXTERN CGFloat RCTScreenScale(void); +RCT_EXTERN CGSize RCTScreenSize(void); // Round float coordinates to nearest whole screen pixel (not point) -CGFloat RCTRoundPixelValue(CGFloat value); -CGFloat RCTCeilPixelValue(CGFloat value); -CGFloat RCTFloorPixelValue(CGFloat value); +RCT_EXTERN CGFloat RCTRoundPixelValue(CGFloat value); +RCT_EXTERN CGFloat RCTCeilPixelValue(CGFloat value); +RCT_EXTERN CGFloat RCTFloorPixelValue(CGFloat value); // Get current time, for precise performance metrics -NSTimeInterval RCTTGetAbsoluteTime(void); +RCT_EXTERN NSTimeInterval RCTTGetAbsoluteTime(void); // Method swizzling -void RCTSwapClassMethods(Class cls, SEL original, SEL replacement); -void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement); +RCT_EXTERN void RCTSwapClassMethods(Class cls, SEL original, SEL replacement); +RCT_EXTERN void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement); // Module subclass support -BOOL RCTClassOverridesClassMethod(Class cls, SEL selector); -BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector); +RCT_EXTERN BOOL RCTClassOverridesClassMethod(Class cls, SEL selector); +RCT_EXTERN BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector); // Creates a standardized error object // TODO(#6472857): create NSErrors and automatically convert them over the bridge. -NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData); -NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData); +RCT_EXTERN NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData); +RCT_EXTERN NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData); -#ifdef __cplusplus -} -#endif +// Returns YES if React is running in a test environment +RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index cea45c324c63e7..ff11b57640074b 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -20,7 +20,7 @@ NSString *RCTJSONStringify(id jsonObject, NSError **error) { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:error]; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:(NSJSONWritingOptions)NSJSONReadingAllowFragments error:error]; return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil; } @@ -35,14 +35,64 @@ id RCTJSONParse(NSString *jsonString, NSError **error) if (jsonData) { RCTLogWarn(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString); } else { - // If our backup conversion fails, log the issue so we can see what strings are causing this (t6452813) - RCTLogError(@"RCTJSONParse received the following string, which could not be converted to UTF8 data: '%@'", jsonString); + RCTLogError(@"RCTJSONParse received invalid UTF8 data"); return nil; } } return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error]; } +id RCTJSONClean(id object) +{ + static dispatch_once_t onceToken; + static NSSet *validLeafTypes; + dispatch_once(&onceToken, ^{ + validLeafTypes = [[NSSet alloc] initWithArray:@[ + [NSString class], + [NSMutableString class], + [NSNumber class], + [NSNull class], + ]]; + }); + + if ([validLeafTypes containsObject:[object classForCoder]]) { + return object; + } + + if ([object isKindOfClass:[NSDictionary class]]) { + __block BOOL copy = NO; + NSMutableDictionary *values = [[NSMutableDictionary alloc] initWithCapacity:[object count]]; + [object enumerateKeysAndObjectsUsingBlock:^(NSString *key, id item, BOOL *stop) { + id value = RCTJSONClean(item); + values[key] = value; + copy |= value != item; + }]; + return copy ? values : object; + } + + if ([object isKindOfClass:[NSArray class]]) { + __block BOOL copy = NO; + __block NSArray *values = object; + [object enumerateObjectsUsingBlock:^(id item, NSUInteger idx, BOOL *stop) { + id value = RCTJSONClean(item); + if (copy) { + [(NSMutableArray *)values addObject:value]; + } else if (value != item) { + // Converted value is different, so we'll need to copy the array + values = [[NSMutableArray alloc] initWithCapacity:values.count]; + for (NSInteger i = 0; i < idx; i++) { + [(NSMutableArray *)values addObject:object[i]]; + } + [(NSMutableArray *)values addObject:value]; + copy = YES; + } + }]; + return values; + } + + return (id)kCFNull; +} + NSString *RCTMD5Hash(NSString *string) { const char *str = [string UTF8String]; @@ -201,3 +251,13 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector) RCTLogError(@"\nError: %@", error); return error; } + +BOOL RCTRunningInTestEnvironment(void) +{ + static BOOL _isTestEnvironment = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _isTestEnvironment = (NSClassFromString(@"SenTestCase") != nil || NSClassFromString(@"XCTest") != nil); + }); + return _isTestEnvironment; +} diff --git a/React/Executors/RCTContextExecutor.h b/React/Executors/RCTContextExecutor.h index 6e62d87b6db8c2..a41fcf31419a43 100644 --- a/React/Executors/RCTContextExecutor.h +++ b/React/Executors/RCTContextExecutor.h @@ -9,7 +9,7 @@ #import -#import "../Base/RCTJavaScriptExecutor.h" +#import "RCTJavaScriptExecutor.h" // TODO (#5906496): Might RCTJSCoreExecutor be a better name for this? @@ -23,6 +23,6 @@ * You probably don't want to use this; use -init instead. */ - (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread - globalContextRef:(JSGlobalContextRef)context; + globalContextRef:(JSGlobalContextRef)context NS_DESIGNATED_INITIALIZER; @end diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 8475e2afa311f2..412ffd25693c36 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -14,7 +14,9 @@ #import #import "RCTAssert.h" +#import "RCTDefines.h" #import "RCTLog.h" +#import "RCTProfile.h" #import "RCTUtils.h" @interface RCTJavaScriptContext : NSObject @@ -46,9 +48,16 @@ - (BOOL)isValid - (void)invalidate { - JSGlobalContextRelease(_ctx); - _ctx = NULL; - _self = nil; + if (self.isValid) { + JSGlobalContextRelease(_ctx); + _ctx = NULL; + _self = nil; + } +} + +- (void)dealloc +{ + CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]); } @end @@ -70,12 +79,12 @@ @implementation RCTContextExecutor static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) { if (argumentCount > 0) { - JSStringRef string = JSValueToStringCopy(context, arguments[0], exception); - if (!string) { + JSStringRef messageRef = JSValueToStringCopy(context, arguments[0], exception); + if (!messageRef) { return JSValueMakeUndefined(context); } - NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string); - JSStringRelease(string); + NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, messageRef); + JSStringRelease(messageRef); NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)" options:NSRegularExpressionCaseInsensitive @@ -85,7 +94,11 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, range:(NSRange){0, message.length} withTemplate:@"[$4$5] \t$2"]; - _RCTLogFormat(RCTLogLevelInfo, NULL, -1, @"%@", message); + RCTLogLevel level = RCTLogLevelInfo; + if (argumentCount > 1) { + level = MAX(level, JSValueToNumber(context, arguments[1], exception) - 1); + } + RCTGetLogFunction()(level, nil, nil, message); } return JSValueMakeUndefined(context); @@ -126,8 +139,6 @@ static JSValueRef RCTNoop(JSContextRef context, JSObjectRef object, JSObjectRef + (void)runRunLoopThread { - // TODO (#5906496): Investigate exactly what this does and why - @autoreleasepool { // copy thread name to pthread name pthread_setname_np([[[NSThread currentThread] name] UTF8String]); @@ -147,15 +158,12 @@ + (void)runRunLoopThread - (instancetype)init { - static NSThread *javaScriptThread; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - // All JS is single threaded, so a serial queue is our only option. - javaScriptThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoopThread) object:nil]; - [javaScriptThread setName:@"com.facebook.React.JavaScript"]; - [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]]; - [javaScriptThread start]; - }); + NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class] + selector:@selector(runRunLoopThread) + object:nil]; + [javaScriptThread setName:@"com.facebook.React.JavaScript"]; + [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]]; + [javaScriptThread start]; return [self initWithJavaScriptThread:javaScriptThread globalContextRef:NULL]; } @@ -163,6 +171,9 @@ - (instancetype)init - (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread globalContextRef:(JSGlobalContextRef)context { + RCTAssert(javaScriptThread != nil, + @"Can't initialize RCTContextExecutor without a javaScriptThread"); + if ((self = [super init])) { _javaScriptThread = javaScriptThread; __weak RCTContextExecutor *weakSelf = self; @@ -210,10 +221,7 @@ - (BOOL)isValid - (void)invalidate { - if (self.isValid) { - [_context performSelector:@selector(invalidate) onThread:_javaScriptThread withObject:nil waitUntilDone:NO]; - _context = nil; - } + [_context performSelector:@selector(invalidate) onThread:_javaScriptThread withObject:nil waitUntilDone:NO]; } - (void)dealloc @@ -224,13 +232,14 @@ - (void)dealloc - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments + context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete { RCTAssert(onComplete != nil, @"onComplete block should not be nil"); __weak RCTContextExecutor *weakSelf = self; - [self executeBlockOnJavaScriptQueue:^{ + [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ RCTContextExecutor *strongSelf = weakSelf; - if (!strongSelf || !strongSelf.isValid) { + if (!strongSelf || !strongSelf.isValid || ![RCTGetExecutorID(strongSelf) isEqualToNumber:executorID]) { return; } NSError *error; @@ -269,58 +278,71 @@ - (void)executeJSCall:(NSString *)name } onComplete(objcValue, nil); - }]; + }), @"js_call", (@{@"module":name, @"method": method, @"args": arguments}))]; } - (void)executeApplicationScript:(NSString *)script - sourceURL:(NSURL *)url + sourceURL:(NSURL *)sourceURL onComplete:(RCTJavaScriptCompleteBlock)onComplete { - RCTAssert(url != nil, @"url should not be nil"); - RCTAssert(onComplete != nil, @"onComplete block should not be nil"); + RCTAssert(sourceURL != nil, @"url should not be nil"); + __weak RCTContextExecutor *weakSelf = self; - [self executeBlockOnJavaScriptQueue:^{ + [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ RCTContextExecutor *strongSelf = weakSelf; if (!strongSelf || !strongSelf.isValid) { return; } JSValueRef jsError = NULL; JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); - JSStringRef sourceURL = JSStringCreateWithCFString((__bridge CFStringRef)url.absoluteString); - JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, sourceURL, 0, &jsError); - JSStringRelease(sourceURL); + JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); + JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError); + JSStringRelease(jsURL); JSStringRelease(execJSString); - NSError *error; - if (!result) { - error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError); + if (onComplete) { + NSError *error; + if (!result) { + error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError); + } + onComplete(error); } - - onComplete(error); - }]; + }), @"js_call", (@{ @"url": sourceURL.absoluteString }))]; } - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block { - if ([NSThread currentThread] != _javaScriptThread) { - [self performSelector:@selector(executeBlockOnJavaScriptQueue:) - onThread:_javaScriptThread withObject:block waitUntilDone:NO]; - } else { - block(); - } + if ([NSThread currentThread] != _javaScriptThread) { + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread withObject:block waitUntilDone:NO]; + } else { + block(); + } +} + +- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block +{ + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread + withObject:block + waitUntilDone:NO]; +} + +- (void)_runBlock:(dispatch_block_t)block +{ + block(); } - (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)objectName callback:(RCTJavaScriptCompleteBlock)onComplete { - RCTAssert(onComplete != nil, @"onComplete block should not be nil"); -#if DEBUG - RCTAssert(RCTJSONParse(script, NULL) != nil, @"%@ wasn't valid JSON!", script); -#endif + if (RCT_DEBUG) { + RCTAssert(RCTJSONParse(script, NULL) != nil, @"%@ wasn't valid JSON!", script); + } __weak RCTContextExecutor *weakSelf = self; - [self executeBlockOnJavaScriptQueue:^{ + [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ RCTContextExecutor *strongSelf = weakSelf; if (!strongSelf || !strongSelf.isValid) { return; @@ -333,19 +355,21 @@ - (void)injectJSONText:(NSString *)script NSString *errorDesc = [NSString stringWithFormat:@"Can't make JSON value from script '%@'", script]; RCTLogError(@"%@", errorDesc); - NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}]; - onComplete(error); + if (onComplete) { + NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}]; + onComplete(error); + } return; } JSObjectRef globalObject = JSContextGetGlobalObject(strongSelf->_context.ctx); - JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)objectName); JSObjectSetProperty(strongSelf->_context.ctx, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL); JSStringRelease(JSName); - onComplete(nil); - }]; - + if (onComplete) { + onComplete(nil); + } + }), @"js_call,json_call", (@{@"objectName": objectName}))]; } @end diff --git a/React/Executors/RCTWebViewExecutor.h b/React/Executors/RCTWebViewExecutor.h index 77d8a8310715c5..db8710c7d579bf 100644 --- a/React/Executors/RCTWebViewExecutor.h +++ b/React/Executors/RCTWebViewExecutor.h @@ -7,6 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import "RCTDefines.h" + +#if RCT_DEV // Debug executors are only supported in dev mode + #import #import "RCTJavaScriptExecutor.h" @@ -40,3 +44,5 @@ - (UIWebView *)invalidateAndReclaimWebView; @end + +#endif diff --git a/React/Executors/RCTWebViewExecutor.m b/React/Executors/RCTWebViewExecutor.m index 55de44ab969479..56323fb99be674 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -7,6 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import "RCTDefines.h" + +#if RCT_DEV // Debug executors are only supported in dev mode + #import "RCTWebViewExecutor.h" #import @@ -38,13 +42,19 @@ @implementation RCTWebViewExecutor { UIWebView *_webView; NSMutableDictionary *_objectsToInject; + NSRegularExpression *_commentsRegex; + NSRegularExpression *_scriptTagsRegex; } +@synthesize valid = _valid; + - (instancetype)initWithWebView:(UIWebView *)webView { if ((self = [super init])) { _objectsToInject = [[NSMutableDictionary alloc] init]; _webView = webView ?: [[UIWebView alloc] init]; + _commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]*?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL], + _scriptTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\/?script[^>]*?)>" options:0 error:NULL], _webView.delegate = self; } return self; @@ -55,13 +65,9 @@ - (id)init return [self initWithWebView:nil]; } -- (BOOL)isValid -{ - return _webView != nil; -} - - (void)invalidate { + _valid = NO; _webView.delegate = nil; _webView = nil; } @@ -76,10 +82,15 @@ - (UIWebView *)invalidateAndReclaimWebView - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments + context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete { RCTAssert(onComplete != nil, @""); [self executeBlockOnJavaScriptQueue:^{ + if (!self.isValid || ![RCTGetExecutorID(self) isEqualToNumber:executorID]) { + return; + } + NSError *error; NSString *argsString = RCTJSONStringify(arguments, &error); if (!argsString) { @@ -120,10 +131,16 @@ - (void)executeApplicationScript:(NSString *)script } RCTAssert(onComplete != nil, @""); - _onApplicationScriptLoaded = onComplete; + __weak RCTWebViewExecutor *weakSelf = self; + _onApplicationScriptLoaded = ^(NSError *error){ + RCTWebViewExecutor *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + strongSelf->_valid = error == nil; + onComplete(error); + }; - script = [script stringByReplacingOccurrencesOfString:@"" withString:@""]; if (_objectsToInject.count > 0) { NSMutableString *scriptWithInjections = [[NSMutableString alloc] initWithString:@"/* BEGIN NATIVELY INJECTED OBJECTS */\n"]; [_objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *blockScript, BOOL *stop) { @@ -138,6 +155,15 @@ - (void)executeApplicationScript:(NSString *)script script = scriptWithInjections; } + script = [_commentsRegex stringByReplacingMatchesInString:script + options:0 + range:NSMakeRange(0, script.length) + withTemplate:@""]; + script = [_scriptTagsRegex stringByReplacingMatchesInString:script + options:0 + range:NSMakeRange(0, script.length) + withTemplate:@"\\\\<$1\\\\>"]; + NSString *runScript = [NSString stringWithFormat:@"", @@ -183,9 +209,14 @@ - (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)objectName callback:(RCTJavaScriptCompleteBlock)onComplete { - RCTAssert(!_objectsToInject[objectName], - @"already injected object named %@", _objectsToInject[objectName]); + if (RCT_DEBUG) { + RCTAssert(!_objectsToInject[objectName], + @"already injected object named %@", _objectsToInject[objectName]); + } _objectsToInject[objectName] = script; onComplete(nil); } + @end + +#endif diff --git a/React/Layout/Layout.c b/React/Layout/Layout.c index 21dec570ac3522..9ed711cd07be52 100644 --- a/React/Layout/Layout.c +++ b/React/Layout/Layout.c @@ -1,21 +1,15 @@ /** - * @generated SignedSource<> - * - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * !! This file is a check-in from github! !! - * !! !! - * !! You should not modify this file directly. Instead: !! - * !! 1) Go to https://github.com/facebook/css-layout !! - * !! 2) Make a pull request and get it merged !! - * !! 3) Execute ./import.sh to pull in the latest version !! - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * * Copyright (c) 2014, 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. + * + * WARNING: You should not modify this file directly. Instead: + * 1) Go to https://github.com/facebook/css-layout + * 2) Make a pull request and get it merged + * 3) Run import.sh to copy Layout.* to react-native-github */ #include @@ -44,6 +38,12 @@ void init_css_node(css_node_t *node) { node->style.dimensions[CSS_WIDTH] = CSS_UNDEFINED; node->style.dimensions[CSS_HEIGHT] = CSS_UNDEFINED; + node->style.minDimensions[CSS_WIDTH] = CSS_UNDEFINED; + node->style.minDimensions[CSS_HEIGHT] = CSS_UNDEFINED; + + node->style.maxDimensions[CSS_WIDTH] = CSS_UNDEFINED; + node->style.maxDimensions[CSS_HEIGHT] = CSS_UNDEFINED; + node->style.position[CSS_LEFT] = CSS_UNDEFINED; node->style.position[CSS_TOP] = CSS_UNDEFINED; node->style.position[CSS_RIGHT] = CSS_UNDEFINED; @@ -249,6 +249,10 @@ static float getPaddingAndBorder(css_node_t *node, int location) { return getPadding(node, location) + getBorder(node, location); } +static float getBorderAxis(css_node_t *node, css_flex_direction_t axis) { + return getBorder(node, leading[axis]) + getBorder(node, trailing[axis]); +} + static float getMarginAxis(css_node_t *node, css_flex_direction_t axis) { return getMargin(node, leading[axis]) + getMargin(node, trailing[axis]); } @@ -298,7 +302,8 @@ static float getDimWithMargin(css_node_t *node, css_flex_direction_t axis) { } static bool isDimDefined(css_node_t *node, css_flex_direction_t axis) { - return !isUndefined(node->style.dimensions[dim[axis]]); + float value = node->style.dimensions[dim[axis]]; + return !isUndefined(value) && value > 0.0; } static bool isPosDefined(css_node_t *node, css_position_t position) { @@ -317,6 +322,30 @@ static float getPosition(css_node_t *node, css_position_t position) { return 0; } +static float boundAxis(css_node_t *node, css_flex_direction_t axis, float value) { + float min = CSS_UNDEFINED; + float max = CSS_UNDEFINED; + + if (axis == CSS_FLEX_DIRECTION_COLUMN) { + min = node->style.minDimensions[CSS_HEIGHT]; + max = node->style.maxDimensions[CSS_HEIGHT]; + } else if (axis == CSS_FLEX_DIRECTION_ROW) { + min = node->style.minDimensions[CSS_WIDTH]; + max = node->style.maxDimensions[CSS_WIDTH]; + } + + float boundValue = value; + + if (!isUndefined(max) && max >= 0.0 && boundValue > max) { + boundValue = max; + } + if (!isUndefined(min) && min >= 0.0 && boundValue < min) { + boundValue = min; + } + + return boundValue; +} + // When the user specifically sets a value for width or height static void setDimensionFromStyle(css_node_t *node, css_flex_direction_t axis) { // The parent already computed us a width or height. We just skip it @@ -330,7 +359,7 @@ static void setDimensionFromStyle(css_node_t *node, css_flex_direction_t axis) { // The dimensions can never be smaller than the padding and border node->layout.dimensions[dim[axis]] = fmaxf( - node->style.dimensions[dim[axis]], + boundAxis(node, axis, node->style.dimensions[dim[axis]]), getPaddingAndBorderAxis(node, axis) ); } @@ -347,6 +376,7 @@ static float getRelativePosition(css_node_t *node, css_flex_direction_t axis) { static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { /** START_GENERATED **/ + css_flex_direction_t mainAxis = getFlexDirection(node); css_flex_direction_t crossAxis = mainAxis == CSS_FLEX_DIRECTION_ROW ? CSS_FLEX_DIRECTION_COLUMN : @@ -385,25 +415,31 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // Let's not measure the text if we already know both dimensions if (isRowUndefined || isColumnUndefined) { - css_dim_t measure_dim = node->measure( + css_dim_t measureDim = node->measure( node->context, + width ); if (isRowUndefined) { - node->layout.dimensions[CSS_WIDTH] = measure_dim.dimensions[CSS_WIDTH] + + node->layout.dimensions[CSS_WIDTH] = measureDim.dimensions[CSS_WIDTH] + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); } if (isColumnUndefined) { - node->layout.dimensions[CSS_HEIGHT] = measure_dim.dimensions[CSS_HEIGHT] + + node->layout.dimensions[CSS_HEIGHT] = measureDim.dimensions[CSS_HEIGHT] + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); } } return; } + int i; + int ii; + css_node_t* child; + css_flex_direction_t axis; + // Pre-fill some dimensions straight from the parent - for (int i = 0; i < node->children_count; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = 0; i < node->children_count; ++i) { + child = node->get_child(node->context, i); // Pre-fill cross axis dimensions when the child is using stretch before // we call the recursive layout pass if (getAlignItem(node, child) == CSS_ALIGN_STRETCH && @@ -411,27 +447,27 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { !isUndefined(node->layout.dimensions[dim[crossAxis]]) && !isDimDefined(child, crossAxis)) { child->layout.dimensions[dim[crossAxis]] = fmaxf( - node->layout.dimensions[dim[crossAxis]] - + boundAxis(child, crossAxis, node->layout.dimensions[dim[crossAxis]] - getPaddingAndBorderAxis(node, crossAxis) - - getMarginAxis(child, crossAxis), + getMarginAxis(child, crossAxis)), // You never want to go smaller than padding getPaddingAndBorderAxis(child, crossAxis) ); } else if (getPositionType(child) == CSS_POSITION_ABSOLUTE) { // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both // left and right or top and bottom). - for (int ii = 0; ii < 2; ii++) { - css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (!isUndefined(node->layout.dimensions[dim[axis]]) && !isDimDefined(child, axis) && isPosDefined(child, leading[axis]) && isPosDefined(child, trailing[axis])) { child->layout.dimensions[dim[axis]] = fmaxf( - node->layout.dimensions[dim[axis]] - - getPaddingAndBorderAxis(node, axis) - - getMarginAxis(child, axis) - - getPosition(child, leading[axis]) - - getPosition(child, trailing[axis]), + boundAxis(child, axis, node->layout.dimensions[dim[axis]] - + getPaddingAndBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, leading[axis]) - + getPosition(child, trailing[axis])), // You never want to go smaller than padding getPaddingAndBorderAxis(child, axis) ); @@ -449,11 +485,12 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // We want to execute the next two loops one per line with flex-wrap int startLine = 0; int endLine = 0; - int nextLine = 0; + // int nextOffset = 0; + int alreadyComputedNextLayout = 0; // We aggregate the total dimensions of the container in those two variables float linesCrossDim = 0; float linesMainDim = 0; - while (endLine != node->children_count) { + while (endLine < node->children_count) { // Layout non flexible children and count children by type // mainContentDim is accumulation of the dimensions and margin of all the @@ -467,8 +504,10 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { int flexibleChildrenCount = 0; float totalFlexible = 0; int nonFlexibleChildrenCount = 0; - for (int i = startLine; i < node->children_count; ++i) { - css_node_t* child = node->get_child(node->context, i); + + float maxWidth; + for (i = startLine; i < node->children_count; ++i) { + child = node->get_child(node->context, i); float nextContentDim = 0; // It only makes sense to consider a child flexible if we have a computed @@ -478,26 +517,27 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { totalFlexible += getFlex(child); // Even if we don't know its exact size yet, we already know the padding, - // border and margin. We'll use this partial information to compute the - // remaining space. + // border and margin. We'll use this partial information, which represents + // the smallest possible size for the child, to compute the remaining + // available space. nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + getMarginAxis(child, mainAxis); } else { - float maxWidth = CSS_UNDEFINED; - if (mainAxis == CSS_FLEX_DIRECTION_ROW) { - // do nothing - } else if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { - maxWidth = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); - } else { + maxWidth = CSS_UNDEFINED; + if (mainAxis != CSS_FLEX_DIRECTION_ROW) { maxWidth = parentMaxWidth - getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); + + if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { + maxWidth = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - + getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); + } } // This is the main recursive call. We layout non flexible children. - if (nextLine == 0) { + if (alreadyComputedNextLayout == 0) { layoutNode(child, maxWidth); } @@ -513,11 +553,14 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // The element we are about to add would make us go to the next line if (isFlexWrap(node) && !isUndefined(node->layout.dimensions[dim[mainAxis]]) && - mainContentDim + nextContentDim > definedMainDim) { - nextLine = i + 1; + mainContentDim + nextContentDim > definedMainDim && + // If there's only one element, then it's bigger than the content + // and needs its own line + i != startLine) { + alreadyComputedNextLayout = 1; break; } - nextLine = 0; + alreadyComputedNextLayout = 0; mainContentDim += nextContentDim; endLine = i + 1; } @@ -542,6 +585,26 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // remaining space if (flexibleChildrenCount != 0) { float flexibleMainDim = remainingMainDim / totalFlexible; + float baseMainDim; + float boundMainDim; + + // Iterate over every child in the axis. If the flex share of remaining + // space doesn't meet min/max bounds, remove this child from flex + // calculations. + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); + if (isFlex(child)) { + baseMainDim = flexibleMainDim * getFlex(child) + + getPaddingAndBorderAxis(child, mainAxis); + boundMainDim = boundAxis(child, mainAxis, baseMainDim); + + if (baseMainDim != boundMainDim) { + remainingMainDim -= boundMainDim; + totalFlexible -= getFlex(child); + } + } + } + flexibleMainDim = remainingMainDim / totalFlexible; // The non flexible children can overflow the container, in this case // we should just assume that there is no space available. @@ -551,21 +614,20 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // We iterate over the full array and only apply the action on flexible // children. This is faster than actually allocating a new array that // contains only flexible children. - for (int i = startLine; i < endLine; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); if (isFlex(child)) { // At this point we know the final size of the element in the main // dimension - child->layout.dimensions[dim[mainAxis]] = flexibleMainDim * getFlex(child) + - getPaddingAndBorderAxis(child, mainAxis); + child->layout.dimensions[dim[mainAxis]] = boundAxis(child, mainAxis, + flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) + ); - float maxWidth = CSS_UNDEFINED; - if (mainAxis == CSS_FLEX_DIRECTION_ROW) { - // do nothing - } else if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { + maxWidth = CSS_UNDEFINED; + if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { maxWidth = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); - } else { + } else if (mainAxis != CSS_FLEX_DIRECTION_ROW) { maxWidth = parentMaxWidth - getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); @@ -580,9 +642,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // space available } else { css_justify_t justifyContent = getJustifyContent(node); - if (justifyContent == CSS_JUSTIFY_FLEX_START) { - // Do nothing - } else if (justifyContent == CSS_JUSTIFY_CENTER) { + if (justifyContent == CSS_JUSTIFY_CENTER) { leadingMainDim = remainingMainDim / 2; } else if (justifyContent == CSS_JUSTIFY_FLEX_END) { leadingMainDim = remainingMainDim; @@ -612,8 +672,8 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { float mainDim = leadingMainDim + getPaddingAndBorder(node, leading[mainAxis]); - for (int i = startLine; i < endLine; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); if (getPositionType(child) == CSS_POSITION_ABSOLUTE && isPosDefined(child, leading[mainAxis])) { @@ -638,38 +698,25 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); // The cross dimension is the max of the elements dimension since there // can only be one element in that cross dimension. - crossDim = fmaxf(crossDim, getDimWithMargin(child, crossAxis)); + crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); } } - float containerMainAxis = node->layout.dimensions[dim[mainAxis]]; - // If the user didn't specify a width or height, and it has not been set - // by the container, then we set it via the children. - if (isUndefined(node->layout.dimensions[dim[mainAxis]])) { - containerMainAxis = fmaxf( - // We're missing the last padding at this point to get the final - // dimension - mainDim + getPaddingAndBorder(node, trailing[mainAxis]), - // We can never assign a width smaller than the padding and borders - getPaddingAndBorderAxis(node, mainAxis) - ); - } - float containerCrossAxis = node->layout.dimensions[dim[crossAxis]]; if (isUndefined(node->layout.dimensions[dim[crossAxis]])) { containerCrossAxis = fmaxf( // For the cross dim, we add both sides at the end because the value // is aggregate via a max function. Intermediate negative values // can mess this computation otherwise - crossDim + getPaddingAndBorderAxis(node, crossAxis), + boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), getPaddingAndBorderAxis(node, crossAxis) ); } // Position elements in the cross axis - for (int i = startLine; i < endLine; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = startLine; i < endLine; ++i) { + child = node->get_child(node->context, i); if (getPositionType(child) == CSS_POSITION_ABSOLUTE && isPosDefined(child, leading[crossAxis])) { @@ -687,21 +734,19 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // alignSelf (child) in order to determine the position in the cross axis if (getPositionType(child) == CSS_POSITION_RELATIVE) { css_align_t alignItem = getAlignItem(node, child); - if (alignItem == CSS_ALIGN_FLEX_START) { - // Do nothing - } else if (alignItem == CSS_ALIGN_STRETCH) { + if (alignItem == CSS_ALIGN_STRETCH) { // You can only stretch if the dimension has not already been set // previously. if (!isDimDefined(child, crossAxis)) { child->layout.dimensions[dim[crossAxis]] = fmaxf( - containerCrossAxis - + boundAxis(child, crossAxis, containerCrossAxis - getPaddingAndBorderAxis(node, crossAxis) - - getMarginAxis(child, crossAxis), + getMarginAxis(child, crossAxis)), // You never want to go smaller than padding getPaddingAndBorderAxis(child, crossAxis) ); } - } else { + } else if (alignItem != CSS_ALIGN_FLEX_START) { // The remaining space between the parent dimensions+padding and child // dimensions+margin. float remainingCrossDim = containerCrossAxis - @@ -732,7 +777,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { node->layout.dimensions[dim[mainAxis]] = fmaxf( // We're missing the last padding at this point to get the final // dimension - linesMainDim + getPaddingAndBorder(node, trailing[mainAxis]), + boundAxis(node, mainAxis, linesMainDim + getPaddingAndBorder(node, trailing[mainAxis])), // We can never assign a width smaller than the padding and borders getPaddingAndBorderAxis(node, mainAxis) ); @@ -743,37 +788,38 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // For the cross dim, we add both sides at the end because the value // is aggregate via a max function. Intermediate negative values // can mess this computation otherwise - linesCrossDim + getPaddingAndBorderAxis(node, crossAxis), + boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), getPaddingAndBorderAxis(node, crossAxis) ); } // Calculate dimensions for absolutely positioned elements - for (int i = 0; i < node->children_count; ++i) { - css_node_t* child = node->get_child(node->context, i); + for (i = 0; i < node->children_count; ++i) { + child = node->get_child(node->context, i); if (getPositionType(child) == CSS_POSITION_ABSOLUTE) { // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both // left and right or top and bottom). - for (int ii = 0; ii < 2; ii++) { - css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (!isUndefined(node->layout.dimensions[dim[axis]]) && !isDimDefined(child, axis) && isPosDefined(child, leading[axis]) && isPosDefined(child, trailing[axis])) { child->layout.dimensions[dim[axis]] = fmaxf( - node->layout.dimensions[dim[axis]] - - getPaddingAndBorderAxis(node, axis) - - getMarginAxis(child, axis) - - getPosition(child, leading[axis]) - - getPosition(child, trailing[axis]), + boundAxis(child, axis, node->layout.dimensions[dim[axis]] - + getBorderAxis(node, axis) - + getMarginAxis(child, axis) - + getPosition(child, leading[axis]) - + getPosition(child, trailing[axis]) + ), // You never want to go smaller than padding getPaddingAndBorderAxis(child, axis) ); } } - for (int ii = 0; ii < 2; ii++) { - css_flex_direction_t axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; + for (ii = 0; ii < 2; ii++) { + axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; if (isPosDefined(child, trailing[axis]) && !isPosDefined(child, leading[axis])) { child->layout.position[leading[axis]] = diff --git a/React/Layout/Layout.h b/React/Layout/Layout.h index 51f72493bb5b2f..fe383ea5728ed1 100644 --- a/React/Layout/Layout.h +++ b/React/Layout/Layout.h @@ -1,21 +1,15 @@ /** - * @generated SignedSource<<58298c7a8815a8675e970b0347dedfed>> - * - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * !! This file is a check-in from github! !! - * !! !! - * !! You should not modify this file directly. Instead: !! - * !! 1) Go to https://github.com/facebook/css-layout !! - * !! 2) Make a pull request and get it merged !! - * !! 3) Execute ./import.sh to pull in the latest version !! - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - * * Copyright (c) 2014, 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. + * + * WARNING: You should not modify this file directly. Instead: + * 1) Go to https://github.com/facebook/css-layout + * 2) Make a pull request and get it merged + * 3) Run import.sh to copy Layout.* to react-native-github */ #ifndef __LAYOUT_H @@ -113,6 +107,8 @@ typedef struct { float padding[4]; float border[4]; float dimensions[2]; + float minDimensions[2]; + float maxDimensions[2]; } css_style_t; typedef struct css_node { diff --git a/React/Layout/import.sh b/React/Layout/import.sh new file mode 100755 index 00000000000000..7e69403f3a44a3 --- /dev/null +++ b/React/Layout/import.sh @@ -0,0 +1,15 @@ +LAYOUT_C=`curl https://raw.githubusercontent.com/facebook/css-layout/master/src/Layout.c` +LAYOUT_H=`curl https://raw.githubusercontent.com/facebook/css-layout/master/src/Layout.h` + +REPLACE_STRING="* + * WARNING: You should not modify this file directly. Instead: + * 1) Go to https://github.com/facebook/css-layout + * 2) Make a pull request and get it merged + * 3) Run import.sh to copy Layout.* to react-native-github + */" + +LAYOUT_C=${LAYOUT_C/\*\//$REPLACE_STRING} +LAYOUT_H=${LAYOUT_H/\*\//$REPLACE_STRING} + +echo "$LAYOUT_C" > Layout.c +echo "$LAYOUT_H" > Layout.h diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m index ae11ce52be14df..2690de1dfea65e 100644 --- a/React/Modules/RCTAlertManager.m +++ b/React/Modules/RCTAlertManager.m @@ -35,6 +35,11 @@ - (instancetype)init return self; } +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + /** * @param {NSDictionary} args Dictionary of the form * @@ -64,37 +69,34 @@ - (instancetype)init return; } - dispatch_async(dispatch_get_main_queue(), ^{ - - UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title - message:message - delegate:self - cancelButtonTitle:nil - otherButtonTitles:nil]; - - NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count]; - - NSInteger index = 0; - for (NSDictionary *button in buttons) { - if (button.count != 1) { - RCTLogError(@"Button definitions should have exactly one key."); - } - NSString *buttonKey = [button.allKeys firstObject]; - NSString *buttonTitle = [button[buttonKey] description]; - [alertView addButtonWithTitle:buttonTitle]; - if ([buttonKey isEqualToString: @"cancel"]) { - alertView.cancelButtonIndex = index; - } - [buttonKeys addObject:buttonKey]; - index ++; + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title + message:message + delegate:self + cancelButtonTitle:nil + otherButtonTitles:nil]; + + NSMutableArray *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count]; + + NSInteger index = 0; + for (NSDictionary *button in buttons) { + if (button.count != 1) { + RCTLogError(@"Button definitions should have exactly one key."); + } + NSString *buttonKey = [button.allKeys firstObject]; + NSString *buttonTitle = [button[buttonKey] description]; + [alertView addButtonWithTitle:buttonTitle]; + if ([buttonKey isEqualToString: @"cancel"]) { + alertView.cancelButtonIndex = index; } + [buttonKeys addObject:buttonKey]; + index ++; + } - [_alerts addObject:alertView]; - [_alertCallbacks addObject:callback ?: ^(id unused) {}]; - [_alertButtonKeys addObject:buttonKeys]; + [_alerts addObject:alertView]; + [_alertCallbacks addObject:callback ?: ^(id unused) {}]; + [_alertButtonKeys addObject:buttonKeys]; - [alertView show]; - }); + [alertView show]; } #pragma mark - UIAlertViewDelegate diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index 8e6d414cf083ef..2c01161d48fa93 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -61,20 +61,6 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } -static dispatch_queue_t RCTFileQueue(void) -{ - static dispatch_queue_t fileQueue = NULL; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - // All JS is single threaded, so a serial queue is our only option. - fileQueue = dispatch_queue_create("com.facebook.rkFile", DISPATCH_QUEUE_SERIAL); - dispatch_set_target_queue(fileQueue, - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); - }); - - return fileQueue; -} - #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -90,6 +76,11 @@ @implementation RCTAsyncLocalStorage RCT_EXPORT_MODULE() +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); +} + - (NSString *)_filePathForKey:(NSString *)key { NSString *safeFileName = RCTMD5Hash(key); @@ -196,99 +187,89 @@ - (id)_writeEntry:(NSArray *)entry return; } - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[@[errorOut], [NSNull null]]); - return; - } - NSMutableArray *errors; - NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; - for (NSString *key in keys) { - id keyError = [self _appendItemForKey:key toArray:result]; - RCTAppendError(keyError, &errors); - } - [self _writeManifest:&errors]; - callback(@[errors ?: [NSNull null], result]); - }); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut], [NSNull null]]); + return; + } + NSMutableArray *errors; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + for (NSString *key in keys) { + id keyError = [self _appendItemForKey:key toArray:result]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + callback(@[errors ?: [NSNull null], result]); } RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[@[errorOut]]); - return; - } - NSMutableArray *errors; - for (NSArray *entry in kvPairs) { - id keyError = [self _writeEntry:entry]; - RCTAppendError(keyError, &errors); - } - [self _writeManifest:&errors]; - if (callback) { - callback(@[errors ?: [NSNull null]]); - } - }); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + id keyError = [self _writeEntry:entry]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } } RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[@[errorOut]]); - return; - } - NSMutableArray *errors; - for (NSString *key in keys) { - id keyError = RCTErrorForKey(key); - if (!keyError) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; - [_manifest removeObjectForKey:key]; - } - RCTAppendError(keyError, &errors); - } - [self _writeManifest:&errors]; - if (callback) { - callback(@[errors ?: [NSNull null]]); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSString *key in keys) { + id keyError = RCTErrorForKey(key); + if (!keyError) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [_manifest removeObjectForKey:key]; } - }); + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } } RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (!errorOut) { - NSError *error; - for (NSString *key in _manifest) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; - } - [_manifest removeAllObjects]; - errorOut = [self _writeManifest:nil]; - } - if (callback) { - callback(@[errorOut ?: [NSNull null]]); + id errorOut = [self _ensureSetup]; + if (!errorOut) { + NSError *error; + for (NSString *key in _manifest) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; } - }); + [_manifest removeAllObjects]; + errorOut = [self _writeManifest:nil]; + } + if (callback) { + callback(@[errorOut ?: [NSNull null]]); + } } RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) { - dispatch_async(RCTFileQueue(), ^{ - id errorOut = [self _ensureSetup]; - if (errorOut) { - callback(@[errorOut, [NSNull null]]); - } else { - callback(@[[NSNull null], [_manifest allKeys]]); - } - }); + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut, [NSNull null]]); + } else { + callback(@[[NSNull null], [_manifest allKeys]]); + } } @end diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index 5be80133bcf96e..ddea1275acabd3 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -9,6 +9,7 @@ #import "RCTExceptionsManager.h" +#import "RCTDefines.h" #import "RCTLog.h" #import "RCTRedBox.h" #import "RCTRootView.h" @@ -19,10 +20,6 @@ @implementation RCTExceptionsManager NSUInteger _reloadRetries; } -#ifndef DEBUG -static NSUInteger RCTReloadRetries = 0; -#endif - RCT_EXPORT_MODULE() - (instancetype)initWithDelegate:(id)delegate @@ -47,42 +44,44 @@ - (instancetype)init return; } -#ifdef DEBUG - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; -#else - if (RCTReloadRetries < _maxReloadAttempts) { - RCTReloadRetries++; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; - }); - } else { - NSError *error; - const NSUInteger MAX_SANITIZED_LENGTH = 75; - // Filter out numbers so the same base errors are mapped to the same categories independent of incorrect values. - NSString *pattern = @"[+-]?\\d+[,.]?\\d*"; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error]; - RCTAssert(error == nil, @"Bad regex pattern: %@", pattern); - NSString *sanitizedMessage = [regex stringByReplacingMatchesInString:message - options:0 - range:NSMakeRange(0, message.length) - withTemplate:@""]; - if (sanitizedMessage.length > MAX_SANITIZED_LENGTH) { - sanitizedMessage = [[sanitizedMessage substringToIndex:MAX_SANITIZED_LENGTH] stringByAppendingString:@"..."]; - } - NSMutableString *prettyStack = [@"\n" mutableCopy]; - for (NSDictionary *frame in stack) { - [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; - } + [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + + if (!RCT_DEBUG) { + + static NSUInteger reloadRetries = 0; + const NSUInteger maxMessageLength = 75; + + if (reloadRetries < _maxReloadAttempts) { + + reloadRetries++; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification + object:nil]; + + } else { - NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:sanitizedMessage]; - [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; + if (message.length > maxMessageLength) { + message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; + } + + NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"]; + for (NSDictionary *frame in stack) { + [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; + } + + NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message]; + [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; + } } -#endif } RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message stack:(NSArray *)stack) { + if (_delegate) { + [_delegate unhandledJSExceptionWithMessage:message stack:stack]; + return; + } + [[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack]; } diff --git a/React/Modules/RCTSourceCode.m b/React/Modules/RCTSourceCode.m index 76e9190bc279ed..1b6eb842ece49d 100644 --- a/React/Modules/RCTSourceCode.m +++ b/React/Modules/RCTSourceCode.m @@ -10,12 +10,15 @@ #import "RCTSourceCode.h" #import "RCTAssert.h" +#import "RCTBridge.h" #import "RCTUtils.h" @implementation RCTSourceCode RCT_EXPORT_MODULE() +@synthesize bridge = _bridge; + RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback failureCallback:(RCTResponseSenderBlock)failureCallback) { @@ -24,7 +27,12 @@ @implementation RCTSourceCode } else { failureCallback(@[RCTMakeError(@"Source code is not available", nil, nil)]); } +} +- (NSDictionary *)constantsToExport +{ + NSString *URL = [self.bridge.bundleURL absoluteString] ?: @""; + return @{@"scriptURL": URL}; } @end diff --git a/React/Modules/RCTStatusBarManager.m b/React/Modules/RCTStatusBarManager.m index ad8ee1df6db4f0..04bb390387c0fb 100644 --- a/React/Modules/RCTStatusBarManager.m +++ b/React/Modules/RCTStatusBarManager.m @@ -26,34 +26,33 @@ static BOOL RCTViewControllerBasedStatusBarAppearance() RCT_EXPORT_MODULE() +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + RCT_EXPORT_METHOD(setStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated) { - dispatch_async(dispatch_get_main_queue(), ^{ - - if (RCTViewControllerBasedStatusBarAppearance()) { - RCTLogError(@"RCTStatusBarManager module requires that the \ - UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); - } else { - [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle - animated:animated]; - } - }); + if (RCTViewControllerBasedStatusBarAppearance()) { + RCTLogError(@"RCTStatusBarManager module requires that the \ + UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); + } else { + [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle + animated:animated]; + } } RCT_EXPORT_METHOD(setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation) { - dispatch_async(dispatch_get_main_queue(), ^{ - - if (RCTViewControllerBasedStatusBarAppearance()) { - RCTLogError(@"RCTStatusBarManager module requires that the \ - UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); - } else { - [[UIApplication sharedApplication] setStatusBarHidden:hidden - withAnimation:animation]; - } - }); + if (RCTViewControllerBasedStatusBarAppearance()) { + RCTLogError(@"RCTStatusBarManager module requires that the \ + UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); + } else { + [[UIApplication sharedApplication] setStatusBarHidden:hidden + withAnimation:animation]; + } } - (NSDictionary *)constantsToExport diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index aaab5fae006c61..e21c9d16fe4732 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -70,6 +70,7 @@ @implementation RCTTiming } @synthesize bridge = _bridge; +@synthesize paused = _paused; RCT_EXPORT_MODULE() @@ -78,7 +79,7 @@ @implementation RCTTiming - (instancetype)init { if ((self = [super init])) { - + _paused = YES; _timers = [[RCTSparseArray alloc] init]; for (NSString *name in @[UIApplicationWillResignActiveNotification, @@ -108,6 +109,11 @@ - (void)dealloc [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (dispatch_queue_t)methodQueue +{ + return RCTJSThread; +} + - (BOOL)isValid { return _bridge != nil; @@ -121,24 +127,20 @@ - (void)invalidate - (void)stopTimers { - [_bridge removeFrameUpdateObserver:self]; + _paused = YES; } - (void)startTimers { - RCTAssertMainThread(); - if (![self isValid] || _timers.count == 0) { return; } - [_bridge addFrameUpdateObserver:self]; + _paused = NO; } - (void)didUpdateFrame:(RCTFrameUpdate *)update { - RCTAssertMainThread(); - NSMutableArray *timersToCall = [[NSMutableArray alloc] init]; for (RCTTimer *timer in _timers.allObjects) { if ([timer updateFoundNeedsJSUpdate]) { @@ -187,21 +189,17 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update interval:jsDuration targetTime:targetTime repeats:repeats]; - dispatch_async(dispatch_get_main_queue(), ^{ - _timers[callbackID] = timer; - [self startTimers]; - }); + _timers[callbackID] = timer; + [self startTimers]; } RCT_EXPORT_METHOD(deleteTimer:(NSNumber *)timerID) { if (timerID) { - dispatch_async(dispatch_get_main_queue(), ^{ - _timers[timerID] = nil; - if (_timers.count == 0) { - [self stopTimers]; - } - }); + _timers[timerID] = nil; + if (_timers.count == 0) { + [self stopTimers]; + } } else { RCTLogWarn(@"Called deleteTimer: with a nil timerID"); } diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index 4f42cd0b7ff33d..6fa0e3249185d3 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -9,10 +9,10 @@ #import -#import "../Base/RCTBridge.h" -#import "../Base/RCTBridgeModule.h" -#import "../Base/RCTInvalidating.h" -#import "../Views/RCTViewManager.h" +#import "RCTBridge.h" +#import "RCTBridgeModule.h" +#import "RCTInvalidating.h" +#import "RCTViewManager.h" @protocol RCTScrollableProtocol; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index b6a350dcd4964e..df90ff1505c7bb 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -18,7 +18,10 @@ #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTConvert.h" +#import "RCTDefines.h" +#import "RCTEventDispatcher.h" #import "RCTLog.h" +#import "RCTProfile.h" #import "RCTRootView.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" @@ -178,7 +181,7 @@ @interface RCTUIManager () @implementation RCTUIManager { - __weak dispatch_queue_t _shadowQueue; + dispatch_queue_t _shadowQueue; // Root views are only mutated on the shadow queue NSMutableSet *_rootViewTags; @@ -220,46 +223,34 @@ @implementation RCTUIManager return name; } +// TODO: only send name once instead of a dictionary of name and type keyed by name static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewName) { - NSMutableDictionary *nativeProps = [[NSMutableDictionary alloc] init]; - static const char *prefix = "getPropConfig"; - static const NSUInteger prefixLength = sizeof("getPropConfig") - 1; - unsigned int methodCount = 0; - Method *methods = class_copyMethodList(objc_getMetaClass(class_getName(managerClass)), &methodCount); - for (unsigned int i = 0; i < methodCount; i++) { + unsigned int count = 0; + Method *methods = class_copyMethodList(object_getClass(managerClass), &count); + NSMutableDictionary *props = [[NSMutableDictionary alloc] initWithCapacity:count]; + for (unsigned int i = 0; i < count; i++) { Method method = methods[i]; - SEL getInfo = method_getName(method); - const char *selName = sel_getName(getInfo); - if (strlen(selName) > prefixLength && strncmp(selName, prefix, prefixLength) == 0) { - NSDictionary *info = ((NSDictionary *(*)(id, SEL))method_getImplementation(method))(managerClass, getInfo); - nativeProps[info[@"name"]] = info; + NSString *methodName = NSStringFromSelector(method_getName(method)); + if ([methodName hasPrefix:@"getPropConfig"]) { + NSRange nameRange = [methodName rangeOfString:@"_"]; + if (nameRange.length) { + NSString *name = [methodName substringFromIndex:nameRange.location + 1]; + NSString *type = [managerClass valueForKey:methodName]; + props[name] = type; + } } } - return @{ - @"uiViewClassName": viewName, - @"nativeProps": nativeProps - }; -} - -/** - * This private constructor should only be called when creating - * isolated UIImanager instances for testing. Normal initialization - * is via -init:, which is called automatically by the bridge. - */ -- (instancetype)initWithShadowQueue:(dispatch_queue_t)shadowQueue -{ - if ((self = [self init])) { - _shadowQueue = shadowQueue; - _viewManagers = [[NSMutableDictionary alloc] init]; - } - return self; + free(methods); + return props; } - (instancetype)init { if ((self = [super init])) { + _shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL); + _pendingUIBlocksLock = [[NSLock alloc] init]; _defaultShadowViews = [[NSMutableDictionary alloc] init]; @@ -277,11 +268,6 @@ - (instancetype)init return self; } -- (void)dealloc -{ - RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); -} - - (BOOL)isValid { return _viewRegistry != nil; @@ -289,20 +275,24 @@ - (BOOL)isValid - (void)invalidate { - RCTAssertMainThread(); + /** + * Called on the JS Thread since all modules are invalidated on the JS thread + */ - for (NSNumber *rootViewTag in _rootViewTags) { - ((UIView *)_viewRegistry[rootViewTag]).userInteractionEnabled = NO; - } + dispatch_async(dispatch_get_main_queue(), ^{ + for (NSNumber *rootViewTag in _rootViewTags) { + ((UIView *)_viewRegistry[rootViewTag]).userInteractionEnabled = NO; + } - _rootViewTags = nil; - _shadowViewRegistry = nil; - _viewRegistry = nil; - _bridge = nil; + _rootViewTags = nil; + _shadowViewRegistry = nil; + _viewRegistry = nil; + _bridge = nil; - [_pendingUIBlocksLock lock]; - _pendingUIBlocks = nil; - [_pendingUIBlocksLock unlock]; + [_pendingUIBlocksLock lock]; + _pendingUIBlocks = nil; + [_pendingUIBlocksLock unlock]; + }); } - (void)setBridge:(RCTBridge *)bridge @@ -310,7 +300,6 @@ - (void)setBridge:(RCTBridge *)bridge RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance"); _bridge = bridge; - _shadowQueue = _bridge.shadowQueue; _shadowViewRegistry = [[RCTSparseArray alloc] init]; // Get view managers from bridge @@ -328,6 +317,11 @@ - (void)setBridge:(RCTBridge *)bridge _viewConfigs = [viewConfigs copy]; } +- (dispatch_queue_t)methodQueue +{ + return _shadowQueue; +} + - (void)registerRootView:(UIView *)rootView; { RCTAssertMainThread(); @@ -366,7 +360,7 @@ - (void)setFrame:(CGRect)frame forRootView:(UIView *)rootView NSNumber *reactTag = rootView.reactTag; RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag); - dispatch_async(_bridge.shadowQueue, ^{ + dispatch_async(_shadowQueue, ^{ RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); rootShadowView.frame = frame; @@ -396,15 +390,15 @@ - (void)_purgeChildren:(NSArray *)children fromRegistry:(RCTSparseArray *)regist - (void)addUIBlock:(RCTViewManagerUIBlock)block { - RCTAssert(![NSThread isMainThread], @"This method should only be called on the shadow thread"); + if (!self.isValid) { + return; + } __weak RCTUIManager *weakViewManager = self; - __weak RCTSparseArray *weakViewRegistry = _viewRegistry; dispatch_block_t outerBlock = ^{ RCTUIManager *strongViewManager = weakViewManager; - RCTSparseArray *strongViewRegistry = weakViewRegistry; - if (strongViewManager && strongViewRegistry) { - block(strongViewManager, strongViewRegistry); + if (strongViewManager && strongViewManager.isValid) { + block(strongViewManager, strongViewManager->_viewRegistry); } }; @@ -415,7 +409,7 @@ - (void)addUIBlock:(RCTViewManagerUIBlock)block - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView { - RCTAssert(![NSThread isMainThread], @"This should never be executed on main thread."); + RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread"); NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1]; @@ -427,17 +421,31 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; - // Parallel arrays + // Parallel arrays are built and then handed off to main thread NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *areNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *parentsAreNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; for (RCTShadowView *shadowView in viewsWithNewFrames) { [frameReactTags addObject:shadowView.reactTag]; [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; [areNew addObject:@(shadowView.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)]; + id event = [NSNull null]; + if (shadowView.hasOnLayout) { + event = @{ + @"target": shadowView.reactTag, + @"layout": @{ + @"x": @(shadowView.frame.origin.x), + @"y": @(shadowView.frame.origin.y), + @"width": @(shadowView.frame.size.width), + @"height": @(shadowView.frame.size.height), + }, + }; + } + [onLayoutEvents addObject:event]; } for (RCTShadowView *shadowView in viewsWithNewFrames) { @@ -455,20 +463,30 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo // Perform layout (possibly animated) NSNumber *rootViewTag = rootShadowView.reactTag; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTResponseSenderBlock callback = self->_layoutAnimation.callback; + __block NSInteger completionsCalled = 0; for (NSUInteger ii = 0; ii < frames.count; ii++) { NSNumber *reactTag = frameReactTags[ii]; UIView *view = viewRegistry[reactTag]; CGRect frame = [frames[ii] CGRectValue]; + id event = onLayoutEvents[ii]; + + BOOL isNew = [areNew[ii] boolValue]; + RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation; + BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; + RCTAnimation *createAnimation = shouldAnimateCreation ? _layoutAnimation.createAnimation : nil; void (^completion)(BOOL finished) = ^(BOOL finished) { - if (self->_layoutAnimation.callback) { - self->_layoutAnimation.callback(@[@(finished)]); + completionsCalled++; + if (event != [NSNull null]) { + [self.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event]; + } + if (callback && completionsCalled == frames.count - 1) { + callback(@[@(finished)]); } }; // Animate view update - BOOL isNew = [areNew[ii] boolValue]; - RCTAnimation *updateAnimation = isNew ? nil: _layoutAnimation.updateAnimation; if (updateAnimation) { [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; @@ -485,9 +503,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo } // Animate view creation - BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; - RCTAnimation *createAnimation = _layoutAnimation.createAnimation; - if (shouldAnimateCreation && createAnimation) { + if (createAnimation) { if ([createAnimation.property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); } else if ([createAnimation.property isEqualToString:@"opacity"]) { @@ -667,7 +683,7 @@ - (void)_manageChildren:(NSNumber *)containerReactTag { id container = registry[containerReactTag]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); - RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one react child to add"); + RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); // Removes (both permanent and temporary moves) are using "before" indices NSArray *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; @@ -677,6 +693,8 @@ - (void)_manageChildren:(NSNumber *)containerReactTag [self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry]; + // TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient + // Figure out what to insert - merge temporary inserts and adds NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary]; for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) { @@ -708,20 +726,16 @@ static BOOL RCTCallPropertySetter(NSString *key, SEL setter, id value, id view, ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView); }; -#if DEBUG - - NSString *viewName = RCTViewNameForModuleName(RCTBridgeModuleNameForClass([manager class])); - NSString *logPrefix = [NSString stringWithFormat: - @"Error setting property '%@' of %@ with tag #%@: ", - key, viewName, [view reactTag]]; - - RCTPerformBlockWithLogPrefix(block, logPrefix); - -#else + if (RCT_DEBUG) { + NSString *viewName = RCTViewNameForModuleName(RCTBridgeModuleNameForClass([manager class])); + NSString *logPrefix = [NSString stringWithFormat: + @"Error setting property '%@' of %@ with tag #%@: ", + key, viewName, [view reactTag]]; - block(); - -#endif + RCTPerformBlockWithLogPrefix(block, logPrefix); + } else { + block(); + } return YES; } @@ -884,8 +898,6 @@ - (void)batchDidComplete - (void)flushUIBlocks { - RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread"); - // First copy the previous blocks into a temporary variable, then reset the // pending blocks to a new array. This guards against mutation while // processing the pending blocks in another thread. @@ -896,9 +908,13 @@ - (void)flushUIBlocks // Execute the previously queued UI blocks dispatch_async(dispatch_get_main_queue(), ^{ + RCTProfileBeginEvent(); for (dispatch_block_t block in previousPendingUIBlocks) { block(); } + RCTProfileEndEvent(@"UIManager flushUIBlocks", @"objc_call", @{ + @"count": @(previousPendingUIBlocks.count), + }); }); } @@ -924,7 +940,7 @@ - (void)flushUIBlocks } // TODO: this doesn't work because sometimes view is inside a modal window - // RCTAssert([rootView isReactRootView], @"React view is not inside a react root view"); + // RCTAssert([rootView isReactRootView], @"React view is not inside a React root view"); // By convention, all coordinates, whether they be touch coordinates, or // measurement coordinates are with respect to the root view. @@ -1001,13 +1017,13 @@ static void RCTMeasureLayout(RCTShadowView *view, } /** - * Returns an array of computed offset layouts in a dictionary form. The layouts are of any react subviews + * Returns an array of computed offset layouts in a dictionary form. The layouts are of any React subviews * that are immediate descendants to the parent view found within a specified rect. The dictionary result * contains left, top, width, height and an index. The index specifies the position among the other subviews. * Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the * passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts. */ -RCT_EXPORT_METHOD(measureViewsInRect:(NSDictionary *)rect +RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect parentView:(NSNumber *)reactTag errorCallback:(RCTResponseSenderBlock)errorCallback callback:(RCTResponseSenderBlock)callback) @@ -1019,7 +1035,7 @@ static void RCTMeasureLayout(RCTShadowView *view, } NSArray *childShadowViews = [shadowView reactSubviews]; NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:[childShadowViews count]]; - CGRect layoutRect = [RCTConvert CGRect:rect]; + [childShadowViews enumerateObjectsUsingBlock:^(RCTShadowView *childShadowView, NSUInteger idx, BOOL *stop) { CGRect childLayout = [childShadowView measureLayoutRelativeToAncestor:shadowView]; @@ -1034,10 +1050,11 @@ static void RCTMeasureLayout(RCTShadowView *view, CGFloat width = childLayout.size.width; CGFloat height = childLayout.size.height; - if (leftOffset <= layoutRect.origin.x + layoutRect.size.width && - leftOffset + width >= layoutRect.origin.x && - topOffset <= layoutRect.origin.y + layoutRect.size.height && - topOffset + height >= layoutRect.origin.y) { + if (leftOffset <= rect.origin.x + rect.size.width && + leftOffset + width >= rect.origin.x && + topOffset <= rect.origin.y + rect.size.height && + topOffset + height >= rect.origin.y) { + // This view is within the layout rect NSDictionary *result = @{@"index": @(idx), @"left": @(leftOffset), @@ -1165,6 +1182,12 @@ - (NSDictionary *)customBubblingEventTypes @"captured": @"onNavigationCompleteCapture" } }, + @"topNavLeftButtonTap": @{ + @"phasedRegistrationNames": @{ + @"bubbled": @"onNavLeftButtonTap", + @"captured": @"onNavLefttButtonTapCapture" + } + }, @"topNavRightButtonTap": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onNavRightButtonTap", @@ -1262,6 +1285,9 @@ - (NSDictionary *)customDirectEventTypes @"topScrollAnimationEnd": @{ @"registrationName": @"onScrollAnimationEnd" }, + @"topLayout": @{ + @"registrationName": @"onLayout" + }, @"topSelectionChange": @{ @"registrationName": @"onSelectionChange" }, @@ -1389,17 +1415,22 @@ - (NSDictionary *)constantsToExport } mutableCopy]; [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTViewManager *manager, BOOL *stop) { + NSMutableDictionary *constantsNamespace = [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; + + // Add custom constants // TODO: should these be inherited? NSDictionary *constants = RCTClassOverridesInstanceMethod([manager class], @selector(constantsToExport)) ? [manager constantsToExport] : nil; if (constants.count) { - NSMutableDictionary *constantsNamespace = [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; RCTAssert(constantsNamespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); // add an additional 'Constants' namespace for each class constantsNamespace[@"Constants"] = constants; - allJSConstants[name] = [constantsNamespace copy]; } + + // Add native props + constantsNamespace[@"nativeProps"] = _viewConfigs[name]; + + allJSConstants[name] = [constantsNamespace copy]; }]; - allJSConstants[@"viewConfigs"] = _viewConfigs; return allJSConstants; } @@ -1408,8 +1439,7 @@ - (NSDictionary *)constantsToExport errorCallback:(RCTResponseSenderBlock)errorCallback) { if (_nextLayoutAnimation) { - RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", - _nextLayoutAnimation, config); + RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation, config); } if (config[@"delete"] != nil) { RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 294bf414595641..42954d36e55092 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 */; }; + 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; }; + 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.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 */; }; @@ -21,6 +23,7 @@ 137327E91AA5CF210034F82E /* RCTTabBarItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E41AA5CF210034F82E /* RCTTabBarItemManager.m */; }; 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E61AA5CF210034F82E /* RCTTabBarManager.m */; }; 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */; }; + 13AF20451AE707F9005F5298 /* RCTSlider.m in Sources */ = {isa = PBXBuildFile; fileRef = 13AF20441AE707F9005F5298 /* RCTSlider.m */; }; 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */; }; 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FEA1A69327A00A75B9A /* RCTExceptionsManager.m */; }; 13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FEE1A69327A00A75B9A /* RCTTiming.m */; }; @@ -32,7 +35,7 @@ 13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080131A69489C00A75B9A /* RCTNavItemManager.m */; }; 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080151A69489C00A75B9A /* RCTTextField.m */; }; 13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; }; - 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */; }; + 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */; }; 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; }; 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; }; 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; }; @@ -47,6 +50,7 @@ 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F362081AABD06A001CE568 /* RCTSwitch.m */; }; 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */; }; 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */; }; + 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */; }; 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A131AAE854800E7D092 /* RCTPicker.m */; }; 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; @@ -82,6 +86,10 @@ 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = ""; }; 00C1A2B11AC0B7E000E89A1C /* RCTDevMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDevMenu.h; sourceTree = ""; }; 00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenu.m; sourceTree = ""; }; + 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = ""; }; + 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; }; + 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; }; + 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControlManager.m; sourceTree = ""; }; 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 = ""; }; @@ -107,6 +115,9 @@ 137327E61AA5CF210034F82E /* RCTTabBarManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTabBarManager.m; sourceTree = ""; }; 13A1F71C1A75392D00D3D453 /* RCTKeyCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTKeyCommands.h; sourceTree = ""; }; 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTKeyCommands.m; sourceTree = ""; }; + 13AF1F851AE6E777005F5298 /* RCTDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDefines.h; sourceTree = ""; }; + 13AF20431AE707F8005F5298 /* RCTSlider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSlider.h; sourceTree = ""; }; + 13AF20441AE707F9005F5298 /* RCTSlider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSlider.m; sourceTree = ""; }; 13B07FC71A68125100A75B9A /* Layout.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = Layout.c; sourceTree = ""; }; 13B07FC81A68125100A75B9A /* Layout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Layout.h; sourceTree = ""; }; 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAlertManager.h; sourceTree = ""; }; @@ -131,8 +142,8 @@ 13B080151A69489C00A75B9A /* RCTTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextField.m; sourceTree = ""; }; 13B080161A69489C00A75B9A /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = ""; }; 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = ""; }; - 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIActivityIndicatorViewManager.h; sourceTree = ""; }; - 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIActivityIndicatorViewManager.m; sourceTree = ""; }; + 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorViewManager.h; sourceTree = ""; }; + 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorViewManager.m; sourceTree = ""; }; 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; }; 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; }; 13C156011AB1A2840079392D /* RCTWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebView.h; sourceTree = ""; }; @@ -165,6 +176,8 @@ 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSwitchManager.m; sourceTree = ""; }; 14F484541AABFCE100FDF6B9 /* RCTSliderManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSliderManager.h; sourceTree = ""; }; 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSliderManager.m; sourceTree = ""; }; + 14F4D3891AE1B7E40049C042 /* RCTProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTProfile.h; sourceTree = ""; }; + 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTProfile.m; sourceTree = ""; }; 58114A121AAE854800E7D092 /* RCTPicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPicker.h; sourceTree = ""; }; 58114A131AAE854800E7D092 /* RCTPicker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPicker.m; sourceTree = ""; }; 58114A141AAE854800E7D092 /* RCTPickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPickerManager.h; sourceTree = ""; }; @@ -283,6 +296,10 @@ 58114A141AAE854800E7D092 /* RCTPickerManager.h */, 58114A151AAE854800E7D092 /* RCTPickerManager.m */, 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */, + 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */, + 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */, + 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */, + 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */, 13B07FF61A6947C200A75B9A /* RCTScrollView.h */, 13B07FF71A6947C200A75B9A /* RCTScrollView.m */, 13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */, @@ -290,6 +307,8 @@ 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */, 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */, 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */, + 13AF20431AE707F8005F5298 /* RCTSlider.h */, + 13AF20441AE707F9005F5298 /* RCTSlider.m */, 14F484541AABFCE100FDF6B9 /* RCTSliderManager.h */, 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */, 14F362071AABD06A001CE568 /* RCTSwitch.h */, @@ -308,8 +327,8 @@ 13B080151A69489C00A75B9A /* RCTTextField.m */, 13B080161A69489C00A75B9A /* RCTTextFieldManager.h */, 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */, - 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */, - 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */, + 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */, + 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */, 13E0674F1A70F44B002CDEE1 /* RCTView.h */, 13E067501A70F44B002CDEE1 /* RCTView.m */, 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */, @@ -372,6 +391,7 @@ 830BA4541A8E3BDA00D53203 /* RCTCache.m */, 83CBBACA1A6023D300E9B192 /* RCTConvert.h */, 83CBBACB1A6023D300E9B192 /* RCTConvert.m */, + 13AF1F851AE6E777005F5298 /* RCTDefines.h */, 83CBBA651A601EF300E9B192 /* RCTEventDispatcher.h */, 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */, 83CBBA4C1A601E3B00E9B192 /* RCTInvalidating.h */, @@ -393,6 +413,8 @@ 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA501A601E3B00E9B192 /* RCTUtils.m */, 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */, + 14F4D3891AE1B7E40049C042 /* RCTProfile.h */, + 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */, ); path = Base; sourceTree = ""; @@ -477,15 +499,18 @@ 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, + 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, + 13AF20451AE707F9005F5298 /* RCTSlider.m in Sources */, 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, + 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */, 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */, 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, - 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */, + 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */, 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, @@ -515,6 +540,7 @@ 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */, 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, + 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */, 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */, @@ -557,7 +583,9 @@ GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", - "$(inherited)", + "RCT_DEBUG=1", + "RCT_DEV=1", + "RCT_NSASSERT=1", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -595,6 +623,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREPROCESSOR_DEFINITIONS = ""; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -611,10 +640,8 @@ 83CBBA401A601D0F00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); + CLANG_STATIC_ANALYZER_MODE = deep; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -622,6 +649,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; + RUN_CLANG_STATIC_ANALYZER = YES; SKIP_INSTALL = YES; }; name = Debug; @@ -629,6 +657,8 @@ 83CBBA411A601D0F00E9B192 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_STATIC_ANALYZER_MODE = deep; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -636,6 +666,7 @@ ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; + RUN_CLANG_STATIC_ANALYZER = NO; SKIP_INSTALL = YES; }; name = Release; diff --git a/React/Views/RCTActivityIndicatorViewManager.h b/React/Views/RCTActivityIndicatorViewManager.h new file mode 100644 index 00000000000000..cbd6816ae4cddf --- /dev/null +++ b/React/Views/RCTActivityIndicatorViewManager.h @@ -0,0 +1,20 @@ +/** + * 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 RCTConvert (UIActivityIndicatorView) + ++ (UIActivityIndicatorViewStyle)UIActivityIndicatorViewStyle:(id)json; + +@end + +@interface RCTActivityIndicatorViewManager : RCTViewManager + +@end diff --git a/React/Views/RCTUIActivityIndicatorViewManager.m b/React/Views/RCTActivityIndicatorViewManager.m similarity index 52% rename from React/Views/RCTUIActivityIndicatorViewManager.m rename to React/Views/RCTActivityIndicatorViewManager.m index e2c9b3d353bf06..3876400dff3714 100644 --- a/React/Views/RCTUIActivityIndicatorViewManager.m +++ b/React/Views/RCTActivityIndicatorViewManager.m @@ -7,35 +7,37 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTUIActivityIndicatorViewManager.h" +#import "RCTActivityIndicatorViewManager.h" #import "RCTConvert.h" @implementation RCTConvert (UIActivityIndicatorView) +// NOTE: It's pointless to support UIActivityIndicatorViewStyleGray +// as we can set the color to any arbitrary value that we want to + RCT_ENUM_CONVERTER(UIActivityIndicatorViewStyle, (@{ - @"white-large": @(UIActivityIndicatorViewStyleWhiteLarge), - @"large-white": @(UIActivityIndicatorViewStyleWhiteLarge), - @"white": @(UIActivityIndicatorViewStyleWhite), - @"gray": @(UIActivityIndicatorViewStyleGray), + @"large": @(UIActivityIndicatorViewStyleWhiteLarge), + @"small": @(UIActivityIndicatorViewStyleWhite), }), UIActivityIndicatorViewStyleWhiteLarge, integerValue) @end -@implementation RCTUIActivityIndicatorViewManager +@implementation RCTActivityIndicatorViewManager -RCT_EXPORT_MODULE(UIActivityIndicatorViewManager) +RCT_EXPORT_MODULE() - (UIView *)view { return [[UIActivityIndicatorView alloc] init]; } -RCT_EXPORT_VIEW_PROPERTY(activityIndicatorViewStyle, UIActivityIndicatorViewStyle) RCT_EXPORT_VIEW_PROPERTY(color, UIColor) +RCT_EXPORT_VIEW_PROPERTY(hidesWhenStopped, BOOL) +RCT_REMAP_VIEW_PROPERTY(size, activityIndicatorViewStyle, UIActivityIndicatorViewStyle) RCT_CUSTOM_VIEW_PROPERTY(animating, BOOL, UIActivityIndicatorView) { - BOOL animating = json ? [json boolValue] : [defaultView isAnimating]; + BOOL animating = json ? [RCTConvert BOOL:json] : [defaultView isAnimating]; if (animating != [view isAnimating]) { if (animating) { [view startAnimating]; @@ -45,14 +47,4 @@ - (UIView *)view } } -- (NSDictionary *)constantsToExport -{ - return - @{ - @"StyleWhite": @(UIActivityIndicatorViewStyleWhite), - @"StyleWhiteLarge": @(UIActivityIndicatorViewStyleWhiteLarge), - @"StyleGray": @(UIActivityIndicatorViewStyleGray), - }; -} - @end diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h index 89e4c0a8088ff7..d372db56e465d9 100644 --- a/React/Views/RCTMap.h +++ b/React/Views/RCTMap.h @@ -21,7 +21,7 @@ extern const CGFloat RCTMapZoomBoundBuffer; @interface RCTMap: MKMapView @property (nonatomic, assign) BOOL followUserLocation; -@property (nonatomic, assign) BOOL hasStartedLoading; +@property (nonatomic, assign) BOOL hasStartedRendering; @property (nonatomic, assign) CGFloat minDelta; @property (nonatomic, assign) CGFloat maxDelta; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets; diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 5037d3390b1423..40b60508e26da4 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -27,7 +27,7 @@ - (instancetype)init { if ((self = [super init])) { - _hasStartedLoading = NO; + _hasStartedRendering = NO; // Find Apple link label for (UIView *subview in self.subviews) { diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index 8de35141586a29..8d334f60ba6d8c 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -67,15 +67,13 @@ - (void)mapView:(RCTMap *)mapView regionWillChangeAnimated:(BOOL)animated { [self _regionChanged:mapView]; - if (animated) { - mapView.regionChangeObserveTimer = [NSTimer timerWithTimeInterval:RCTMapRegionChangeObserveInterval - target:self - selector:@selector(_onTick:) - userInfo:@{ RCTMapViewKey: mapView } - repeats:YES]; - - [[NSRunLoop mainRunLoop] addTimer:mapView.regionChangeObserveTimer forMode:NSRunLoopCommonModes]; - } + 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 @@ -86,15 +84,15 @@ - (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated [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) { + // started rendering, as these won't represent the final location + if (mapView.hasStartedRendering) { [self _emitRegionChangeEvent:mapView continuous:NO]; }; } -- (void)mapViewWillStartLoadingMap:(RCTMap *)mapView +- (void)mapViewWillStartRenderingMap:(RCTMap *)mapView { - mapView.hasStartedLoading = YES; + mapView.hasStartedRendering = YES; [self _emitRegionChangeEvent:mapView continuous:NO]; } diff --git a/React/Views/RCTNavItem.h b/React/Views/RCTNavItem.h index 5ae874522e8a5d..cd9833a4477183 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -12,11 +12,19 @@ @interface RCTNavItem : UIView @property (nonatomic, copy) NSString *title; +@property (nonatomic, strong) UIImage *leftButtonIcon; +@property (nonatomic, copy) NSString *leftButtonTitle; +@property (nonatomic, strong) UIImage *rightButtonIcon; @property (nonatomic, copy) NSString *rightButtonTitle; +@property (nonatomic, strong) UIImage *backButtonIcon; @property (nonatomic, copy) NSString *backButtonTitle; @property (nonatomic, assign) BOOL navigationBarHidden; -@property (nonatomic, copy) UIColor *tintColor; -@property (nonatomic, copy) UIColor *barTintColor; -@property (nonatomic, copy) UIColor *titleTextColor; +@property (nonatomic, strong) UIColor *tintColor; +@property (nonatomic, strong) UIColor *barTintColor; +@property (nonatomic, strong) UIColor *titleTextColor; + +@property (nonatomic, readonly) UIBarButtonItem *backButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *leftButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *rightButtonItem; @end diff --git a/React/Views/RCTNavItem.m b/React/Views/RCTNavItem.m index 6b1e92f44a3493..56346a363b10ac 100644 --- a/React/Views/RCTNavItem.m +++ b/React/Views/RCTNavItem.m @@ -11,5 +11,104 @@ @implementation RCTNavItem -@end +@synthesize backButtonItem = _backButtonItem; +@synthesize leftButtonItem = _leftButtonItem; +@synthesize rightButtonItem = _rightButtonItem; + +- (void)setBackButtonTitle:(NSString *)backButtonTitle +{ + _backButtonTitle = backButtonTitle; + _backButtonItem = nil; +} + +- (void)setBackButtonIcon:(UIImage *)backButtonIcon +{ + _backButtonIcon = backButtonIcon; + _backButtonItem = nil; +} + +- (UIBarButtonItem *)backButtonItem +{ + if (!_backButtonItem) { + if (_backButtonIcon) { + _backButtonItem = [[UIBarButtonItem alloc] initWithImage:_backButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_backButtonTitle.length) { + _backButtonItem = [[UIBarButtonItem alloc] initWithTitle:_backButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _backButtonItem = nil; + } + } + return _backButtonItem; +} + +- (void)setLeftButtonTitle:(NSString *)leftButtonTitle +{ + _leftButtonTitle = leftButtonTitle; + _leftButtonItem = nil; +} + +- (void)setLeftButtonIcon:(UIImage *)leftButtonIcon +{ + _leftButtonIcon = leftButtonIcon; + _leftButtonIcon = nil; +} +- (UIBarButtonItem *)leftButtonItem +{ + if (!_leftButtonItem) { + if (_leftButtonIcon) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithImage:_leftButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_leftButtonTitle.length) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithTitle:_leftButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _leftButtonItem = nil; + } + } + return _leftButtonItem; +} + +- (void)setRightButtonTitle:(NSString *)rightButtonTitle +{ + _rightButtonTitle = rightButtonTitle; + _rightButtonItem = nil; +} + +- (void)setRightButtonIcon:(UIImage *)rightButtonIcon +{ + _rightButtonIcon = rightButtonIcon; + _rightButtonItem = nil; +} + +- (UIBarButtonItem *)rightButtonItem +{ + if (!_rightButtonItem) { + if (_rightButtonIcon) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithImage:_rightButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_rightButtonTitle.length) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithTitle:_rightButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _rightButtonItem = nil; + } + } + return _rightButtonItem; +} + +@end diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index fc601632f4c4e4..33588c938acdb7 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -21,12 +21,20 @@ - (UIView *)view return [[RCTNavItem alloc] init]; } +RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(title, NSString) -RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString); -RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString); -RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL); -RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor) + +RCT_EXPORT_VIEW_PROPERTY(backButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString) + +RCT_EXPORT_VIEW_PROPERTY(leftButtonTitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(leftButtonIcon, UIImage) + +RCT_EXPORT_VIEW_PROPERTY(rightButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString) @end diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index f3ebb6554a2cb8..57415fbb79a8ce 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -60,7 +60,7 @@ @interface RCTNavigationController : UINavigationController context) { [weakSelf freeLock]; _currentlyTransitioningFrom = 0; _currentlyTransitioningTo = 0; _dummyView.frame = CGRectZero; - [_bridge removeFrameUpdateObserver:self]; + _paused = YES; // Reset the parallel position tracker }]; } @@ -457,6 +461,10 @@ - (void)reactBridgeDidFinishTransaction // --- previously caught up -------- ------- still caught up ---------- viewControllerCount == previousReactCount && currentReactCount == previousReactCount; +BOOL jsGettingtooSlow = + // --- previously not caught up -------- ------- no longer caught up ---------- + viewControllerCount < previousReactCount && currentReactCount < previousReactCount; + BOOL reactPushOne = jsGettingAhead && currentReactCount == previousReactCount + 1; BOOL reactPopN = jsGettingAhead && currentReactCount < previousReactCount; @@ -467,14 +475,15 @@ - (void)reactBridgeDidFinishTransaction if (!(jsGettingAhead || jsCatchingUp || jsMakingNoProgressButNeedsToCatchUp || - jsMakingNoProgressAndDoesntNeedTo)) { + jsMakingNoProgressAndDoesntNeedTo || + jsGettingtooSlow)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } 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 + // 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++) { if (_currentViews[i] != _previousViews[i]) { diff --git a/React/Views/RCTScrollView.h b/React/Views/RCTScrollView.h index f218ea6ead4360..0333a38a74f024 100644 --- a/React/Views/RCTScrollView.h +++ b/React/Views/RCTScrollView.h @@ -45,6 +45,6 @@ @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @property (nonatomic, assign) NSTimeInterval scrollEventThrottle; @property (nonatomic, assign) BOOL centerContent; -@property (nonatomic, copy) NSArray *stickyHeaderIndices; +@property (nonatomic, copy) NSIndexSet *stickyHeaderIndices; @end diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 0376d2a9c0e960..1373d8bd16bbbb 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -28,8 +28,8 @@ */ @interface RCTCustomScrollView : UIScrollView -@property (nonatomic, copy, readwrite) NSArray *stickyHeaderIndices; -@property (nonatomic, readwrite, assign) BOOL centerContent; +@property (nonatomic, copy) NSIndexSet *stickyHeaderIndices; +@property (nonatomic, assign) BOOL centerContent; @end @@ -155,97 +155,72 @@ - (void)setContentOffset:(CGPoint)contentOffset [super setContentOffset:contentOffset]; } -- (void)setBounds:(CGRect)bounds -{ - [super setBounds:bounds]; - [self dockClosestSectionHeader]; -} - - (void)dockClosestSectionHeader { UIView *contentView = [self contentView]; - if (_stickyHeaderIndices.count == 0 || !contentView) { - return; - } - - // find the section header that needs to be docked - NSInteger firstIndexInView = [[_stickyHeaderIndices firstObject] integerValue] + 1; - CGRect scrollBounds = self.bounds; - scrollBounds.origin.x += self.contentInset.left; - scrollBounds.origin.y += self.contentInset.top; - - NSInteger i = 0; - for (UIView *subview in contentView.subviews) { - CGRect rowFrame = [RCTCustomScrollView _calculateUntransformedFrame:subview]; - if (CGRectIntersectsRect(scrollBounds, rowFrame)) { - firstIndexInView = i; - break; + CGFloat scrollTop = self.bounds.origin.y + self.contentInset.top; + + // Find the section headers that need to be docked + __block UIView *previousHeader = nil; + __block UIView *currentHeader = nil; + __block UIView *nextHeader = nil; + NSInteger subviewCount = contentView.reactSubviews.count; + [_stickyHeaderIndices enumerateIndexesWithOptions:0 usingBlock:^(NSUInteger idx, BOOL *stop) { + + if (idx >= subviewCount) { + RCTLogError(@"Sticky header index %zd was outside the range {0, %zd}", idx, subviewCount); + return; } - i++; - } - NSInteger stickyHeaderii = 0; - for (NSNumber *stickyHeaderI in _stickyHeaderIndices) { - if ([stickyHeaderI integerValue] > firstIndexInView) { - break; - } - stickyHeaderii++; - } - stickyHeaderii = MAX(0, stickyHeaderii - 1); - // Set up transforms for the various section headers - NSInteger currentlyDockedIndex = [_stickyHeaderIndices[stickyHeaderii] integerValue]; - NSInteger previouslyDockedIndex = stickyHeaderii > 0 ? [_stickyHeaderIndices[stickyHeaderii-1] integerValue] : -1; - NSInteger nextDockedIndex = (stickyHeaderii < _stickyHeaderIndices.count - 1) ? - [_stickyHeaderIndices[stickyHeaderii + 1] integerValue] : -1; + UIView *header = contentView.reactSubviews[idx]; + + // If nextHeader not yet found, search for docked headers + if (!nextHeader) { + CGFloat height = header.bounds.size.height; + CGFloat top = header.center.y - height * header.layer.anchorPoint.y; + if (top > scrollTop) { + nextHeader = header; + } else { + previousHeader = currentHeader; + currentHeader = header; + } + } - UIView *currentHeader = contentView.subviews[currentlyDockedIndex]; - UIView *previousHeader = previouslyDockedIndex >= 0 ? contentView.subviews[previouslyDockedIndex] : nil; - CGRect curFrame = [RCTCustomScrollView _calculateUntransformedFrame:currentHeader]; + // Reset transforms for header views + header.transform = CGAffineTransformIdentity; + header.layer.zPosition = ZINDEX_DEFAULT; - if (previousHeader) { - // the previous header is offset to sit right above the currentlyDockedHeader's initial position - // (so it scrolls away nicely once the currentHeader locks into position) - CGRect previousFrame = [RCTCustomScrollView _calculateUntransformedFrame:previousHeader]; - CGFloat yOffset = curFrame.origin.y - previousFrame.origin.y - previousFrame.size.height; - previousHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); - } + }]; - UIView *nextHeader = nextDockedIndex >= 0 ? contentView.subviews[nextDockedIndex] : nil; - CGRect nextFrame = [RCTCustomScrollView _calculateUntransformedFrame:nextHeader]; - - if (curFrame.origin.y < scrollBounds.origin.y) { - // scrolled off (or being scrolled off) the top of the screen - CGFloat yOffset = 0; - if (nextHeader && nextFrame.origin.y < scrollBounds.origin.y + curFrame.size.height) { - // next frame is bumping me off if scrolling down (or i'm bumping the next one off if scrolling up) - yOffset = nextFrame.origin.y - curFrame.origin.y - curFrame.size.height; - } else { - // standard sticky header position - yOffset = scrollBounds.origin.y - curFrame.origin.y; - } - currentHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); - currentHeader.layer.zPosition = ZINDEX_STICKY_HEADER; - } else { - // i'm the current header but in the viewport, so just scroll in normal position - currentHeader.transform = CGAffineTransformIdentity; - currentHeader.layer.zPosition = ZINDEX_DEFAULT; + // If no docked header, bail out + if (!currentHeader) { + return; } - // in our setup, 'next header' will always just scroll with the page + // Adjust current header to hug the top of the screen + CGFloat currentFrameHeight = currentHeader.bounds.size.height; + CGFloat currentFrameTop = currentHeader.center.y - currentFrameHeight * currentHeader.layer.anchorPoint.y; + CGFloat yOffset = scrollTop - currentFrameTop; if (nextHeader) { - nextHeader.transform = CGAffineTransformIdentity; - nextHeader.layer.zPosition = ZINDEX_DEFAULT; + // The next header nudges the current header out of the way when it reaches + // the top of the screen + CGFloat nextFrameHeight = nextHeader.bounds.size.height; + CGFloat nextFrameTop = nextHeader.center.y - nextFrameHeight * nextHeader.layer.anchorPoint.y; + CGFloat overlap = currentFrameHeight - (nextFrameTop - scrollTop); + yOffset -= MAX(0, overlap); } -} + currentHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); + currentHeader.layer.zPosition = ZINDEX_STICKY_HEADER; -+ (CGRect)_calculateUntransformedFrame:(UIView *)view -{ - CGRect frame = CGRectNull; - if (view) { - frame.size = view.bounds.size; - frame.origin = CGPointMake(view.layer.position.x - view.bounds.size.width * view.layer.anchorPoint.x, view.layer.position.y - view.bounds.size.height * view.layer.anchorPoint.y); + if (previousHeader) { + // The previous header sits right above the currentHeader's initial position + // so it scrolls away nicely once the currentHeader has locked into place + CGFloat previousFrameHeight = previousHeader.bounds.size.height; + CGFloat targetCenter = currentFrameTop - previousFrameHeight * (1.0 - previousHeader.layer.anchorPoint.y); + yOffset = targetCenter - previousHeader.center.y; + previousHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); + previousHeader.layer.zPosition = ZINDEX_STICKY_HEADER; } - return frame; } @end @@ -312,7 +287,7 @@ - (void)setCenterContent:(BOOL)centerContent _scrollView.centerContent = centerContent; } -- (void)setStickyHeaderIndices:(NSArray *)headerIndices +- (void)setStickyHeaderIndices:(NSIndexSet *)headerIndices { RCTAssert(_scrollView.contentSize.width <= self.frame.size.width, @"sticky headers are not supported with horizontal scrolled views"); @@ -390,6 +365,7 @@ - (void)delegateMethod:(UIScrollView *)scrollView \ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + [_scrollView dockClosestSectionHeader]; [self updateClippedSubviews]; NSTimeInterval now = CACurrentMediaTime(); diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index ededf5f7fc22d4..8441de74d29b1c 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -41,7 +41,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(scrollsToTop, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL) -RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSNumberArray) +RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSIndexSet) RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat) RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) diff --git a/React/Views/RCTSegmentedControl.h b/React/Views/RCTSegmentedControl.h new file mode 100644 index 00000000000000..8e6e1255ef0492 --- /dev/null +++ b/React/Views/RCTSegmentedControl.h @@ -0,0 +1,20 @@ +// +// RCTSegmentedControl.h +// React +// +// Created by Clay Allsopp on 3/31/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +@class RCTEventDispatcher; + +@interface RCTSegmentedControl : UISegmentedControl + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, copy) NSArray *values; +@property (nonatomic, assign) NSInteger selectedIndex; + +@end diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m new file mode 100644 index 00000000000000..59e4cfb86b5fa4 --- /dev/null +++ b/React/Views/RCTSegmentedControl.m @@ -0,0 +1,57 @@ +// +// RCTSegmentedControl.m +// React +// +// Created by Clay Allsopp on 3/31/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTSegmentedControl.h" + +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" + +@implementation RCTSegmentedControl +{ + RCTEventDispatcher *_eventDispatcher; +} + +- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _eventDispatcher = eventDispatcher; + _selectedIndex = self.selectedSegmentIndex; + [self addTarget:self action:@selector(onChange:) + forControlEvents:UIControlEventValueChanged]; + } + return self; +} + +- (void)setValues:(NSArray *)values +{ + _values = [values copy]; + [self removeAllSegments]; + for (NSString *value in values) { + [self insertSegmentWithTitle:value atIndex:self.numberOfSegments animated:NO]; + } + super.selectedSegmentIndex = _selectedIndex; +} + +- (void)setSelectedIndex:(NSInteger)selectedIndex +{ + _selectedIndex = selectedIndex; + super.selectedSegmentIndex = selectedIndex; +} + +- (void)onChange:(UISegmentedControl *)sender +{ + NSDictionary *event = @{ + @"target": self.reactTag, + @"value": [self titleForSegmentAtIndex:sender.selectedSegmentIndex], + @"selectedSegmentIndex": @(sender.selectedSegmentIndex) + }; + [_eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} + +@end diff --git a/React/Views/RCTSegmentedControlManager.h b/React/Views/RCTSegmentedControlManager.h new file mode 100644 index 00000000000000..03647c72edb9bf --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.h @@ -0,0 +1,14 @@ +/** + * 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 RCTSegmentedControlManager : RCTViewManager + +@end diff --git a/React/Views/RCTSegmentedControlManager.m b/React/Views/RCTSegmentedControlManager.m new file mode 100644 index 00000000000000..d7e1156ff00725 --- /dev/null +++ b/React/Views/RCTSegmentedControlManager.m @@ -0,0 +1,39 @@ +/** + * 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 "RCTSegmentedControlManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTSegmentedControl.h" + +@implementation RCTSegmentedControlManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[RCTSegmentedControl alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(values, NSStringArray) +RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL) +RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) + +- (NSDictionary *)constantsToExport +{ + RCTSegmentedControl *view = [[RCTSegmentedControl alloc] init]; + return @{ + @"ComponentHeight": @(view.intrinsicContentSize.height), + }; +} + +@end diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 6efb0c1d118967..83350ac469cb7e 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -9,8 +9,7 @@ #import -#import "../Layout/Layout.h" - +#import "Layout.h" #import "RCTViewNodeProtocol.h" @class RCTSparseArray; @@ -42,6 +41,7 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); @property (nonatomic, assign) BOOL isBGColorExplicitlySet; // Used to propagate to children @property (nonatomic, strong) UIColor *backgroundColor; // Used to propagate to children @property (nonatomic, assign) RCTUpdateLifecycle layoutLifecycle; +@property (nonatomic, assign) BOOL hasOnLayout; /** * isNewView - Used to track the first time the view is introduced into the hierarchy. It is initialized YES, then is diff --git a/React/Views/RCTSlider.h b/React/Views/RCTSlider.h new file mode 100644 index 00000000000000..916419a29d3c90 --- /dev/null +++ b/React/Views/RCTSlider.h @@ -0,0 +1,14 @@ +/** + * 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 + +@interface RCTSlider : UISlider + +@end diff --git a/React/Views/RCTSlider.m b/React/Views/RCTSlider.m new file mode 100644 index 00000000000000..04e8d841e75f6f --- /dev/null +++ b/React/Views/RCTSlider.m @@ -0,0 +1,35 @@ +/** + * 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 "RCTSlider.h" + +@implementation RCTSlider +{ + float _unclippedValue; +} + +- (void)setValue:(float)value +{ + _unclippedValue = value; + super.value = value; +} + +- (void)setMinimumValue:(float)minimumValue +{ + super.minimumValue = minimumValue; + super.value = _unclippedValue; +} + +- (void)setMaximumValue:(float)maximumValue +{ + super.maximumValue = maximumValue; + super.value = _unclippedValue; +} + +@end diff --git a/React/Views/RCTSliderManager.m b/React/Views/RCTSliderManager.m index 58b763b9244a4f..f57e1f36209a2a 100644 --- a/React/Views/RCTSliderManager.m +++ b/React/Views/RCTSliderManager.m @@ -11,6 +11,7 @@ #import "RCTBridge.h" #import "RCTEventDispatcher.h" +#import "RCTSlider.h" #import "UIView+React.h" @implementation RCTSliderManager @@ -19,36 +20,37 @@ @implementation RCTSliderManager - (UIView *)view { - UISlider *slider = [[UISlider alloc] init]; + RCTSlider *slider = [[RCTSlider alloc] init]; [slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; [slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpInside]; return slider; } -- (void)sliderValueChanged:(UISlider *)sender +static void RCTSendSliderEvent(RCTSliderManager *self, UISlider *sender, BOOL continuous) { NSDictionary *event = @{ @"target": sender.reactTag, @"value": @(sender.value), - @"continuous": @YES, + @"continuous": @(continuous), }; [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; } -- (void)sliderTouchEnd:(UISlider *)sender +- (void)sliderValueChanged:(UISlider *)sender { - NSDictionary *event = @{ - @"target": sender.reactTag, - @"value": @(sender.value), - @"continuous": @NO, - }; + RCTSendSliderEvent(self, sender, YES); +} - [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; +- (void)sliderTouchEnd:(UISlider *)sender +{ + RCTSendSliderEvent(self, sender, NO); } RCT_EXPORT_VIEW_PROPERTY(value, float); RCT_EXPORT_VIEW_PROPERTY(minimumValue, float); RCT_EXPORT_VIEW_PROPERTY(maximumValue, float); +RCT_EXPORT_VIEW_PROPERTY(minimumTrackTintColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(maximumTrackTintColor, UIColor); @end diff --git a/React/Views/RCTSwitchManager.m b/React/Views/RCTSwitchManager.m index eb0d626e62e161..c60d83e81ebb2f 100644 --- a/React/Views/RCTSwitchManager.m +++ b/React/Views/RCTSwitchManager.m @@ -42,7 +42,14 @@ - (void)onChange:(RCTSwitch *)sender RCT_EXPORT_VIEW_PROPERTY(onTintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(thumbTintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(on, BOOL); -RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL); +RCT_REMAP_VIEW_PROPERTY(value, on, BOOL); +RCT_CUSTOM_VIEW_PROPERTY(disabled, BOOL, RCTSwitch) +{ + if (json) { + view.enabled = !([RCTConvert BOOL:json]); + } else { + view.enabled = defaultView.enabled; + } +} @end diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 11ad47e320da75..8a7fb4a43278d1 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -18,24 +18,6 @@ #import "RCTWrapperViewController.h" #import "UIView+React.h" -@interface RKCustomTabBarController : UITabBarController - -@end - -@implementation RKCustomTabBarController - -@synthesize currentTopLayoutGuide = _currentTopLayoutGuide; -@synthesize currentBottomLayoutGuide = _currentBottomLayoutGuide; - -- (void)viewWillLayoutSubviews -{ - [super viewWillLayoutSubviews]; - _currentTopLayoutGuide = self.topLayoutGuide; - _currentBottomLayoutGuide = self.bottomLayoutGuide; -} - -@end - @interface RCTTabBar() @end @@ -53,7 +35,7 @@ - (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = eventDispatcher; _tabViews = [[NSMutableArray alloc] init]; - _tabController = [[RKCustomTabBarController alloc] init]; + _tabController = [[UITabBarController alloc] init]; _tabController.delegate = self; [self addSubview:_tabController.view]; } diff --git a/React/Views/RCTTabBarItemManager.m b/React/Views/RCTTabBarItemManager.m index 8bbe782b7346ce..cdfa8669ce98d5 100644 --- a/React/Views/RCTTabBarItemManager.m +++ b/React/Views/RCTTabBarItemManager.m @@ -24,7 +24,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(selected, BOOL); RCT_EXPORT_VIEW_PROPERTY(icon, NSString); RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, UIImage); -RCT_REMAP_VIEW_PROPERTY(badgeValue, barItem.badgeValue, NSString); +RCT_REMAP_VIEW_PROPERTY(badge, barItem.badgeValue, NSString); RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) { view.barItem.title = json ? [RCTConvert NSString:json] : defaultView.barItem.title; diff --git a/React/Views/RCTTextField.h b/React/Views/RCTTextField.h index bd1be9c187bd39..ef0a07887faabc 100644 --- a/React/Views/RCTTextField.h +++ b/React/Views/RCTTextField.h @@ -17,6 +17,7 @@ @property (nonatomic, assign) BOOL autoCorrect; @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, strong) UIColor *placeholderTextColor; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/React/Views/RCTTextField.m b/React/Views/RCTTextField.m index 35eb84d9653dde..12d52b1b8b7561 100644 --- a/React/Views/RCTTextField.m +++ b/React/Views/RCTTextField.m @@ -42,6 +42,30 @@ - (void)setText:(NSString *)text } } +static void RCTUpdatePlaceholder(RCTTextField *self) +{ + if (self.placeholder.length > 0 && self.placeholderTextColor) { + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder + attributes:@{ + NSForegroundColorAttributeName : self.placeholderTextColor + }]; + } else if (self.placeholder.length) { + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder]; + } +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + _placeholderTextColor = placeholderTextColor; + RCTUpdatePlaceholder(self); +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + super.placeholder = placeholder; + RCTUpdatePlaceholder(self); +} + - (NSArray *)reactSubviews { // TODO: do we support subviews of textfield in React? diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m index 6e78d86a3b1c39..ff401a719c7c7a 100644 --- a/React/Views/RCTTextFieldManager.m +++ b/React/Views/RCTTextFieldManager.m @@ -10,7 +10,6 @@ #import "RCTTextFieldManager.h" #import "RCTBridge.h" -#import "RCTConvert.h" #import "RCTShadowView.h" #import "RCTSparseArray.h" #import "RCTTextField.h" @@ -28,6 +27,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) +RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(text, NSString) RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode) RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL) diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 73fe2c7cbb0338..1a4bcb40007e75 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -13,13 +13,6 @@ #import "RCTPointerEvents.h" -typedef NS_ENUM(NSInteger, RCTBorderSide) { - RCTBorderSideTop, - RCTBorderSideRight, - RCTBorderSideBottom, - RCTBorderSideLeft -}; - @protocol RCTAutoInsetsProtocol; @interface RCTView : UIView diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index d40798302b2a94..c0786b5abfa863 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -12,9 +12,10 @@ #import "RCTAutoInsetsProtocol.h" #import "RCTConvert.h" #import "RCTLog.h" +#import "RCTUtils.h" #import "UIView+React.h" -static const RCTBorderSide RCTBorderSideCount = 4; +static void *RCTViewCornerRadiusKVOContext = &RCTViewCornerRadiusKVOContext; static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) { @@ -30,6 +31,10 @@ return nil; } +static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]); +static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform); +static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise); + @implementation UIView (RCTViewUnmounting) - (void)react_remountAllSubviews @@ -107,8 +112,39 @@ - (UIView *)react_findClipView @implementation RCTView { NSMutableArray *_reactSubviews; - CAShapeLayer *_borderLayers[RCTBorderSideCount]; - CGFloat _borderWidths[RCTBorderSideCount]; + UIColor *_backgroundColor; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + _borderWidth = -1; + _borderTopWidth = -1; + _borderRightWidth = -1; + _borderBottomWidth = -1; + _borderLeftWidth = -1; + + _backgroundColor = [super backgroundColor]; + [super setBackgroundColor:[UIColor clearColor]]; + + [self.layer addObserver:self forKeyPath:@"cornerRadius" options:0 context:RCTViewCornerRadiusKVOContext]; + } + + return self; +} + +- (void)dealloc +{ + [self.layer removeObserver:self forKeyPath:@"cornerRadius" context:RCTViewCornerRadiusKVOContext]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == RCTViewCornerRadiusKVOContext) { + [self.layer setNeedsDisplay]; + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } } - (NSString *)accessibilityLabel @@ -381,189 +417,353 @@ - (void)layoutSubviews if (_reactSubviews) { [self updateClippedSubviews]; } - - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - if (_borderLayers[side]) [self updatePathForShapeLayerForSide:side]; - } } -- (void)layoutSublayersOfLayer:(CALayer *)layer +#pragma mark - Borders + +- (UIColor *)backgroundColor { - [super layoutSublayersOfLayer:layer]; + return _backgroundColor; +} - const CGRect bounds = layer.bounds; - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - _borderLayers[side].frame = bounds; +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + if ([_backgroundColor isEqual:backgroundColor]) { + return; } + _backgroundColor = backgroundColor; + [self.layer setNeedsDisplay]; } -- (BOOL)getTrapezoidPoints:(CGPoint[4])outPoints forSide:(RCTBorderSide)side +- (UIImage *)generateBorderImage:(out CGRect *)contentsCenter { - const CGRect bounds = self.layer.bounds; - const CGFloat minX = CGRectGetMinX(bounds); - const CGFloat maxX = CGRectGetMaxX(bounds); - const CGFloat minY = CGRectGetMinY(bounds); - const CGFloat maxY = CGRectGetMaxY(bounds); + const CGFloat maxRadius = MIN(self.bounds.size.height, self.bounds.size.width) / 2.0; + const CGFloat radius = MAX(0, MIN(self.layer.cornerRadius, maxRadius)); -#define BW(SIDE) [self borderWidthForSide:RCTBorderSide##SIDE] + const CGFloat borderWidth = MAX(0, _borderWidth); + const CGFloat topWidth = _borderTopWidth >= 0 ? _borderTopWidth : borderWidth; + const CGFloat rightWidth = _borderRightWidth >= 0 ? _borderRightWidth : borderWidth; + const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth; + const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth; - switch (side) { - case RCTBorderSideRight: - outPoints[0] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom)); - outPoints[1] = CGPointMake(maxX - BW(Right), minY + BW(Top)); - outPoints[2] = CGPointMake(maxX, minY); - outPoints[3] = CGPointMake(maxX, maxY); - break; - case RCTBorderSideBottom: - outPoints[0] = CGPointMake(minX + BW(Left), maxY - BW(Bottom)); - outPoints[1] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom)); - outPoints[2] = CGPointMake(maxX, maxY); - outPoints[3] = CGPointMake(minX, maxY); - break; - case RCTBorderSideLeft: - outPoints[0] = CGPointMake(minX + BW(Left), minY + BW(Top)); - outPoints[1] = CGPointMake(minX + BW(Left), maxY - BW(Bottom)); - outPoints[2] = CGPointMake(minX, maxY); - outPoints[3] = CGPointMake(minX, minY); - break; - case RCTBorderSideTop: - outPoints[0] = CGPointMake(maxX - BW(Right), minY + BW(Top)); - outPoints[1] = CGPointMake(minX + BW(Left), minY + BW(Top)); - outPoints[2] = CGPointMake(minX, minY); - outPoints[3] = CGPointMake(maxX, minY); - break; - } + const CGFloat topRadius = MAX(0, radius - topWidth); + const CGFloat rightRadius = MAX(0, radius - rightWidth); + const CGFloat bottomRadius = MAX(0, radius - bottomWidth); + const CGFloat leftRadius = MAX(0, radius - leftWidth); - return YES; -} + const UIEdgeInsets edgeInsets = UIEdgeInsetsMake(topWidth + topRadius, leftWidth + leftRadius, bottomWidth + bottomRadius, rightWidth + rightRadius); + const CGSize size = CGSizeMake(edgeInsets.left + 1 + edgeInsets.right, edgeInsets.top + 1 + edgeInsets.bottom); -- (CAShapeLayer *)createShapeLayerIfNotExistsForSide:(RCTBorderSide)side -{ - CAShapeLayer *borderLayer = _borderLayers[side]; - if (!borderLayer) { - borderLayer = [CAShapeLayer layer]; - borderLayer.fillColor = self.layer.borderColor; - [self.layer addSublayer:borderLayer]; - _borderLayers[side] = borderLayer; - } - return borderLayer; -} + UIScreen *screen = self.window.screen ?: [UIScreen mainScreen]; + UIGraphicsBeginImageContextWithOptions(size, NO, screen.scale * 2); -- (void)updatePathForShapeLayerForSide:(RCTBorderSide)side -{ - CAShapeLayer *borderLayer = [self createShapeLayerIfNotExistsForSide:side]; + CGContextRef ctx = UIGraphicsGetCurrentContext(); + const CGRect rect = {CGPointZero, size}; + CGPathRef path = CGPathCreateWithRoundedRect(rect, radius, radius, NULL); - CGPoint trapezoidPoints[4]; - [self getTrapezoidPoints:trapezoidPoints forSide:side]; + if (_backgroundColor) { + CGContextSaveGState(ctx); - CGMutablePathRef path = CGPathCreateMutable(); - CGPathAddLines(path, NULL, trapezoidPoints, 4); - CGPathCloseSubpath(path); - borderLayer.path = path; + CGContextAddPath(ctx, path); + CGContextSetFillColorWithColor(ctx, _backgroundColor.CGColor); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } + + CGContextAddPath(ctx, path); CGPathRelease(path); -} -- (void)updateBorderLayers -{ - BOOL widthsAndColorsSame = YES; - CGFloat width = _borderWidths[0]; - CGColorRef color = _borderLayers[0].fillColor; - for (RCTBorderSide side = 1; side < RCTBorderSideCount; side++) { - CAShapeLayer *layer = _borderLayers[side]; - if (_borderWidths[side] != width || (layer && !CGColorEqualToColor(layer.fillColor, color))) { - widthsAndColorsSame = NO; - break; + if (radius > 0 && topWidth > 0 && rightWidth > 0 && bottomWidth > 0 && leftWidth > 0) { + const UIEdgeInsets insetEdgeInsets = UIEdgeInsetsMake(topWidth, leftWidth, bottomWidth, rightWidth); + const CGRect insetRect = UIEdgeInsetsInsetRect(rect, insetEdgeInsets); + CGPathRef insetPath = RCTPathCreateWithRoundedRect(insetRect, leftRadius, topRadius, rightRadius, topRadius, leftRadius, bottomRadius, rightRadius, bottomRadius, NULL); + CGContextAddPath(ctx, insetPath); + CGPathRelease(insetPath); + } + + CGContextEOClip(ctx); + + BOOL hasEqualColor = !_borderTopColor && !_borderRightColor && !_borderBottomColor && !_borderLeftColor; + BOOL hasEqualBorder = _borderWidth >= 0 && _borderTopWidth < 0 && _borderRightWidth < 0 && _borderBottomWidth < 0 && _borderLeftWidth < 0; + if (radius <= 0 && hasEqualBorder && hasEqualColor) { + CGContextSetStrokeColorWithColor(ctx, _borderColor); + CGContextSetLineWidth(ctx, 2 * _borderWidth); + CGContextClipToRect(ctx, rect); + CGContextStrokeRect(ctx, rect); + } else if (radius <= 0 && hasEqualColor) { + CGContextSetFillColorWithColor(ctx, _borderColor); + CGContextAddRect(ctx, rect); + const CGRect insetRect = UIEdgeInsetsInsetRect(rect, edgeInsets); + CGContextAddRect(ctx, insetRect); + CGContextEOFillPath(ctx); + } else { + BOOL didSet = NO; + CGPoint topLeft; + if (topRadius > 0 && leftRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, topWidth, 2 * leftRadius, 2 * topRadius), CGPointMake(0, 0), CGPointMake(leftWidth, topWidth), points); + if (!isnan(points[1].x) && !isnan(points[1].y)) { + topLeft = points[1]; + didSet = YES; + } } - } - if (widthsAndColorsSame) { - // Set main layer border - if (width) { - _borderWidth = self.layer.borderWidth = width; + if (!didSet) { + topLeft = CGPointMake(leftWidth, topWidth); } - if (color) { - self.layer.borderColor = color; + + didSet = NO; + CGPoint bottomLeft; + if (bottomRadius > 0 && leftRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, (size.height - bottomWidth) - 2 * bottomRadius, 2 * leftRadius, 2 * bottomRadius), CGPointMake(0, size.height), CGPointMake(leftWidth, size.height - bottomWidth), points); + if (!isnan(points[1].x) && !isnan(points[1].y)) { + bottomLeft = points[1]; + didSet = YES; + } } - // Remove border layers - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - [_borderLayers[side] removeFromSuperlayer]; - _borderLayers[side] = nil; + if (!didSet) { + bottomLeft = CGPointMake(leftWidth, size.height - bottomWidth); } - } else { + didSet = NO; + CGPoint topRight; + if (topRadius > 0 && rightRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, topWidth, 2 * rightRadius, 2 * topRadius), CGPointMake(size.width, 0), CGPointMake(size.width - rightWidth, topWidth), points); + if (!isnan(points[0].x) && !isnan(points[0].y)) { + topRight = points[0]; + didSet = YES; + } + } + + if (!didSet) { + topRight = CGPointMake(size.width - rightWidth, topWidth); + } + + didSet = NO; + CGPoint bottomRight; + if (bottomRadius > 0 && rightRadius > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, (size.height - bottomWidth) - 2 * bottomRadius, 2 * rightRadius, 2 * bottomRadius), CGPointMake(size.width, size.height), CGPointMake(size.width - rightWidth, size.height - bottomWidth), points); + if (!isnan(points[0].x) && !isnan(points[0].y)) { + bottomRight = points[0]; + didSet = YES; + } + } - // Clear main layer border - self.layer.borderWidth = 0; + if (!didSet) { + bottomRight = CGPointMake(size.width - rightWidth, size.height - bottomWidth); + } + + // RIGHT + if (rightWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(size.width, 0), + topRight, + bottomRight, + CGPointMake(size.width, size.height), + }; + + CGContextSetFillColorWithColor(ctx, _borderRightColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } - // Set up border layers - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - [self updatePathForShapeLayerForSide:side]; + // BOTTOM + if (bottomWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(0, size.height), + bottomLeft, + bottomRight, + CGPointMake(size.width, size.height), + }; + + CGContextSetFillColorWithColor(ctx, _borderBottomColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } + + // LEFT + if (leftWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(0, 0), + topLeft, + bottomLeft, + CGPointMake(0, size.height), + }; + + CGContextSetFillColorWithColor(ctx, _borderLeftColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); + } + + // TOP + if (topWidth > 0) { + CGContextSaveGState(ctx); + + const CGPoint points[] = { + CGPointMake(0, 0), + topLeft, + topRight, + CGPointMake(size.width, 0), + }; + + CGContextSetFillColorWithColor(ctx, _borderTopColor ?: _borderColor); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + + CGContextRestoreGState(ctx); } } -} -- (CGFloat)borderWidthForSide:(RCTBorderSide)side -{ - return _borderWidths[side] ?: _borderWidth; + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + *contentsCenter = CGRectMake(edgeInsets.left / size.width, edgeInsets.top / size.height, 1.0 / size.width, 1.0 / size.height); + return [image resizableImageWithCapInsets:edgeInsets]; } -- (void)setBorderWidth:(CGFloat)width forSide:(RCTBorderSide)side +- (void)displayLayer:(CALayer *)layer { - _borderWidths[side] = width; - [self updateBorderLayers]; -} + CGRect contentsCenter; + UIImage *image = [self generateBorderImage:&contentsCenter]; -#define BORDER_WIDTH(SIDE) \ -- (CGFloat)border##SIDE##Width { return [self borderWidthForSide:RCTBorderSide##SIDE]; } \ -- (void)setBorder##SIDE##Width:(CGFloat)width { [self setBorderWidth:width forSide:RCTBorderSide##SIDE]; } + if (RCTRunningInTestEnvironment()) { + const CGSize size = self.bounds.size; + UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); + [image drawInRect:(CGRect){CGPointZero, size}]; + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); -BORDER_WIDTH(Top) -BORDER_WIDTH(Right) -BORDER_WIDTH(Bottom) -BORDER_WIDTH(Left) + contentsCenter = CGRectMake(0, 0, 1, 1); + } -- (CGColorRef)borderColorForSide:(RCTBorderSide)side -{ - return _borderLayers[side].fillColor ?: self.layer.borderColor; + layer.contents = (id)image.CGImage; + layer.contentsCenter = contentsCenter; + layer.contentsScale = image.scale; + layer.magnificationFilter = kCAFilterNearest; } -- (void)setBorderColor:(CGColorRef)color forSide:(RCTBorderSide)side -{ - [self createShapeLayerIfNotExistsForSide:side].fillColor = color; - [self updateBorderLayers]; -} +#pragma mark Border Color -#define BORDER_COLOR(SIDE) \ -- (CGColorRef)border##SIDE##Color { return [self borderColorForSide:RCTBorderSide##SIDE]; } \ -- (void)setBorder##SIDE##Color:(CGColorRef)color { [self setBorderColor:color forSide:RCTBorderSide##SIDE]; } +#define setBorderColor(side) \ + - (void)setBorder##side##Color:(CGColorRef)border##side##Color \ + { \ + if (CGColorEqualToColor(_border##side##Color, border##side##Color)) { \ + return; \ + } \ + _border##side##Color = border##side##Color; \ + [self.layer setNeedsDisplay]; \ + } -BORDER_COLOR(Top) -BORDER_COLOR(Right) -BORDER_COLOR(Bottom) -BORDER_COLOR(Left) +setBorderColor() +setBorderColor(Top) +setBorderColor(Right) +setBorderColor(Bottom) +setBorderColor(Left) -- (void)setBorderWidth:(CGFloat)borderWidth -{ - _borderWidth = borderWidth; - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - _borderWidths[side] = borderWidth; +#pragma mark - Border Width + +#define setBorderWidth(side) \ + - (void)setBorder##side##Width:(CGFloat)border##side##Width \ + { \ + if (_border##side##Width == border##side##Width) { \ + return; \ + } \ + _border##side##Width = border##side##Width; \ + [self.layer setNeedsDisplay]; \ } - [self updateBorderLayers]; -} -- (void)setBorderColor:(CGColorRef)borderColor +setBorderWidth() +setBorderWidth(Top) +setBorderWidth(Right) +setBorderWidth(Bottom) +setBorderWidth(Left) + +@end + +static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise) { - self.layer.borderColor = borderColor; - for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { - _borderLayers[side].fillColor = borderColor; + CGFloat xScale = 1, yScale = 1, radius = 0; + if (xRadius != 0) { + xScale = 1; + yScale = yRadius / xRadius; + radius = xRadius; + } else if (yRadius != 0) { + xScale = xRadius / yRadius; + yScale = 1; + radius = yRadius; } - [self updateBorderLayers]; + + CGAffineTransform t = CGAffineTransformMakeTranslation(x, y); + t = CGAffineTransformScale(t, xScale, yScale); + if (m != NULL) { + t = CGAffineTransformConcat(t, *m); + } + + CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise); } -- (CGColorRef)borderColor +static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform) { - return self.layer.borderColor; + const CGFloat minX = CGRectGetMinX(rect); + const CGFloat minY = CGRectGetMinY(rect); + const CGFloat maxX = CGRectGetMaxX(rect); + const CGFloat maxY = CGRectGetMaxY(rect); + + CGMutablePathRef path = CGPathCreateMutable(); + RCTPathAddEllipticArc(path, transform, minX + topLeftRadiusX, minY + topLeftRadiusY, topLeftRadiusX, topLeftRadiusY, M_PI, 3 * M_PI_2, false); + RCTPathAddEllipticArc(path, transform, maxX - topRightRadiusX, minY + topRightRadiusY, topRightRadiusX, topRightRadiusY, 3 * M_PI_2, 0, false); + RCTPathAddEllipticArc(path, transform, maxX - bottomRightRadiusX, maxY - bottomRightRadiusY, bottomRightRadiusX, bottomRightRadiusY, 0, M_PI_2, false); + RCTPathAddEllipticArc(path, transform, minX + bottomLeftRadiusX, maxY - bottomLeftRadiusY, bottomLeftRadiusX, bottomLeftRadiusY, M_PI_2, M_PI, false); + CGPathCloseSubpath(path); + return path; } -@end +static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]) +{ + const CGFloat ellipseCenterX = CGRectGetMidX(ellipseBoundingRect); + const CGFloat ellipseCenterY = CGRectGetMidY(ellipseBoundingRect); + + // ellipseBoundingRect.origin.x -= ellipseCenterX; + // ellipseBoundingRect.origin.y -= ellipseCenterY; + + p1.x -= ellipseCenterX; + p1.y -= ellipseCenterY; + + p2.x -= ellipseCenterX; + p2.y -= ellipseCenterY; + + const CGFloat m = (p2.y - p1.y) / (p2.x - p1.x); + const CGFloat a = ellipseBoundingRect.size.width / 2; + const CGFloat b = ellipseBoundingRect.size.height / 2; + const CGFloat c = p1.y - m * p1.x; + const CGFloat A = (b * b + a * a * m * m); + const CGFloat B = 2 * a * a * c * m; + const CGFloat D = sqrt((a * a * (b * b - c * c)) / A + pow(B / (2 * A), 2)); + + const CGFloat x_ = -B / (2 * A); + const CGFloat x1 = x_ + D; + const CGFloat x2 = x_ - D; + const CGFloat y1 = m * x1 + c; + const CGFloat y2 = m * x2 + c; + + intersections[0] = CGPointMake(x1 + ellipseCenterX, y1 + ellipseCenterY); + intersections[1] = CGPointMake(x2 + ellipseCenterX, y2 + ellipseCenterY); + return YES; +} diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index 74c7bbd8ce7a17..77dc6669708900 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -9,9 +9,9 @@ #import -#import "../Base/RCTBridgeModule.h" -#import "../Base/RCTConvert.h" -#import "../Base/RCTLog.h" +#import "RCTBridgeModule.h" +#import "RCTConvert.h" +#import "RCTLog.h" @class RCTBridge; @class RCTEventDispatcher; @@ -109,8 +109,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * within the view or shadowView. */ #define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) \ -RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ -- (void)set_##name:(id)json forView:(id)view withDefaultView:(id)defaultView { \ +RCT_CUSTOM_VIEW_PROPERTY(name, type, UIView) { \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ RCTLogError(@"%@ does not have setter for `%s` property", [view class], #name); \ @@ -118,8 +117,7 @@ RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ } #define RCT_REMAP_SHADOW_PROPERTY(name, keyPath, type) \ -RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ -- (void)set_##name:(id)json forShadowView:(id)view withDefaultView:(id)defaultView { \ +RCT_CUSTOM_SHADOW_PROPERTY(name, type, RCTShadowView) { \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ RCTLogError(@"%@ does not have setter for `%s` property", [view class], #name); \ @@ -132,11 +130,11 @@ RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ * refer to "json", "view" and "defaultView" to implement the required logic. */ #define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) \ -RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ ++ (NSString *)getPropConfigView_##name { return @#type; } \ - (void)set_##name:(id)json forView:(viewClass *)view withDefaultView:(viewClass *)defaultView #define RCT_CUSTOM_SHADOW_PROPERTY(name, type, viewClass) \ -RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ ++ (NSString *)getPropConfigShadow_##name { return @#type; } \ - (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView /** @@ -164,17 +162,4 @@ RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ [self set_##newName:json forView:view withDefaultView:defaultView]; \ } -/** - * PROP_CONFIG macros should only be paired with property setters. - */ -#define RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ -+ (NSDictionary *)getPropConfigView_##name { \ - return @{@"name": @#name, @"type": @#type}; \ -} - -#define RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ -+ (NSDictionary *)getPropConfigShadow_##name { \ - return @{@"name": @#name, @"type": @#type}; \ -} - @end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 3c848537454872..4dfb296fda35de 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -14,6 +14,7 @@ #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTShadowView.h" +#import "RCTUIManager.h" #import "RCTUtils.h" #import "RCTView.h" @@ -23,6 +24,11 @@ @implementation RCTViewManager RCT_EXPORT_MODULE() +- (dispatch_queue_t)methodQueue +{ + return [_bridge.uiManager methodQueue]; +} + - (UIView *)view { return [[RCTView alloc] init]; @@ -158,7 +164,9 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) RCT_EXPORT_SHADOW_PROPERTY(borderRightWidth, CGFloat); RCT_EXPORT_SHADOW_PROPERTY(borderBottomWidth, CGFloat); RCT_EXPORT_SHADOW_PROPERTY(borderLeftWidth, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(borderWidth, CGFloat); +RCT_CUSTOM_SHADOW_PROPERTY(borderWidth, CGFloat, RCTShadowView) { + [view setBorderWidth:[RCTConvert CGFloat:json]]; +} RCT_EXPORT_SHADOW_PROPERTY(marginTop, CGFloat); RCT_EXPORT_SHADOW_PROPERTY(marginRight, CGFloat); @@ -190,4 +198,6 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; } +RCT_REMAP_SHADOW_PROPERTY(onLayout, hasOnLayout, BOOL) + @end diff --git a/React/Views/RCTViewNodeProtocol.h b/React/Views/RCTViewNodeProtocol.h index 691aaaba15af3a..e78cc2ce7b26fc 100644 --- a/React/Views/RCTViewNodeProtocol.h +++ b/React/Views/RCTViewNodeProtocol.h @@ -35,7 +35,7 @@ @end // TODO: this is kinda dumb - let's come up with a -// better way of identifying root react views please! +// better way of identifying root React views please! static inline BOOL RCTIsReactRootView(NSNumber *reactTag) { return reactTag.integerValue % 10 == 1; } diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index bb9bb2acfb6b44..56cda0c88b6971 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -31,6 +31,7 @@ @implementation RCTWebView - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super initWithFrame:CGRectZero])) { + super.backgroundColor = [UIColor clearColor]; _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; _eventDispatcher = eventDispatcher; @@ -95,6 +96,18 @@ - (void)setContentInset:(UIEdgeInsets)contentInset updateOffset:NO]; } +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); + self.opaque = _webView.opaque = (alpha == 1.0); + _webView.backgroundColor = backgroundColor; +} + +- (UIColor *)backgroundColor +{ + return _webView.backgroundColor; +} + - (NSMutableDictionary *)baseEvent { NSURL *url = _webView.request.URL; diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index e25a7da68b98b2..015285871ed4e0 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -25,6 +25,8 @@ - (UIView *)view RCT_REMAP_VIEW_PROPERTY(url, URL, NSURL); RCT_REMAP_VIEW_PROPERTY(html, HTML, NSString); +RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL); +RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets); RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL); RCT_EXPORT_VIEW_PROPERTY(shouldInjectAJAXHandler, BOOL); diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index f9f35163448f78..400ce5fab6a6f2 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -64,7 +64,6 @@ - (void)viewWillAppear:(BOOL)animated // TODO: find a way to make this less-tightly coupled to navigation controller if ([self.parentViewController isKindOfClass:[UINavigationController class]]) { - [self.navigationController setNavigationBarHidden:_navItem.navigationBarHidden animated:animated]; @@ -73,36 +72,23 @@ - (void)viewWillAppear:(BOOL)animated return; } - self.navigationItem.title = _navItem.title; - UINavigationBar *bar = self.navigationController.navigationBar; - if (_navItem.barTintColor) { - bar.barTintColor = _navItem.barTintColor; - } - if (_navItem.tintColor) { - BOOL canSetTintColor = _navItem.barTintColor == nil; - if (canSetTintColor) { - bar.tintColor = _navItem.tintColor; - } - } + bar.barTintColor = _navItem.barTintColor; + bar.tintColor = _navItem.tintColor; if (_navItem.titleTextColor) { [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; } - if (_navItem.rightButtonTitle.length > 0) { - self.navigationItem.rightBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle - style:UIBarButtonItemStyleDone - target:self - action:@selector(handleNavRightButtonTapped)]; + UINavigationItem *item = self.navigationItem; + item.title = _navItem.title; + item.backBarButtonItem = _navItem.backButtonItem; + if ((item.leftBarButtonItem = _navItem.leftButtonItem)) { + item.leftBarButtonItem.target = self; + item.leftBarButtonItem.action = @selector(handleNavLeftButtonTapped); } - - if (_navItem.backButtonTitle.length > 0) { - self.navigationItem.backBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + if ((item.rightBarButtonItem = _navItem.rightButtonItem)) { + item.rightBarButtonItem.target = self; + item.rightBarButtonItem.action = @selector(handleNavRightButtonTapped); } } } @@ -117,6 +103,12 @@ - (void)loadView self.view = _wrapperView; } +- (void)handleNavLeftButtonTapped +{ + [_eventDispatcher sendInputEventWithName:@"topNavLeftButtonTap" + body:@{@"target":_navItem.reactTag}]; +} + - (void)handleNavRightButtonTapped { [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap" diff --git a/docs/EmbeddedApp.md b/docs/EmbeddedApp.md index 3580cf7be368e4..e86421e6bcffbc 100644 --- a/docs/EmbeddedApp.md +++ b/docs/EmbeddedApp.md @@ -111,7 +111,7 @@ Here I disabled **AutoLayout** for simplicity. In real production world, you sho ## Add RCTRootView To Container View -Ready for the most interesting part? Now we shall create the `RCTRootView`, where your React Native app lives in. +Ready for the most interesting part? Now we shall create the `RCTRootView`, where your React Native app lives. In `ReactView.m`, we need to first initiate `RCTRootView` with the URI of your `index.ios.bundle`. `index.ios.bundle` will be created by packager and served by React Native server, which will be discussed later on. diff --git a/docs/LinkingLibraries.md b/docs/LinkingLibraries.md index c90768c94cfc4c..560ed6b590c757 100644 --- a/docs/LinkingLibraries.md +++ b/docs/LinkingLibraries.md @@ -37,7 +37,7 @@ on Xcode); Click on your main project file (the one that represents the `.xcodeproj`) select `Build Phases` and drag the static library from the `Products` folder -insed the Library you are importing to `Link Binary With Libraries` +inside the Library you are importing to `Link Binary With Libraries` ![](/react-native/img/AddToBuildPhases.png) diff --git a/docs/NativeComponentsIOS.md b/docs/NativeComponentsIOS.md index 14a186fe4829fa..9f70e188cbd680 100644 --- a/docs/NativeComponentsIOS.md +++ b/docs/NativeComponentsIOS.md @@ -18,6 +18,7 @@ Let's say we want to add an interactive Map to our app - might as well use [`MKM Native views are created and manipulated by subclasses of `RCTViewManager`. These subclasses are similar in function to view controllers, but are essentially singletons - only one instance of each is created by the bridge. They vend native views to the `RCTUIManager`, which delegates back to them to set and update the properties of the views as necessary. The `RCTViewManager`s are also typically the delegates for the views, sending events back to JavaScript via the bridge. Vending a view is simple: + - Create the basic subclass. - Add the `RCT_EXPORT_MODULE()` marker macro. - Implement the `-(UIView *)view` method @@ -57,7 +58,7 @@ This is now a fully-functioning native map view component in JavaScript, complet ## Properties -The first thing we can do to make this component more usable is to bridge over some native properties. Let's say we want to be able to disable pitch control specify the visible region. Disabling pitch is a simple boolean, so we just add this one line: +The first thing we can do to make this component more usable is to bridge over some native properties. Let's say we want to be able to disable pitch control and specify the visible region. Disabling pitch is a simple boolean, so we just add this one line: ```objective-c // RCTMapManager.m @@ -86,7 +87,7 @@ class MapView extends React.Component { } } -var RCTMap= requireNativeComponent('RCTMap', MapView); +var RCTMap = requireNativeComponent('RCTMap', MapView); MapView.propTypes = { /** @@ -96,7 +97,7 @@ MapView.propTypes = { * angle is ignored and the map is always displayed as if the user * is looking straight down onto it. */ - pitchEnabled = React.PropTypes.bool, + pitchEnabled: React.PropTypes.bool, }; module.exports = MapView; @@ -170,7 +171,7 @@ MapView.propTypes = { * angle is ignored and the map is always displayed as if the user * is looking straight down onto it. */ - pitchEnabled = React.PropTypes.bool, + pitchEnabled: React.PropTypes.bool, /** * The region to be displayed by the map. @@ -245,7 +246,7 @@ RCT_EXPORT_MODULE() { MKCoordinateRegion region = mapView.region; NSDictionary *event = @{ - @"target": [mapView reactTag], + @"target": mapView.reactTag, @"region": @{ @"latitude": @(region.center.latitude), @"longitude": @(region.center.longitude), diff --git a/docs/NativeModulesIOS.md b/docs/NativeModulesIOS.md index 51fbb22a30403b..24821ff88d6c59 100644 --- a/docs/NativeModulesIOS.md +++ b/docs/NativeModulesIOS.md @@ -7,11 +7,11 @@ permalink: docs/nativemodulesios.html next: nativecomponentsios --- -Sometimes an app needs access to platform API, and React Native doesn't have a corresponding module yet. Maybe you want to reuse some existing Objective-C or C++ code without having to reimplement it in JavaScript, or write some high performance, multi-threaded code such as for image processing, a database, or any number of advanced extensions. +Sometimes an app needs access to platform API, and React Native doesn't have a corresponding module yet. Maybe you want to reuse some existing Objective-C, Swift or C++ code without having to reimplement it in JavaScript, or write some high performance, multi-threaded code such as for image processing, a database, or any number of advanced extensions. We designed React Native such that it is possible for you to write real native code and have access to the full power of the platform. This is a more advanced feature and we don't expect it to be part of the usual development process, however it is essential that it exists. If React Native doesn't support a native feature that you need, you should be able to build it yourself. -This is a more advanced guide that shows how to build a native module. It assumes the reader knows Objective-C (Swift is not supported yet) and core libraries (Foundation, UIKit). +This is a more advanced guide that shows how to build a native module. It assumes the reader knows Objective-C or Swift and core libraries (Foundation, UIKit). ## iOS Calendar Module Example @@ -177,7 +177,6 @@ If you want to pass error-like objects to JavaScript, use `RCTMakeError` from [` The native module should not have any assumptions about what thread it is being called on. React Native invokes native modules methods on a separate serial GCD queue, but this is an implementation detail and might change. The `- (dispatch_queue_t)methodQueue` method allows the native module to specify which queue its methods should be run on. For example, if it needs to use a main-thread-only iOS API, it should specify this via: - ```objective-c - (dispatch_queue_t)methodQueue { @@ -194,6 +193,24 @@ Similarly, if an operation may take a long time to complete, the native module s } ``` +The specified `methodQueue` will be shared by all of the methods in your module. If *just one* of your methods is long-running (or needs to be run on a different queue than the others for some reason), you can use `dispatch_async` inside the method to perform that particular method's code on another queue, without affecting the others: + +```objective-c +RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback) +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Call long-running code on background thread + ... + // You can invoke callback from any thread/queue + callback(@[...]); + }); +} +``` + +> **NOTE**: Sharing dispatch queues between modules +> +> The `methodQueue` method will be called once when the module is initialized, and then retained by the bridge, so there is no need to retain the queue yourself, unless you wish to make use of it within your module. However, if you wish to share the same queue between multiple modules then you will need to ensure that you retain and return the same queue instance for each of them; merely returning a queue of the same name for each won't work. + ## Exporting Constants A native module can export constants that are immediately available to JavaScript at runtime. This is useful for communicating static data that would otherwise require a round-trip through the bridge. @@ -248,3 +265,39 @@ var subscription = DeviceEventEmitter.addListener( subscription.remove(); ``` For more examples of sending events to JavaScript, see [`RCTLocationObserver`](https://github.com/facebook/react-native/blob/master/Libraries/Geolocation/RCTLocationObserver.m). + +## Exporting Swift + +Swift doesn't have support for macros so exposing it to React Native requires a bit more setup but works relatively the same. + +Let's say we have the same `CalendarManager` but as a Swift class: + +```swift +// CalendarManager.swift + +@objc(CalendarManager) +class CalendarManager: NSObject { + + @objc func addEvent(name: String, location: String, date: NSNumber) -> Void { + // Date is ready to use! + } + +} +``` + +> **NOTE**: It is important to use the @objc modifiers to ensure the class and functions are exported properly to the Objective-C runtime. + +Then create a private implementation file that will register the required information with the React Native bridge: + +```objc +// CalendarManagerBridge.m +#import "RCTBridgeModule.h" + +@interface RCT_EXTERN_MODULE(CalendarManager, NSObject) + +RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSNumber *)date) + +@end +``` + +You can also use `RCT_EXTERN_REMAP_MODULE` and `RCT_EXTERN_REMAP_METHOD` to alter the JavaScript name of the module or methods you are exporting. For more information see [`RCTBridgeModule`](https://github.com/facebook/react-native/blob/master/React/Base/RCTBridgeModule.h). diff --git a/docs/Style.md b/docs/Style.md index cb33f9bca4e062..ec6571bcd5e601 100644 --- a/docs/Style.md +++ b/docs/Style.md @@ -95,7 +95,8 @@ var List = React.createClass({ You can checkout latest support of CSS Properties in following Links. -- [View Properties](http://facebook.github.io/react-native/docs/view.html#style) -- [Image Properties](http://facebook.github.io/react-native/docs/image.html#style) -- [Text Properties](http://facebook.github.io/react-native/docs/text.html#style) -- [Flex Properties](http://facebook.github.io/react-native/docs/flexbox.html#content) +- [View Properties](/react-native/docs/view.html#style) +- [Image Properties](/react-native/docs/image.html#style) +- [Text Properties](/react-native/docs/text.html#style) +- [Flex Properties](/react-native/docs/flexbox.html#content) +- [Transform Properties](/react-native/docs/transforms.html#content) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 7cb77c08449734..b8d7b2aabaccc8 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -287,7 +287,7 @@ Let's now modify this application to render all of this data in a `ListView` com Why is a `ListView` better than just rendering all of these elements or putting them in a `ScrollView`? Despite React being fast, rendering a possibly infinite list of elements could be slow. `ListView` schedules rendering of views so that you only display the ones on screen and those already rendered but off screen are removed from the native view hierarchy. -First thing's first: add the `ListView` require to the top of the file. +First things first: add the `ListView` require to the top of the file. ```javascript var { diff --git a/package.json b/package.json index 3ed7cdc579eab6..4b76d688abfa21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native", - "version": "0.4.0", + "version": "0.4.2", "description": "A framework for building native apps using React", "repository": { "type": "git", @@ -23,6 +23,7 @@ "main": "Libraries/react-native/react-native.js", "files": [ "React", + "React.podspec", "Examples/SampleApp", "Libraries", "packager", @@ -47,18 +48,19 @@ "chalk": "^1.0.0", "connect": "2.8.3", "debug": "~2.1.0", + "graceful-fs": "^3.0.6", "image-size": "0.3.5", "joi": "~5.1.0", - "jstransform": "10.1.0", + "jstransform": "11.0.1", "module-deps": "3.5.6", "optimist": "0.6.1", "promise": "^7.0.0", "react-timer-mixin": "^0.13.1", - "react-tools": "0.13.1", + "react-tools": "0.13.2", "rebound": "^0.0.12", - "sane": "1.0.3", + "sane": "^1.1.2", "source-map": "0.1.31", - "stacktrace-parser": "0.1.1", + "stacktrace-parser": "git://github.com/frantic/stacktrace-parser.git#493c5e5638", "uglify-js": "~2.4.16", "underscore": "1.7.0", "worker-farm": "1.1.0", diff --git a/packager/debugger.html b/packager/debugger.html index d0d4aba54bfaf9..d72e40ead23ba4 100644 --- a/packager/debugger.html +++ b/packager/debugger.html @@ -41,13 +41,13 @@ window.localStorage.setItem('sessionID', message.id); window.location.reload(); }, - 'executeApplicationScript:sourceURL:onComplete:': function(message, sendReply) { + 'executeApplicationScript': function(message, sendReply) { for (var key in message.inject) { window[key] = JSON.parse(message.inject[key]); } loadScript(message.url, sendReply.bind(null, null)); }, - 'executeJSCall:method:arguments:callback:': function(message, sendReply) { + 'executeJSCall': function(message, sendReply) { var returnValue = [[], [], [], [], []]; try { if (window && window.require) { diff --git a/packager/getFlowTypeCheckMiddleware.js b/packager/getFlowTypeCheckMiddleware.js index 312da45e21c1f5..c7f3e2b1c6a2bd 100644 --- a/packager/getFlowTypeCheckMiddleware.js +++ b/packager/getFlowTypeCheckMiddleware.js @@ -10,12 +10,15 @@ var chalk = require('chalk'); var exec = require('child_process').exec; +var Activity = require('./react-packager/src/Activity'); var hasWarned = {}; +var DISABLE_FLOW_CHECK = true; // temporarily disable while we figure out versioning issues. function getFlowTypeCheckMiddleware(options) { return function(req, res, next) { - if (options.skipflow) { + var isBundle = req.url.indexOf('.bundle') !== -1; + if (DISABLE_FLOW_CHECK || options.skipflow || !isBundle) { return next(); } if (options.flowroot || options.projectRoots.length === 1) { @@ -44,20 +47,10 @@ function getFlowTypeCheckMiddleware(options) { function doFlowTypecheck(res, flowroot, next) { var flowCmd = 'cd "' + flowroot + '" && flow --json --timeout 20'; - var start = Date.now(); - // Log start message if flow is slow to let user know something is happening. - var flowSlow = setTimeout( - function() { - console.log(chalk.gray('flow: Running static typechecks.')); - }, - 500 - ); + var eventId = Activity.startEvent('flow static typechecks'); exec(flowCmd, function(flowError, stdout, stderr) { - clearTimeout(flowSlow); + Activity.endEvent(eventId); if (!flowError) { - console.log(chalk.gray( - 'flow: Typechecks passed (' + (Date.now() - start) + 'ms).') - ); return next(); } else { try { diff --git a/packager/launchChromeDevTools.applescript b/packager/launchChromeDevTools.applescript index 1fe6f4b075f435..4b718f5bd48c78 100755 --- a/packager/launchChromeDevTools.applescript +++ b/packager/launchChromeDevTools.applescript @@ -10,7 +10,7 @@ on run argv set theURL to item 1 of argv - tell application "Google Chrome" + tell application "Chrome" activate if (count every window) = 0 then diff --git a/packager/launchEditor.js b/packager/launchEditor.js index cf89ed4f04b779..b572b5cbd7c314 100644 --- a/packager/launchEditor.js +++ b/packager/launchEditor.js @@ -8,23 +8,21 @@ */ 'use strict'; +var chalk = require('chalk'); var fs = require('fs'); -var spawn = require('child_process').spawn; - -var firstLaunch = true; - -function guessEditor() { - if (firstLaunch) { - console.log('When you see Red Box with stack trace, you can click any ' + - 'stack frame to jump to the source file. The packager will launch your ' + - 'editor of choice. It will first look at REACT_EDITOR environment ' + - 'variable, then at EDITOR. To set it up, you can add something like ' + - 'REACT_EDITOR=atom to your .bashrc.'); - firstLaunch = false; - } - - var editor = process.env.REACT_EDITOR || process.env.EDITOR || 'subl'; - return editor; +var exec = require('child_process').exec; + +function printInstructions(title) { + console.log([ + '', + chalk.bgBlue.white.bold(' ' + title + ' '), + ' When you see Red Box with stack trace, you can click any ', + ' stack frame to jump to the source file. The packager will launch your ', + ' editor of choice. It will first look at REACT_EDITOR environment ', + ' variable, then at EDITOR. To set it up, you can add something like ', + ' REACT_EDITOR=atom to your .bashrc.', + '' + ].join('\n')); } function launchEditor(fileName, lineNumber) { @@ -37,9 +35,18 @@ function launchEditor(fileName, lineNumber) { argument += ':' + lineNumber; } - var editor = guessEditor(); - console.log('Opening ' + fileName + ' with ' + editor); - spawn(editor, [argument], { stdio: ['pipe', 'pipe', process.stderr] }); + var editor = process.env.REACT_EDITOR || process.env.EDITOR; + if (editor) { + console.log('Opening ' + chalk.underline(fileName) + ' with ' + chalk.bold(editor)); + exec(editor + ' ' + argument, function(error) { + if (error) { + console.log(chalk.red(error.message)); + printInstructions('How to fix'); + } + }); + } else { + printInstructions('PRO TIP'); + } } module.exports = launchEditor; diff --git a/packager/packager.js b/packager/packager.js index 9069c6f1bea8eb..f8d7372f81ed65 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -200,6 +200,7 @@ function getAppMiddleware(options) { cacheVersion: '2', transformModulePath: require.resolve('./transformer.js'), assetRoots: options.assetRoots, + assetExts: ['png', 'jpeg', 'jpg'] }); } @@ -212,7 +213,8 @@ function runServer( .use(openStackFrameInEditor) .use(getDevToolsLauncher(options)) .use(statusPageMiddleware) - .use(getFlowTypeCheckMiddleware(options)) + // Temporarily disable flow check until it's more stable + //.use(getFlowTypeCheckMiddleware(options)) .use(getAppMiddleware(options)); options.projectRoots.forEach(function(root) { diff --git a/packager/react-packager/index.js b/packager/react-packager/index.js index 3a70659cf09553..a7b6264b7b740f 100644 --- a/packager/react-packager/index.js +++ b/packager/react-packager/index.js @@ -8,6 +8,8 @@ */ 'use strict'; +useGracefulFs(); + var Activity = require('./src/Activity'); var Server = require('./src/Server'); @@ -45,3 +47,16 @@ exports.getDependencies = function(options, main) { return r.dependencies; }); }; + +function useGracefulFs() { + var fs = require('fs'); + var gracefulFs = require('graceful-fs'); + + // A bit sneaky but it's not straightforward to update all the + // modules we depend on. + Object.keys(fs).forEach(function(method) { + if (typeof fs[method] === 'function' && gracefulFs[method]) { + fs[method] = gracefulFs[method]; + } + }); +} diff --git a/packager/react-packager/src/Activity/__mocks__/chalk.js b/packager/react-packager/src/Activity/__mocks__/chalk.js new file mode 100644 index 00000000000000..2981f979d90dd5 --- /dev/null +++ b/packager/react-packager/src/Activity/__mocks__/chalk.js @@ -0,0 +1,13 @@ +/** + * 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. + */ +'use strict'; + +module.exports = { + dim: function(s) { return s; }, +}; diff --git a/packager/react-packager/src/Activity/index.js b/packager/react-packager/src/Activity/index.js index 05285d0fc98c8a..8e593f9ffe7d09 100644 --- a/packager/react-packager/src/Activity/index.js +++ b/packager/react-packager/src/Activity/index.js @@ -8,6 +8,8 @@ */ 'use strict'; +var chalk = require('chalk'); + var COLLECTION_PERIOD = 1000; var _endedEvents = Object.create(null); @@ -132,22 +134,22 @@ function _writeAction(action) { switch (action.action) { case 'startEvent': - console.log( + console.log(chalk.dim( '[' + fmtTime + '] ' + ' ' + action.eventName + data - ); + )); break; case 'endEvent': var startAction = _eventStarts[action.eventId]; var startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : ''; - console.log( + console.log(chalk.dim( '[' + fmtTime + '] ' + ' ' + startAction.eventName + - '(' + (action.tstamp - startAction.tstamp) + 'ms)' + + ' (' + (action.tstamp - startAction.tstamp) + 'ms)' + startData - ); + )); delete _eventStarts[action.eventId]; break; diff --git a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js index eede72c0ca3cbb..ba804b5f19e004 100644 --- a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -1,72 +1,82 @@ 'use strict'; jest - .autoMockOff() - .mock('../../lib/declareOpts') - .mock('fs'); + .dontMock('path') + .dontMock('../../lib/getAssetDataFromName') + .dontMock('../'); -var fs = require('fs'); -var AssetServer = require('../'); var Promise = require('bluebird'); describe('AssetServer', function() { - pit('should work for the simple case', function() { - var server = new AssetServer({ - projectRoots: ['/root'], - assetExts: ['png'], - }); + var AssetServer; + var crypto; + var fs; + + beforeEach(function() { + AssetServer = require('../'); + crypto = require('crypto'); + fs = require('fs'); + }); + + describe('assetServer.get', function() { + pit('should work for the simple case', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); - fs.__setMockFilesystem({ - 'root': { - imgs: { - 'b.png': 'b image', - 'b@2x.png': 'b2 image', + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.png': 'b image', + 'b@2x.png': 'b2 image', + } } - } - }); + }); - return Promise.all([ - server.get('imgs/b.png'), - server.get('imgs/b@1x.png'), - ]).then(function(resp) { - resp.forEach(function(data) { - expect(data).toBe('b image'); + return Promise.all([ + server.get('imgs/b.png'), + server.get('imgs/b@1x.png'), + ]).then(function(resp) { + resp.forEach(function(data) { + expect(data).toBe('b image'); + }); }); }); - }); - pit.only('should pick the bigger one', function() { - var server = new AssetServer({ - projectRoots: ['/root'], - assetExts: ['png'], - }); + pit('should pick the bigger one', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); - fs.__setMockFilesystem({ - 'root': { - imgs: { - 'b@1x.png': 'b1 image', - 'b@2x.png': 'b2 image', - 'b@4x.png': 'b4 image', - 'b@4.5x.png': 'b4.5 image', + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b@1x.png': 'b1 image', + 'b@2x.png': 'b2 image', + 'b@4x.png': 'b4 image', + 'b@4.5x.png': 'b4.5 image', + } } - } - }); + }); - return server.get('imgs/b@3x.png').then(function(data) { - expect(data).toBe('b4 image'); + return server.get('imgs/b@3x.png').then(function(data) { + expect(data).toBe('b4 image'); + }); }); - }); - pit('should support multiple project roots', function() { - var server = new AssetServer({ - projectRoots: ['/root'], - assetExts: ['png'], - }); + pit('should support multiple project roots', function() { + var server = new AssetServer({ + projectRoots: ['/root', '/root2'], + assetExts: ['png'], + }); - fs.__setMockFilesystem({ - 'root': { - imgs: { - 'b.png': 'b image', + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.png': 'b image', + }, }, 'root2': { 'newImages': { @@ -75,11 +85,53 @@ describe('AssetServer', function() { }, }, }, - } + }); + + return server.get('newImages/imgs/b.png').then(function(data) { + expect(data).toBe('b1 image'); + }); }); + }); + + describe('assetSerer.getAssetData', function() { + pit('should get assetData', function() { + var hash = { + update: jest.genMockFn(), + digest: jest.genMockFn(), + }; - return server.get('newImages/imgs/b.png').then(function(data) { - expect(data).toBe('b1 image'); + hash.digest.mockImpl(function() { + return 'wow such hash'; + }); + crypto.createHash.mockImpl(function() { + return hash; + }); + + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b@1x.png': 'b1 image', + 'b@2x.png': 'b2 image', + 'b@4x.png': 'b4 image', + 'b@4.5x.png': 'b4.5 image', + } + } + }); + + return server.getAssetData('imgs/b.png').then(function(data) { + expect(hash.update.mock.calls.length).toBe(4); + expect(data).toEqual({ + type: 'png', + name: 'b', + scales: [1, 2, 4, 4.5], + hash: 'wow such hash', + }); + }); }); }); }); diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index bdabafff4a177a..6f07dd01d38f89 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -9,10 +9,11 @@ 'use strict'; var declareOpts = require('../lib/declareOpts'); -var extractAssetResolution = require('../lib/extractAssetResolution'); +var getAssetDataFromName = require('../lib/getAssetDataFromName'); var path = require('path'); var Promise = require('bluebird'); var fs = require('fs'); +var crypto = require('crypto'); var lstat = Promise.promisify(fs.lstat); var readDir = Promise.promisify(fs.readdir); @@ -44,11 +45,11 @@ function AssetServer(options) { * * 1. We first parse the directory of the asset * 2. We check to find a matching directory in one of the project roots - * 3. We then build a map of all assets and their resolutions in this directory + * 3. We then build a map of all assets and their scales in this directory * 4. Then pick the closest resolution (rounding up) to the requested one */ -AssetServer.prototype.get = function(assetPath) { +AssetServer.prototype._getAssetRecord = function(assetPath) { var filename = path.basename(assetPath); return findRoot( @@ -60,13 +61,7 @@ AssetServer.prototype.get = function(assetPath) { readDir(dir), ]; }).spread(function(dir, files) { - // Easy case. File exactly what the client requested. - var index = files.indexOf(filename); - if (index > -1) { - return readFile(path.join(dir, filename)); - } - - var assetData = extractAssetResolution(filename); + var assetData = getAssetDataFromName(filename); var map = buildAssetMap(dir, files); var record = map[assetData.assetName]; @@ -74,8 +69,15 @@ AssetServer.prototype.get = function(assetPath) { throw new Error('Asset not found'); } - for (var i = 0; i < record.resolutions.length; i++) { - if (record.resolutions[i] >= assetData.resolution) { + return record; + }); +}; + +AssetServer.prototype.get = function(assetPath) { + var assetData = getAssetDataFromName(assetPath); + return this._getAssetRecord(assetPath).then(function(record) { + for (var i = 0; i < record.scales.length; i++) { + if (record.scales[i] >= assetData.resolution) { return readFile(record.files[i]); } } @@ -84,6 +86,33 @@ AssetServer.prototype.get = function(assetPath) { }); }; +AssetServer.prototype.getAssetData = function(assetPath) { + var nameData = getAssetDataFromName(assetPath); + var data = { + name: nameData.name, + type: 'png', + }; + + return this._getAssetRecord(assetPath).then(function(record) { + data.scales = record.scales; + + return Promise.all( + record.files.map(function(file) { + return lstat(file); + }) + ); + }).then(function(stats) { + var hash = crypto.createHash('md5'); + + stats.forEach(function(stat) { + hash.update(stat.mtime.getTime().toString()); + }); + + data.hash = hash.digest('hex'); + return data; + }); +}; + function findRoot(roots, dir) { return Promise.some( roots.map(function(root) { @@ -105,26 +134,26 @@ function findRoot(roots, dir) { } function buildAssetMap(dir, files) { - var assets = files.map(extractAssetResolution); + var assets = files.map(getAssetDataFromName); var map = Object.create(null); assets.forEach(function(asset, i) { var file = files[i]; var record = map[asset.assetName]; if (!record) { record = map[asset.assetName] = { - resolutions: [], + scales: [], files: [], }; } var insertIndex; - var length = record.resolutions.length; + var length = record.scales.length; for (insertIndex = 0; insertIndex < length; insertIndex++) { - if (asset.resolution < record.resolutions[insertIndex]) { + if (asset.resolution < record.scales[insertIndex]) { break; } } - record.resolutions.splice(insertIndex, 0, asset.resolution); + record.scales.splice(insertIndex, 0, asset.resolution); record.files.splice(insertIndex, 0, path.join(dir, file)); }); diff --git a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js index 3cdfa1c6a6e2bb..90db1c4ade5f54 100644 --- a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js +++ b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js @@ -45,6 +45,8 @@ function ModuleDescriptor(fields) { this.altId = fields.altId; + this.isJSON = fields.isJSON; + this._fields = fields; } diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index 98ae7eb73f8f73..9cb08122c23fcb 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -14,7 +14,7 @@ jest .dontMock('absolute-path') .dontMock('../docblock') .dontMock('../../replacePatterns') - .dontMock('../../../../lib/extractAssetResolution') + .dontMock('../../../../lib/getAssetDataFromName') .setMock('../../../ModuleDescriptor', function(data) {return data;}); describe('DependencyGraph', function() { @@ -101,6 +101,46 @@ describe('DependencyGraph', function() { }); }); + pit('should get json dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'package' + }), + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./a.json")' + ].join('\n'), + 'a.json': JSON.stringify({}), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { + id: 'index', + altId: 'package/index', + path: '/root/index.js', + dependencies: ['./a.json'] + }, + { + id: 'package/a.json', + isJSON: true, + path: '/root/a.json', + dependencies: [] + }, + ]); + }); + }); + pit('should get dependencies with deprecated assets', function() { var root = '/root'; fs.__setMockFilesystem({ @@ -129,7 +169,8 @@ describe('DependencyGraph', function() { { id: 'image!a', path: '/root/imgs/a.png', dependencies: [], - isAsset_DEPRECATED: true + isAsset_DEPRECATED: true, + resolution: 1, }, ]); }); @@ -288,7 +329,8 @@ describe('DependencyGraph', function() { id: 'image!a', path: '/root/imgs/a.png', dependencies: [], - isAsset_DEPRECATED: true + isAsset_DEPRECATED: true, + resolution: 1, }, ]); }); @@ -1350,6 +1392,7 @@ describe('DependencyGraph', function() { path: '/root/foo.png', dependencies: [], isAsset_DEPRECATED: true, + resolution: 1, }, ]); }); diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index 9257d788b1baa9..08a4b513bf0023 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -18,7 +18,7 @@ var isAbsolutePath = require('absolute-path'); var debug = require('debug')('DependecyGraph'); var util = require('util'); var declareOpts = require('../../../lib/declareOpts'); -var extractAssetResolution = require('../../../lib/extractAssetResolution'); +var getAssetDataFromName = require('../../../lib/getAssetDataFromName'); var readFile = Promise.promisify(fs.readFile); var readDir = Promise.promisify(fs.readdir); @@ -66,7 +66,7 @@ function DependecyGraph(options) { this._debugUpdateEvents = []; this._moduleExtPattern = new RegExp( - '\.(' + ['js'].concat(this._assetExts).join('|') + ')$' + '\.(' + ['js', 'json'].concat(this._assetExts).join('|') + ')$' ); // Kick off the search process to precompute the dependency graph. @@ -259,7 +259,7 @@ DependecyGraph.prototype.resolveDependency = function( } // JS modules can be required without extensios. - if (!this._isFileAsset(modulePath)) { + if (!this._isFileAsset(modulePath) && !modulePath.match(/\.json$/)) { modulePath = withExtJs(modulePath); } @@ -422,7 +422,7 @@ DependecyGraph.prototype._processModule = function(modulePath) { var module; if (this._assetExts.indexOf(extname(modulePath)) > -1) { - var assetData = extractAssetResolution(this._lookupName(modulePath)); + var assetData = getAssetDataFromName(this._lookupName(modulePath)); moduleData.id = assetData.assetName; moduleData.resolution = assetData.resolution; moduleData.isAsset = true; @@ -432,13 +432,23 @@ DependecyGraph.prototype._processModule = function(modulePath) { return Promise.resolve(module); } + if (extname(modulePath) === 'json') { + moduleData.id = this._lookupName(modulePath); + moduleData.isJSON = true; + moduleData.dependencies = []; + module = new ModuleDescriptor(moduleData); + this._updateGraphWithModule(module); + return Promise.resolve(module); + } + var self = this; return readFile(modulePath, 'utf8') .then(function(content) { var moduleDocBlock = docblock.parseAsObject(content); if (moduleDocBlock.providesModule || moduleDocBlock.provides) { - moduleData.id = - moduleDocBlock.providesModule || moduleDocBlock.provides; + moduleData.id = /^(\S*)/.exec( + moduleDocBlock.providesModule || moduleDocBlock.provides + )[1]; // Incase someone wants to require this module via // packageName/path/to/module @@ -641,6 +651,7 @@ DependecyGraph.prototype._processAsset_DEPRECATED = function(file) { path: path.resolve(file), isAsset_DEPRECATED: true, dependencies: [], + resolution: getAssetDataFromName(file).resolution, }); } }; diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js index 8df5bbcc05ab41..80e62f05dee332 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js @@ -3,53 +3,14 @@ * * @provides Array.prototype.es6 * @polyfill - * @requires __DEV__ */ /*eslint-disable */ /*jslint bitwise: true */ -(function (undefined) { - if (__DEV__) { - // Define DEV-only setter that blows up when someone incorrectly - // iterates over arrays. - try { - Object.defineProperty && Object.defineProperty( - Array.prototype, - '__ARRAY_ENUMERATION_GUARD__', - { - configurable: true, - enumerable: true, - get: function() { - console.error( - 'Your code is broken! Do not iterate over arrays with ' + - 'for...in.' - ); - } - } - ); - } catch (e) { - // Nothing - } - } - +(function(undefined) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex function findIndex(predicate, context) { - /** - * Why am I seeing this "findIndex" method as a value in my array!? - * - * We polyfill the "findIndex" method -- called like - * `[1, 2, 3].findIndex(1)` -- for older browsers. A side effect of the way - * we do that is that the method is enumerable. If you were incorrectly - * iterating over your array using the object property iterator syntax - * `for (key in obj)` you will see the method name "findIndex" as a key. - * - * To fix your code please do one of the following: - * - * - Use a regular for loop with index. - * - Use one of the array methods: a.forEach, a.map, etc. - * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - */ if (this == null) { throw new TypeError( 'Array.prototype.findIndex called on null or undefined' @@ -69,32 +30,29 @@ } if (!Array.prototype.findIndex) { - Array.prototype.findIndex = findIndex; + Object.defineProperty(Array.prototype, 'findIndex', { + enumerable: false, + writable: true, + configurable: true, + value: findIndex + }); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find if (!Array.prototype.find) { - Array.prototype.find = function(predicate, context) { - /** - * Why am I seeing this "find" method as a value in my array!? - * - * We polyfill the "find" method -- called like - * `[1, 2, 3].find(1)` -- for older browsers. A side effect of the way - * we do that is that the method is enumerable. If you were incorrectly - * iterating over your array using the object property iterator syntax - * `for (key in obj)` you will see the method name "find" as a key. - * - * To fix your code please do one of the following: - * - * - Use a regular for loop with index. - * - Use one of the array methods: a.forEach, a.map, etc. - * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - */ - if (this == null) { - throw new TypeError('Array.prototype.find called on null or undefined'); + Object.defineProperty(Array.prototype, 'find', { + enumerable: false, + writable: true, + configurable: true, + value: function(predicate, context) { + if (this == null) { + throw new TypeError( + 'Array.prototype.find called on null or undefined' + ); + } + var index = findIndex.call(this, predicate, context); + return index === -1 ? undefined : this[index]; } - var index = findIndex.call(this, predicate, context); - return index === -1 ? undefined : this[index]; - }; + }); } })(); diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js index 91fb970f885521..57576961243cac 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js @@ -35,25 +35,34 @@ function getNativeLogFunction(level) { return function() { var str = Array.prototype.map.call(arguments, function(arg) { - if (arg == null) { - return arg === null ? 'null' : 'undefined'; - } else if (typeof arg === 'string') { - return '"' + arg + '"'; + var ret; + var type = typeof arg; + if (arg === null) { + ret = 'null'; + } else if (arg === undefined) { + ret = 'undefined'; + } else if (type === 'string') { + ret = '"' + arg + '"'; + } else if (type === 'function') { + try { + ret = arg.toString(); + } catch (e) { + ret = '[function unknown]'; + } } else { // Perform a try catch, just in case the object has a circular // reference or stringify throws for some other reason. try { - return JSON.stringify(arg); + ret = JSON.stringify(arg); } catch (e) { if (typeof arg.toString === 'function') { try { - return arg.toString(); - } catch (E) { - return 'unknown'; - } + ret = arg.toString(); + } catch (E) {} } } } + return ret || '["' + type + '" failed to stringify]'; }).join(', '); global.nativeLoggingHook(str, level); }; diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js index 9af96c1442ce07..cd1a28e558cdbb 100644 --- a/packager/react-packager/src/FileWatcher/index.js +++ b/packager/react-packager/src/FileWatcher/index.js @@ -26,7 +26,7 @@ var detectingWatcherClass = new Promise(function(resolve) { module.exports = FileWatcher; -var MAX_WAIT_TIME = 3000; +var MAX_WAIT_TIME = 25000; // Singleton var fileWatcher = null; @@ -73,7 +73,7 @@ function createWatcher(rootConfig) { var rejectTimeout = setTimeout(function() { reject(new Error([ 'Watcher took too long to load', - 'Try running `watchman` from your terminal', + 'Try running `watchman version` from your terminal', 'https://facebook.github.io/watchman/docs/troubleshooting.html', ].join('\n'))); }, MAX_WAIT_TIME); diff --git a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js index 72845a2e175460..eb307a02eb558a 100644 --- a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js +++ b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js @@ -11,6 +11,7 @@ jest .dontMock('worker-farm') .dontMock('os') + .dontMock('../../lib/ModuleTransport') .dontMock('../index'); var OPTIONS = { @@ -37,7 +38,7 @@ describe('Transformer', function() { pit('should loadFileAndTransform', function() { workers.mockImpl(function(data, callback) { - callback(null, { code: 'transformed' }); + callback(null, { code: 'transformed', map: 'sourceMap' }); }); require('fs').readFile.mockImpl(function(file, callback) { callback(null, 'content'); @@ -47,6 +48,7 @@ describe('Transformer', function() { .then(function(data) { expect(data).toEqual({ code: 'transformed', + map: 'sourceMap', sourcePath: 'file', sourceCode: 'content' }); diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index 962eb7fe9b9dfb..33e0170377eab4 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -14,6 +14,7 @@ var Cache = require('./Cache'); var workerFarm = require('worker-farm'); var declareOpts = require('../lib/declareOpts'); var util = require('util'); +var ModuleTransport = require('../lib/ModuleTransport'); var readFile = Promise.promisify(fs.readFile); @@ -100,11 +101,12 @@ Transformer.prototype.loadFileAndTransform = function(filePath) { throw formatError(res.error, filePath, sourceCode); } - return { + return new ModuleTransport({ code: res.code, + map: res.map, sourcePath: filePath, - sourceCode: sourceCode - }; + sourceCode: sourceCode, + }); } ); }); diff --git a/packager/react-packager/src/Packager/Package.js b/packager/react-packager/src/Packager/Package.js index 0f55c8edcc9ebf..c621f74700b0c9 100644 --- a/packager/react-packager/src/Packager/Package.js +++ b/packager/react-packager/src/Packager/Package.js @@ -11,44 +11,56 @@ var _ = require('underscore'); var base64VLQ = require('./base64-vlq'); var UglifyJS = require('uglify-js'); +var ModuleTransport = require('../lib/ModuleTransport'); module.exports = Package; function Package(sourceMapUrl) { this._finalized = false; this._modules = []; + this._assets = []; this._sourceMapUrl = sourceMapUrl; + this._shouldCombineSourceMaps = false; } Package.prototype.setMainModuleId = function(moduleId) { this._mainModuleId = moduleId; }; -Package.prototype.addModule = function( - transformedCode, - sourceCode, - sourcePath -) { - this._modules.push({ - transformedCode: transformedCode, - sourceCode: sourceCode, - sourcePath: sourcePath - }); +Package.prototype.addModule = function(module) { + if (!(module instanceof ModuleTransport)) { + throw new Error('Expeceted a ModuleTransport object'); + } + + // If we get a map from the transformer we'll switch to a mode + // were we're combining the source maps as opposed to + if (!this._shouldCombineSourceMaps && module.map != null) { + this._shouldCombineSourceMaps = true; + } + + this._modules.push(module); +}; + +Package.prototype.addAsset = function(asset) { + this._assets.push(asset); }; Package.prototype.finalize = function(options) { options = options || {}; if (options.runMainModule) { var runCode = ';require("' + this._mainModuleId + '");'; - this.addModule( - runCode, - runCode, - 'RunMainModule.js' - ); + this.addModule(new ModuleTransport({ + code: runCode, + virtual: true, + sourceCode: runCode, + sourcePath: 'RunMainModule.js' + })); } Object.freeze(this._modules); Object.seal(this._modules); + Object.freeze(this._assets); + Object.seal(this._assets); this._finalized = true; }; @@ -60,7 +72,7 @@ Package.prototype._assertFinalized = function() { Package.prototype._getSource = function() { if (this._source == null) { - this._source = _.pluck(this._modules, 'transformedCode').join('\n'); + this._source = _.pluck(this._modules, 'code').join('\n'); } return this._source; }; @@ -129,10 +141,50 @@ Package.prototype.getMinifiedSourceAndMap = function() { } }; +/** + * I found a neat trick in the sourcemap spec that makes it easy + * to concat sourcemaps. The `sections` field allows us to combine + * the sourcemap easily by adding an offset. Tested on chrome. + * Seems like it's not yet in Firefox but that should be fine for + * now. + */ +Package.prototype._getCombinedSourceMaps = function(options) { + var result = { + version: 3, + file: 'bundle.js', + sections: [], + }; + + var line = 0; + this._modules.forEach(function(module) { + var map = module.map; + if (module.virtual) { + map = generateSourceMapForVirtualModule(module); + } + + if (options.excludeSource) { + map = _.extend({}, map, {sourcesContent: []}); + } + + result.sections.push({ + offset: { line: line, column: 0 }, + map: map, + }); + line += module.code.split('\n').length; + }); + + return result; +}; + Package.prototype.getSourceMap = function(options) { this._assertFinalized(); options = options || {}; + + if (this._shouldCombineSourceMaps) { + return this._getCombinedSourceMaps(options); + } + var mappings = this._getMappings(); var map = { file: 'bundle.js', @@ -146,6 +198,10 @@ Package.prototype.getSourceMap = function(options) { return map; }; +Package.prototype.getAssets = function() { + return this._assets; +}; + Package.prototype._getMappings = function() { var modules = this._modules; @@ -157,13 +213,14 @@ Package.prototype._getMappings = function() { // except for the lineno mappinp: curLineno - prevLineno = 1; Which is C. var line = 'AACA'; + var moduleLines = Object.create(null); var mappings = ''; for (var i = 0; i < modules.length; i++) { var module = modules[i]; - var transformedCode = module.transformedCode; + var code = module.code; var lastCharNewLine = false; - module.lines = 0; - for (var t = 0; t < transformedCode.length; t++) { + moduleLines[module.sourcePath] = 0; + for (var t = 0; t < code.length; t++) { if (t === 0 && i === 0) { mappings += firstLine; } else if (t === 0) { @@ -172,13 +229,15 @@ Package.prototype._getMappings = function() { // This is the only place were we actually don't know the mapping ahead // of time. When it's a new module (and not the first) the lineno // mapping is 0 (current) - number of lines in prev module. - mappings += base64VLQ.encode(0 - modules[i - 1].lines); + mappings += base64VLQ.encode( + 0 - moduleLines[modules[i - 1].sourcePath] + ); mappings += 'A'; } else if (lastCharNewLine) { - module.lines++; + moduleLines[module.sourcePath]++; mappings += line; } - lastCharNewLine = transformedCode[t] === '\n'; + lastCharNewLine = code[t] === '\n'; if (lastCharNewLine) { mappings += ';'; } @@ -207,7 +266,25 @@ Package.prototype.getDebugInfo = function() { this._modules.map(function(m) { return '

Path:

' + m.sourcePath + '

Source:

' + '
'; + _.escape(m.code) + '
'; }).join('\n'), ].join('\n'); }; + +function generateSourceMapForVirtualModule(module) { + // All lines map 1-to-1 + var mappings = 'AAAA;'; + + for (var i = 1; i < module.code.split('\n').length; i++) { + mappings += 'AACA;'; + } + + return { + version: 3, + sources: [ module.sourcePath ], + names: [], + mappings: mappings, + file: module.sourcePath, + sourcesContent: [ module.code ], + }; +} diff --git a/packager/react-packager/src/Packager/__tests__/Package-test.js b/packager/react-packager/src/Packager/__tests__/Package-test.js index 5a7438d27f4293..0aaa3971cd8423 100644 --- a/packager/react-packager/src/Packager/__tests__/Package-test.js +++ b/packager/react-packager/src/Packager/__tests__/Package-test.js @@ -13,11 +13,13 @@ jest.autoMockOff(); var SourceMapGenerator = require('source-map').SourceMapGenerator; describe('Package', function() { + var ModuleTransport; var Package; var ppackage; beforeEach(function() { Package = require('../Package'); + ModuleTransport = require('../../lib/ModuleTransport'); ppackage = new Package('test_url'); ppackage.getSourceMap = jest.genMockFn().mockImpl(function() { return 'test-source-map'; @@ -26,8 +28,17 @@ describe('Package', function() { describe('source package', function() { it('should create a package and get the source', function() { - ppackage.addModule('transformed foo;', 'source foo', 'foo path'); - ppackage.addModule('transformed bar;', 'source bar', 'bar path'); + ppackage.addModule(new ModuleTransport({ + code: 'transformed foo;', + sourceCode: 'source foo', + sourcePath: 'foo path', + })); + ppackage.addModule(new ModuleTransport({ + code: 'transformed bar;', + sourceCode: 'source bar', + sourcePath: 'bar path', + })); + ppackage.finalize({}); expect(ppackage.getSource()).toBe([ 'transformed foo;', @@ -37,8 +48,18 @@ describe('Package', function() { }); it('should create a package and add run module code', function() { - ppackage.addModule('transformed foo;', 'source foo', 'foo path'); - ppackage.addModule('transformed bar;', 'source bar', 'bar path'); + ppackage.addModule(new ModuleTransport({ + code: 'transformed foo;', + sourceCode: 'source foo', + sourcePath: 'foo path' + })); + + ppackage.addModule(new ModuleTransport({ + code: 'transformed bar;', + sourceCode: 'source bar', + sourcePath: 'bar path' + })); + ppackage.setMainModuleId('foo'); ppackage.finalize({runMainModule: true}); expect(ppackage.getSource()).toBe([ @@ -59,7 +80,11 @@ describe('Package', function() { return minified; }; - ppackage.addModule('transformed foo;', 'source foo', 'foo path'); + ppackage.addModule(new ModuleTransport({ + code: 'transformed foo;', + sourceCode: 'source foo', + sourcePath: 'foo path' + })); ppackage.finalize(); expect(ppackage.getMinifiedSourceAndMap()).toBe(minified); }); @@ -68,13 +93,116 @@ describe('Package', function() { describe('sourcemap package', function() { it('should create sourcemap', function() { var p = new Package('test_url'); - p.addModule('transformed foo;\n', 'source foo', 'foo path'); - p.addModule('transformed bar;\n', 'source bar', 'bar path'); + p.addModule(new ModuleTransport({ + code: [ + 'transformed foo', + 'transformed foo', + 'transformed foo', + ].join('\n'), + sourceCode: [ + 'source foo', + 'source foo', + 'source foo', + ].join('\n'), + sourcePath: 'foo path', + })); + p.addModule(new ModuleTransport({ + code: [ + 'transformed bar', + 'transformed bar', + 'transformed bar', + ].join('\n'), + sourceCode: [ + 'source bar', + 'source bar', + 'source bar', + ].join('\n'), + sourcePath: 'bar path', + })); + p.setMainModuleId('foo'); p.finalize({runMainModule: true}); var s = p.getSourceMap(); expect(s).toEqual(genSourceMap(p._modules)); }); + + it('should combine sourcemaps', function() { + var p = new Package('test_url'); + + p.addModule(new ModuleTransport({ + code: 'transformed foo;\n', + map: {name: 'sourcemap foo'}, + sourceCode: 'source foo', + sourcePath: 'foo path' + })); + + p.addModule(new ModuleTransport({ + code: 'transformed foo;\n', + map: {name: 'sourcemap bar'}, + sourceCode: 'source foo', + sourcePath: 'foo path' + })); + + p.addModule(new ModuleTransport({ + code: 'image module;\nimage module;', + virtual: true, + sourceCode: 'image module;\nimage module;', + sourcePath: 'image.png', + })); + + p.setMainModuleId('foo'); + p.finalize({runMainModule: true}); + + var s = p.getSourceMap(); + expect(s).toEqual({ + file: 'bundle.js', + version: 3, + sections: [ + { offset: { line: 0, column: 0 }, map: { name: 'sourcemap foo' } }, + { offset: { line: 2, column: 0 }, map: { name: 'sourcemap bar' } }, + { + offset: { + column: 0, + line: 4 + }, + map: { + file: 'image.png', + mappings: 'AAAA;AACA;', + names: {}, + sources: [ 'image.png' ], + sourcesContent: ['image module;\nimage module;'], + version: 3, + } + }, + { + offset: { + column: 0, + line: 6 + }, + map: { + file: 'RunMainModule.js', + mappings: 'AAAA;', + names: {}, + sources: [ 'RunMainModule.js' ], + sourcesContent: [';require("foo");'], + version: 3, + } + } + ], + }); + }); + }); + + describe('getAssets()', function() { + it('should save and return asset objects', function() { + var p = new Package('test_url'); + var asset1 = {}; + var asset2 = {}; + p.addAsset(asset1); + p.addAsset(asset2); + p.finalize(); + expect(p.getAssets()).toEqual([asset1, asset2]); + }); }); }); @@ -83,7 +211,7 @@ describe('Package', function() { var packageLineNo = 0; for (var i = 0; i < modules.length; i++) { var module = modules[i]; - var transformedCode = module.transformedCode; + var transformedCode = module.code; var sourcePath = module.sourcePath; var sourceCode = module.sourceCode; var transformedLineCount = 0; diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Packager/__tests__/Packager-test.js index 0c9d4a84d6340f..d4a1c0f36e3ac4 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Packager/__tests__/Packager-test.js @@ -13,6 +13,7 @@ jest .dontMock('path') .dontMock('os') .dontMock('underscore') + .dontMock('../../lib/ModuleTransport') .setMock('uglify-js') .dontMock('../'); @@ -43,7 +44,20 @@ describe('Packager', function() { }; }); - var packager = new Packager({projectRoots: ['/root']}); + + require('fs').readFile.mockImpl(function(file, callback) { + callback(null, '{"json":true}'); + }); + + var assetServer = { + getAssetData: jest.genMockFn(), + }; + + var packager = new Packager({ + projectRoots: ['/root'], + assetServer: assetServer, + }); + var modules = [ {id: 'foo', path: '/root/foo.js', dependencies: []}, {id: 'bar', path: '/root/bar.js', dependencies: []}, @@ -52,6 +66,7 @@ describe('Packager', function() { path: '/root/img/img.png', isAsset_DEPRECATED: true, dependencies: [], + resolution: 2, }, { id: 'new_image.png', @@ -59,7 +74,13 @@ describe('Packager', function() { isAsset: true, resolution: 2, dependencies: [] - } + }, + { + id: 'package/file.json', + path: '/root/file.json', + isJSON: true, + dependencies: [], + }, ]; getDependencies.mockImpl(function() { @@ -73,6 +94,7 @@ describe('Packager', function() { .mockImpl(function(path) { return Promise.resolve({ code: 'transformed ' + path, + map: 'sourcemap ' + path, sourceCode: 'source ' + path, sourcePath: path }); @@ -86,50 +108,90 @@ describe('Packager', function() { cb(null, { width: 50, height: 100 }); }); + assetServer.getAssetData.mockImpl(function() { + return { + scales: [1,2,3], + hash: 'i am a hash', + name: 'img', + type: 'png', + }; + }); + return packager.package('/root/foo.js', true, 'source_map_url') .then(function(p) { - expect(p.addModule.mock.calls[0]).toEqual([ - 'lol transformed /root/foo.js lol', - 'source /root/foo.js', - '/root/foo.js' - ]); - expect(p.addModule.mock.calls[1]).toEqual([ - 'lol transformed /root/bar.js lol', - 'source /root/bar.js', - '/root/bar.js' - ]); - expect(p.addModule.mock.calls[2]).toEqual([ - 'lol module.exports = ' + - JSON.stringify({ uri: 'img', isStatic: true}) + + expect(p.addModule.mock.calls[0][0]).toEqual({ + code: 'lol transformed /root/foo.js lol', + map: 'sourcemap /root/foo.js', + sourceCode: 'source /root/foo.js', + sourcePath: '/root/foo.js', + }); + + expect(p.addModule.mock.calls[1][0]).toEqual({ + code: 'lol transformed /root/bar.js lol', + map: 'sourcemap /root/bar.js', + sourceCode: 'source /root/bar.js', + sourcePath: '/root/bar.js' + }); + + var imgModule_DEPRECATED = { + __packager_asset: true, + isStatic: true, + path: '/root/img/img.png', + uri: 'img', + width: 25, + height: 50, + deprecated: true, + }; + + expect(p.addModule.mock.calls[2][0]).toEqual({ + code: 'lol module.exports = ' + + JSON.stringify(imgModule_DEPRECATED) + '; lol', - 'module.exports = ' + - JSON.stringify({ uri: 'img', isStatic: true}) + + sourceCode: 'module.exports = ' + + JSON.stringify(imgModule_DEPRECATED) + ';', - '/root/img/img.png' - ]); + sourcePath: '/root/img/img.png' + }); var imgModule = { - isStatic: true, - path: '/root/img/new_image.png', - uri: 'assets/img/new_image.png', + __packager_asset: true, + fileSystemLocation: '/root/img', + httpServerLocation: '/assets/img', width: 25, height: 50, + scales: [1, 2, 3], + hash: 'i am a hash', + name: 'img', + type: 'png', }; - expect(p.addModule.mock.calls[3]).toEqual([ - 'lol module.exports = ' + + expect(p.addModule.mock.calls[3][0]).toEqual({ + code: 'lol module.exports = require("AssetRegistry").registerAsset(' + JSON.stringify(imgModule) + - '; lol', - 'module.exports = ' + + '); lol', + sourceCode: 'module.exports = require("AssetRegistry").registerAsset(' + JSON.stringify(imgModule) + - ';', - '/root/img/new_image.png' - ]); + ');', + sourcePath: '/root/img/new_image.png' + }); + + expect(p.addModule.mock.calls[4][0]).toEqual({ + code: 'lol module.exports = {"json":true}; lol', + sourceCode: 'module.exports = {"json":true};', + sourcePath: '/root/file.json' + }); expect(p.finalize.mock.calls[0]).toEqual([ {runMainModule: true} ]); + + expect(p.addAsset.mock.calls[0]).toEqual([ + imgModule_DEPRECATED + ]); + + expect(p.addAsset.mock.calls[1]).toEqual([ + imgModule + ]); }); }); - }); diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index 74e2ff4c358709..e647f7642c1fb1 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -14,12 +14,15 @@ var path = require('path'); var Promise = require('bluebird'); var Transformer = require('../JSTransformer'); var DependencyResolver = require('../DependencyResolver'); -var _ = require('underscore'); var Package = require('./Package'); var Activity = require('../Activity'); +var ModuleTransport = require('../lib/ModuleTransport'); var declareOpts = require('../lib/declareOpts'); var imageSize = require('image-size'); +var sizeOf = Promise.promisify(imageSize); +var readFile = Promise.promisify(fs.readFile); + var validateOpts = declareOpts({ projectRoots: { type: 'array', @@ -64,6 +67,10 @@ var validateOpts = declareOpts({ type: 'object', required: true, }, + assetServer: { + type: 'object', + required: true, + } }); function Packager(options) { @@ -91,6 +98,7 @@ function Packager(options) { }); this._projectRoots = opts.projectRoots; + this._assetServer = opts.assetServer; } Packager.prototype.kill = function() { @@ -98,9 +106,9 @@ Packager.prototype.kill = function() { }; Packager.prototype.package = function(main, runModule, sourceMapUrl, isDev) { - var transformModule = this._transformModule.bind(this); var ppackage = new Package(sourceMapUrl); + var transformModule = this._transformModule.bind(this, ppackage); var findEventId = Activity.startEvent('find dependencies'); var transformEventId; @@ -117,12 +125,8 @@ Packager.prototype.package = function(main, runModule, sourceMapUrl, isDev) { .then(function(transformedModules) { Activity.endEvent(transformEventId); - transformedModules.forEach(function(transformed) { - ppackage.addModule( - transformed.code, - transformed.sourceCode, - transformed.sourcePath - ); + transformedModules.forEach(function(moduleTransport) { + ppackage.addModule(moduleTransport); }); ppackage.finalize({ runMainModule: runModule }); @@ -138,16 +142,15 @@ Packager.prototype.getDependencies = function(main, isDev) { return this._resolver.getDependencies(main, { dev: isDev }); }; -Packager.prototype._transformModule = function(module) { +Packager.prototype._transformModule = function(ppackage, module) { var transform; if (module.isAsset_DEPRECATED) { - transform = Promise.resolve(generateAssetModule_DEPRECATED(module)); + transform = this.generateAssetModule_DEPRECATED(ppackage, module); } else if (module.isAsset) { - transform = generateAssetModule( - module, - getPathRelativeToRoot(this._projectRoots, module.path) - ); + transform = this.generateAssetModule(ppackage, module); + } else if (module.isJSON) { + transform = generateJSONModule(module); } else { transform = this._transformer.loadFileAndTransform( path.resolve(module.path) @@ -156,56 +159,88 @@ Packager.prototype._transformModule = function(module) { var resolver = this._resolver; return transform.then(function(transformed) { - return _.extend( - {}, - transformed, - {code: resolver.wrapModule(module, transformed.code)} - ); + var code = resolver.wrapModule(module, transformed.code); + return new ModuleTransport({ + code: code, + map: transformed.map, + sourceCode: transformed.sourceCode, + sourcePath: transformed.sourcePath, + }); }); }; - -function verifyRootExists(root) { - // Verify that the root exists. - assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory'); -} - Packager.prototype.getGraphDebugInfo = function() { return this._resolver.getDebugInfo(); }; -function generateAssetModule_DEPRECATED(module) { - var code = 'module.exports = ' + JSON.stringify({ - uri: module.id.replace(/^[^!]+!/, ''), - isStatic: true, - }) + ';'; - - return { - code: code, - sourceCode: code, - sourcePath: module.path, - }; -} - -var sizeOf = Promise.promisify(imageSize); - -function generateAssetModule(module, relPath) { +Packager.prototype.generateAssetModule_DEPRECATED = function(ppackage, module) { return sizeOf(module.path).then(function(dimensions) { var img = { + __packager_asset: true, isStatic: true, - path: module.path, //TODO(amasad): this should be path inside tar file. - uri: path.join('assets', relPath), + path: module.path, + uri: module.id.replace(/^[^!]+!/, ''), width: dimensions.width / module.resolution, height: dimensions.height / module.resolution, + deprecated: true, }; + ppackage.addAsset(img); + var code = 'module.exports = ' + JSON.stringify(img) + ';'; - return { + return new ModuleTransport({ code: code, sourceCode: code, sourcePath: module.path, + virtual: true, + }); + }); +}; + +Packager.prototype.generateAssetModule = function(ppackage, module) { + var relPath = getPathRelativeToRoot(this._projectRoots, module.path); + + return Promise.all([ + sizeOf(module.path), + this._assetServer.getAssetData(relPath), + ]).spread(function(dimensions, assetData) { + var img = { + __packager_asset: true, + fileSystemLocation: path.dirname(module.path), + httpServerLocation: path.join('/assets', path.dirname(relPath)), + width: dimensions.width / module.resolution, + height: dimensions.height / module.resolution, + scales: assetData.scales, + hash: assetData.hash, + name: assetData.name, + type: assetData.type, }; + + ppackage.addAsset(img); + + var ASSET_TEMPLATE = 'module.exports = require("AssetRegistry").registerAsset(%json);'; + var code = ASSET_TEMPLATE.replace('%json', JSON.stringify(img)); + + return new ModuleTransport({ + code: code, + sourceCode: code, + sourcePath: module.path, + virtual: true, + }); + }); +}; + +function generateJSONModule(module) { + return readFile(module.path).then(function(data) { + var code = 'module.exports = ' + data.toString('utf8') + ';'; + + return new ModuleTransport({ + code: code, + sourceCode: code, + sourcePath: module.path, + virtual: true, + }); }); } @@ -222,4 +257,9 @@ function getPathRelativeToRoot(roots, absPath) { ); } +function verifyRootExists(root) { + // Verify that the root exists. + assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory'); +} + module.exports = Packager; diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 3c7be04355e558..79022b21181166 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -71,13 +71,17 @@ function Server(options) { this._packages = Object.create(null); this._changeWatchers = []; + var assetGlobs = opts.assetExts.map(function(ext) { + return '**/*.' + ext; + }); + var watchRootConfigs = opts.projectRoots.map(function(dir) { return { dir: dir, globs: [ '**/*.js', - '**/package.json', - ] + '**/*.json', + ].concat(assetGlobs), }; }); @@ -86,9 +90,7 @@ function Server(options) { opts.assetRoots.map(function(dir) { return { dir: dir, - globs: opts.assetExts.map(function(ext) { - return '**/*.' + ext; - }), + globs: assetGlobs, }; }) ); @@ -98,15 +100,16 @@ function Server(options) { ? FileWatcher.createDummyWatcher() : new FileWatcher(watchRootConfigs); - var packagerOpts = Object.create(opts); - packagerOpts.fileWatcher = this._fileWatcher; - this._packager = new Packager(packagerOpts); - this._assetServer = new AssetServer({ projectRoots: opts.projectRoots, assetExts: opts.assetExts, }); + var packagerOpts = Object.create(opts); + packagerOpts.fileWatcher = this._fileWatcher; + packagerOpts.assetServer = this._assetServer; + this._packager = new Packager(packagerOpts); + var onFileChange = this._onFileChange.bind(this); this._fileWatcher.on('all', onFileChange); diff --git a/packager/react-packager/src/__mocks__/fs.js b/packager/react-packager/src/__mocks__/fs.js index 0ea13d15d04688..d0e08a2f4baacf 100644 --- a/packager/react-packager/src/__mocks__/fs.js +++ b/packager/react-packager/src/__mocks__/fs.js @@ -67,6 +67,12 @@ fs.lstat.mockImpl(function(filepath, callback) { return callback(e); } + var mtime = { + getTime: function() { + return Math.ceil(Math.random() * 10000000); + } + }; + if (node && typeof node === 'object' && node.SYMLINK == null) { callback(null, { isDirectory: function() { @@ -74,7 +80,8 @@ fs.lstat.mockImpl(function(filepath, callback) { }, isSymbolicLink: function() { return false; - } + }, + mtime: mtime, }); } else { callback(null, { @@ -86,7 +93,8 @@ fs.lstat.mockImpl(function(filepath, callback) { return true; } return false; - } + }, + mtime: mtime, }); } }); diff --git a/packager/react-packager/src/lib/ModuleTransport.js b/packager/react-packager/src/lib/ModuleTransport.js new file mode 100644 index 00000000000000..a5f1d5689106df --- /dev/null +++ b/packager/react-packager/src/lib/ModuleTransport.js @@ -0,0 +1,38 @@ +/** + * 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. + */ +'use strict'; + +function ModuleTransport(data) { + assertExists(data, 'code'); + this.code = data.code; + + assertExists(data, 'sourceCode'); + this.sourceCode = data.sourceCode; + + assertExists(data, 'sourcePath'); + this.sourcePath = data.sourcePath; + + this.virtual = data.virtual; + + if (this.virtual && data.map) { + throw new Error('Virtual modules cannot have source maps'); + } + + this.map = data.map; + + Object.freeze(this); +} + +module.exports = ModuleTransport; + +function assertExists(obj, field) { + if (obj[field] == null) { + throw new Error('Modules must have `' + field + '`'); + } +} diff --git a/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js b/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js index ad5ac3fbfe5edd..d0309ca6a30335 100644 --- a/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js +++ b/packager/react-packager/src/lib/__tests__/extractAssetResolution-test.js @@ -1,42 +1,52 @@ 'use strict'; jest.autoMockOff(); -var extractAssetResolution = require('../extractAssetResolution'); +var getAssetDataFromName = require('../getAssetDataFromName'); -describe('extractAssetResolution', function() { +describe('getAssetDataFromName', function() { it('should extract resolution simple case', function() { - var data = extractAssetResolution('test@2x.png'); + var data = getAssetDataFromName('test@2x.png'); expect(data).toEqual({ assetName: 'test.png', resolution: 2, + type: 'png', + name: 'test', }); }); it('should default resolution to 1', function() { - var data = extractAssetResolution('test.png'); + var data = getAssetDataFromName('test.png'); expect(data).toEqual({ assetName: 'test.png', resolution: 1, + type: 'png', + name: 'test', }); }); it('should support float', function() { - var data = extractAssetResolution('test@1.1x.png'); + var data = getAssetDataFromName('test@1.1x.png'); expect(data).toEqual({ assetName: 'test.png', resolution: 1.1, + type: 'png', + name: 'test', }); - data = extractAssetResolution('test@.1x.png'); + data = getAssetDataFromName('test@.1x.png'); expect(data).toEqual({ assetName: 'test.png', resolution: 0.1, + type: 'png', + name: 'test', }); - data = extractAssetResolution('test@0.2x.png'); + data = getAssetDataFromName('test@0.2x.png'); expect(data).toEqual({ assetName: 'test.png', resolution: 0.2, + type: 'png', + name: 'test', }); }); }); diff --git a/packager/react-packager/src/lib/extractAssetResolution.js b/packager/react-packager/src/lib/getAssetDataFromName.js similarity index 63% rename from packager/react-packager/src/lib/extractAssetResolution.js rename to packager/react-packager/src/lib/getAssetDataFromName.js index 8fb91afc4ebcff..c4848fd179b020 100644 --- a/packager/react-packager/src/lib/extractAssetResolution.js +++ b/packager/react-packager/src/lib/getAssetDataFromName.js @@ -2,7 +2,7 @@ var path = require('path'); -function extractAssetResolution(filename) { +function getAssetDataFromName(filename) { var ext = path.extname(filename); var re = new RegExp('@([\\d\\.]+)x\\' + ext + '$'); @@ -19,10 +19,13 @@ function extractAssetResolution(filename) { } } + var assetName = match ? filename.replace(re, ext) : filename; return { resolution: resolution, - assetName: match ? filename.replace(re, ext) : filename, + assetName: assetName, + type: ext.slice(1), + name: path.basename(assetName, ext) }; } -module.exports = extractAssetResolution; +module.exports = getAssetDataFromName; diff --git a/packager/webSocketProxy.js b/packager/webSocketProxy.js index 8223bbf24b0e78..f863621362e421 100644 --- a/packager/webSocketProxy.js +++ b/packager/webSocketProxy.js @@ -34,7 +34,12 @@ function attachToServer(server, path) { ws.on('message', function(message) { allClientsExcept(ws).forEach(function(cn) { - cn.send(message); + try { + // Sometimes this call throws 'not opened' + cn.send(message); + } catch(e) { + console.warn('WARN: ' + e.message); + } }); }); }); diff --git a/website/core/HeaderLinks.js b/website/core/HeaderLinks.js index 7a481ed2959571..32c371a7dcf2f3 100644 --- a/website/core/HeaderLinks.js +++ b/website/core/HeaderLinks.js @@ -17,7 +17,7 @@ var HeaderLinks = React.createClass({ {section: 'support', href: '/react-native/support.html', text: 'Support'}, ], linksExternal: [ - {section: 'github', href: 'http://github.com/facebook/react-native', text: 'GitHub'}, + {section: 'github', href: 'https://github.com/facebook/react-native', text: 'GitHub'}, {section: 'react', href: 'http://facebook.github.io/react', text: 'React'}, ], diff --git a/website/jsdocs/jsdocs.js b/website/jsdocs/jsdocs.js index 3f7cb953e1008e..086ad296683e66 100644 --- a/website/jsdocs/jsdocs.js +++ b/website/jsdocs/jsdocs.js @@ -105,10 +105,20 @@ function getFileDocBlock(commentsForFile) { commentsForFile.some(function(comment, i) { if (comment.loc.start.line === 1) { var lines = comment.value.split('\n'); + var inCopyrightBlock = false; var filteredLines = lines.filter(function(line) { - var hasCopyright = !!line.match(/^\s*\*\s+Copyright/); + if (!!line.match(/^\s*\*\s+Copyright \(c\)/)) { + inCopyrightBlock = true; + } + var hasProvides = !!line.match(/^\s*\*\s+@provides/); - return !hasCopyright && !hasProvides; + var hasFlow = !!line.match(/^\s*\*\s+@flow/); + + if (hasFlow || hasProvides) { + inCopyrightBlock = false; + } + + return !inCopyrightBlock && !hasFlow && !hasProvides; }); docblock = filteredLines.join('\n'); return true; diff --git a/website/layout/AutodocsLayout.js b/website/layout/AutodocsLayout.js index 23b2238144e434..b9bef328c7c2c7 100644 --- a/website/layout/AutodocsLayout.js +++ b/website/layout/AutodocsLayout.js @@ -33,6 +33,10 @@ var ComponentDoc = React.createClass({ return '{' + Object.keys(type.value).map((key => key + ': ' + this.renderType(type.value[key]))).join(', ') + '}'; } + if (type.name == 'union') { + return type.value.map(this.renderType).join(', '); + } + if (type.name === 'arrayOf') { return '[' + this.renderType(type.value) + ']'; } @@ -84,7 +88,7 @@ var ComponentDoc = React.createClass({ return ( ); @@ -96,14 +100,18 @@ var ComponentDoc = React.createClass({
{(style.composes || []).map((name) => { var link; - if (name !== 'LayoutPropTypes') { - name = name.replace('StylePropTypes', ''); - link = - {name}#style...; - } else { + if (name === 'LayoutPropTypes') { name = 'Flexbox'; link = {name}...; + } else if (name === 'TransformPropTypes') { + name = 'Transforms'; + link = + {name}...; + } else { + name = name.replace('StylePropTypes', ''); + link = + {name}#style...; } return (
diff --git a/website/server/convert.js b/website/server/convert.js index 8dfc4ce925a2e3..8edc1b190ba33b 100644 --- a/website/server/convert.js +++ b/website/server/convert.js @@ -69,7 +69,10 @@ function execute() { try { value = JSON.parse(value); } catch(e) { } metadata[key] = value; } - metadatas.files.push(metadata); + + if (metadata.sidebar !== false) { + metadatas.files.push(metadata); + } if (metadata.permalink.match(/^https?:/)) { return; diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 54252af5b5e11e..a4500e9ebb798c 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -19,9 +19,12 @@ function getNameFromPath(filepath) { while (ext = path.extname(filepath)) { filepath = path.basename(filepath, ext); } + if (filepath === 'LayoutPropTypes') { return 'Flexbox'; - } else if (filepath == 'TabBarItemIOS') { + } else if (filepath === 'TransformPropTypes') { + return 'Transforms'; + } else if (filepath === 'TabBarItemIOS') { return 'TabBarIOS.Item'; } return filepath; @@ -41,6 +44,33 @@ function getExample(componentName) { }; } +// Hide a component from the sidebar by making it return false from +// this function +function shouldDisplayInSidebar(componentName) { + if (componentName === 'Transforms') { + return false; + } + + return true; +} + +function getNextComponent(i) { + var next; + var filepath = all[i]; + + if (all[i + 1]) { + var nextComponentName = getNameFromPath(all[i + 1]); + + if (shouldDisplayInSidebar(nextComponentName)) { + return slugify(nextComponentName); + } else { + return getNextComponent(i + 1); + } + } else { + return 'network'; + } +} + function componentsToMarkdown(type, json, filepath, i, styles) { var componentName = getNameFromPath(filepath); @@ -56,16 +86,19 @@ function componentsToMarkdown(type, json, filepath, i, styles) { } json.example = getExample(componentName); + // Put Flexbox into the Polyfills category + var category = (type === 'style' ? 'Polyfills' : type + 's'); + var next = getNextComponent(i); + var res = [ '---', 'id: ' + slugify(componentName), 'title: ' + componentName, 'layout: autodocs', - 'category: ' + (type === 'style' ? 'Polyfills' : type + 's'), + 'category: ' + category, 'permalink: docs/' + slugify(componentName) + '.html', - 'next: ' + (all[i + 1] ? - slugify(getNameFromPath(all[i + 1])) : - 'network'), + 'next: ' + next, + 'sidebar: ' + shouldDisplayInSidebar(componentName), '---', JSON.stringify(json, null, 2), ].filter(function(line) { return line; }).join('\n'); @@ -80,6 +113,7 @@ function renderComponent(filepath) { docgenHelpers.findExportedOrFirst, docgen.defaultHandlers.concat(docgenHelpers.stylePropTypeHandler) ); + return componentsToMarkdown('component', json, filepath, n++, styleDocs); } @@ -102,6 +136,14 @@ function renderStyle(filepath) { docgenHelpers.findExportedObject, [docgen.handlers.propTypeHandler] ); + + // Remove deprecated transform props from docs + if (filepath === "../Libraries/StyleSheet/TransformPropTypes.js") { + ['rotation', 'scaleX', 'scaleY', 'translateX', 'translateY'].forEach(function(key) { + delete json['props'][key]; + }); + } + return componentsToMarkdown('style', json, filepath, n++); } @@ -148,27 +190,29 @@ var apis = [ var styles = [ '../Libraries/StyleSheet/LayoutPropTypes.js', + '../Libraries/StyleSheet/TransformPropTypes.js', '../Libraries/Components/View/ViewStylePropTypes.js', '../Libraries/Text/TextStylePropTypes.js', '../Libraries/Image/ImageStylePropTypes.js', ]; var polyfills = [ - '../Libraries/GeoLocation/Geolocation.ios.js', + '../Libraries/GeoLocation/Geolocation.js', ]; var all = components .concat(apis) - .concat(styles.slice(0, 1)) + .concat(styles.slice(0, 2)) .concat(polyfills); -var styleDocs = styles.slice(1).reduce(function(docs, filepath) { +var styleDocs = styles.slice(2).reduce(function(docs, filepath) { docs[path.basename(filepath).replace(path.extname(filepath), '')] = docgen.parse( fs.readFileSync(filepath), docgenHelpers.findExportedObject, [docgen.handlers.propTypeHandler] ); + return docs; }, {}); @@ -177,7 +221,7 @@ module.exports = function() { return [].concat( components.map(renderComponent), apis.map(renderAPI('api')), - styles.slice(0, 1).map(renderStyle), + styles.slice(0, 2).map(renderStyle), polyfills.map(renderAPI('Polyfill')) ); }; diff --git a/website/server/generate.js b/website/server/generate.js index 2ac303b4980925..aab331c19b8668 100644 --- a/website/server/generate.js +++ b/website/server/generate.js @@ -36,7 +36,7 @@ glob('src/**/*.*', function(er, files) { return; } if (response.statusCode != 200) { - reject(new Error('Status ' + response.statusCode + ':\n' + body)); + reject(new Error('Status ' + response.statusCode + ':\n' + body)); return; } mkdirp.sync(targetFile.replace(new RegExp('/[^/]*$'), '')); diff --git a/website/src/react-native/index.js b/website/src/react-native/index.js index 72833fcd8abf84..729e74b4882baf 100644 --- a/website/src/react-native/index.js +++ b/website/src/react-native/index.js @@ -176,7 +176,7 @@ RCT_EXPORT_MODULE(); // Available as NativeModules.MyCustomModule.processString RCT_EXPORT_METHOD(processString:(NSString *)input callback:(RCTResponseSenderBlock)callback) { - callback(@[[input stringByReplacingOccurrencesOfString:@"Goodbye" withString:@"Hello"];]]); + callback(@[[input stringByReplacingOccurrencesOfString:@"Goodbye" withString:@"Hello"]]); } @end`}