diff --git a/.eslintrc b/.eslintrc index 84045365c..e1ec93029 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,8 +1,19 @@ { + "parser": "babel-eslint", + + "ecmaFeatures": { + "jsx": true + }, + "env": { + "es6": true, "jasmine": true, }, + "plugins": [ + "react" + ], + // Map from global var to bool specifying if it can be redefined "globals": { "__DEV__": true, @@ -36,10 +47,10 @@ }, "rules": { + "comma-dangle": 0, // disallow trailing commas in object literals "no-cond-assign": 1, // disallow assignment in conditional expressions "no-console": 0, // disallow use of console (off by default in the node environment) "no-constant-condition": 0, // disallow use of constant expressions in conditions - "no-comma-dangle": 0, // disallow trailing commas in object literals "no-control-regex": 1, // disallow control characters in regular expressions "no-debugger": 1, // disallow use of debugger "no-dupe-keys": 1, // disallow duplicate keys when creating object literals @@ -107,6 +118,7 @@ "no-warning-comments": 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default) "no-with": 1, // disallow use of the with statement "radix": 1, // require use of the second argument for parseInt() (off by default) + "semi-spacing": 1, // require a space after a semi-colon "vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default) "wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default) "yoda": 1, // require or disallow Yoda conditions @@ -177,7 +189,7 @@ "space-in-parens": 0, // require or disallow spaces inside parentheses (off by default) "space-infix-ops": 1, // require spaces around operators "space-return-throw-case": 1, // require a space after return, throw, and case - "space-unary-word-ops": 1, // require a space around word operators such as typeof (off by default) + "space-unary-ops": [1, { "words": true, "nonwords": false }], // require or disallow spaces before/after unary operators (words on by default, nonwords off by default) "max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default) "one-var": 0, // allow just one var statement per function (off by default) "wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default) @@ -190,6 +202,22 @@ "max-params": 0, // limits the number of parameters that can be used in the function declaration. (off by default) "max-statements": 0, // specify the maximum number of statement allowed in a function (off by default) "no-bitwise": 1, // disallow use of bitwise operators (off by default) - "no-plusplus": 0 // disallow use of unary operators, ++ and -- (off by default) + "no-plusplus": 0, // disallow use of unary operators, ++ and -- (off by default) + + "react/display-name": 0, + "react/jsx-boolean-value": 0, + "react/jsx-quotes": [1, "double", "avoid-escape"], + "react/jsx-no-undef": 1, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 0, + "react/jsx-uses-vars": 1, + "react/no-did-mount-set-state": [1, "allow-in-func"], + "react/no-did-update-set-state": [1, "allow-in-func"], + "react/no-multi-comp": 0, + "react/no-unknown-property": 0, + "react/prop-types": 0, + "react/react-in-jsx-scope": 0, + "react/self-closing-comp": 1, + "react/wrap-multilines": 0 } } diff --git a/.flowconfig b/.flowconfig index 448732ec8..9953a269f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -16,13 +16,13 @@ .*/node_modules/react-tools/src/event/EventPropagators.js # Ignore commoner tests -.*/node_modules/react-tools/node_modules/commoner/test/.* +.*/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/.* +.*/node_modules/jest-cli/.* # Ignore Website .*/website/.* diff --git a/Examples/Movies/MovieCell.js b/Examples/Movies/MovieCell.js index 7e2b3df17..e6a9b1078 100644 --- a/Examples/Movies/MovieCell.js +++ b/Examples/Movies/MovieCell.js @@ -34,7 +34,10 @@ var MovieCell = React.createClass({ var criticsScore = this.props.movie.ratings.critics_score; return ( - + - ); } diff --git a/Examples/Movies/Movies.xcodeproj/project.pbxproj b/Examples/Movies/Movies.xcodeproj/project.pbxproj index e86674f68..21fc0cb88 100644 --- a/Examples/Movies/Movies.xcodeproj/project.pbxproj +++ b/Examples/Movies/Movies.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 140D9B661AC36C42004F25EE /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14312D241AC3654D00CDC950 /* libRCTLinking.a */; }; 14A2D4421AC3E43800CC738A /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14A2D4411AC3E41A00CC738A /* libReact.a */; }; 58C5726B1AA6239E00CDF9C8 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C5725B1AA6236500CDF9C8 /* libRCTText.a */; }; + 67C95F201B0E64A30040BCE2 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 67C95F1E1B0E647A0040BCE2 /* libRCTWebSocket.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,6 +55,13 @@ remoteGlobalIDString = 58B5119B1A9E6C1200147676; remoteInfo = RCTText; }; + 67C95F1D1B0E647A0040BCE2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 67C95F151B0E647A0040BCE2 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C86DF461ADF2C930047B81A; + remoteInfo = RCTWebSocket; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -69,6 +77,7 @@ 14312D1E1AC3654D00CDC950 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 14A2D43C1AC3E41A00CC738A /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; 587650F61A9EB120008B8F17 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = SOURCE_ROOT; }; + 67C95F151B0E647A0040BCE2 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../../Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -76,6 +85,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 67C95F201B0E64A30040BCE2 /* libRCTWebSocket.a in Frameworks */, 14A2D4421AC3E43800CC738A /* libReact.a in Frameworks */, 140D9B661AC36C42004F25EE /* libRCTLinking.a in Frameworks */, 1341801E1AA91750003F314A /* libRCTNetwork.a in Frameworks */, @@ -135,6 +145,7 @@ 58C571FC1AA6124500CDF9C8 /* Libraries */ = { isa = PBXGroup; children = ( + 67C95F151B0E647A0040BCE2 /* RCTWebSocket.xcodeproj */, 14A2D43C1AC3E41A00CC738A /* React.xcodeproj */, 14312D1E1AC3654D00CDC950 /* RCTLinking.xcodeproj */, 134180151AA91740003F314A /* RCTNetwork.xcodeproj */, @@ -152,6 +163,14 @@ name = Products; sourceTree = ""; }; + 67C95F161B0E647A0040BCE2 /* Products */ = { + isa = PBXGroup; + children = ( + 67C95F1E1B0E647A0040BCE2 /* libRCTWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( @@ -228,6 +247,10 @@ ProductGroup = 58C572571AA6236500CDF9C8 /* Products */; ProjectRef = 587650F61A9EB120008B8F17 /* RCTText.xcodeproj */; }, + { + ProductGroup = 67C95F161B0E647A0040BCE2 /* Products */; + ProjectRef = 67C95F151B0E647A0040BCE2 /* RCTWebSocket.xcodeproj */; + }, { ProductGroup = 14A2D43D1AC3E41A00CC738A /* Products */; ProjectRef = 14A2D43C1AC3E41A00CC738A /* React.xcodeproj */; @@ -276,6 +299,13 @@ remoteRef = 58C5725A1AA6236500CDF9C8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 67C95F1E1B0E647A0040BCE2 /* libRCTWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTWebSocket.a; + remoteRef = 67C95F1D1B0E647A0040BCE2 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index e484d0f65..05ab80187 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -237,10 +237,31 @@ var SearchScreen = React.createClass({ return ; }, - renderRow: function(movie: Object) { + renderSeparator: function( + sectionID: number | string, + rowID: number | string, + adjacentRowHighlighted: boolean + ) { + var style = styles.rowSeparator; + if (adjacentRowHighlighted) { + style = [style, styles.rowSeparatorHide]; + } + return ( + + ); + }, + + renderRow: function( + movie: Object, + sectionID: number | string, + rowID: number | string, + highlightRowFunc: (sectionID: ?number | string, rowID: ?number | string) => void, + ) { return ( this.selectMovie(movie)} + onHighlight={() => highlightRowFunc(sectionID, rowID)} + onUnhighlight={() => highlightRowFunc(null, null)} movie={movie} /> ); @@ -254,6 +275,7 @@ var SearchScreen = React.createClass({ /> : + alert('onAccessibilityTap success')} + accessible={true}> + + Accessibility normal tap example + + + alert('onMagicTap success')} + accessible={true}> + + Accessibility magic tap example + + + + + Accessibility label example + + + + + Accessibility traits example + + + + ); + }, +}); + +exports.title = 'AcccessibilityIOS'; +exports.description = 'Interface to show iOS\' accessibility samples'; +exports.examples = [ + { + title: 'Accessibility elements', + render(): ReactElement { return ; } + }, +]; diff --git a/Examples/UIExplorer/AppStateIOSExample.js b/Examples/UIExplorer/AppStateIOSExample.js index 9272f9e78..c2a011ceb 100644 --- a/Examples/UIExplorer/AppStateIOSExample.js +++ b/Examples/UIExplorer/AppStateIOSExample.js @@ -28,13 +28,19 @@ var AppStateSubscription = React.createClass({ return { appState: AppStateIOS.currentState, previousAppStates: [], + memoryWarnings: 0, }; }, componentDidMount: function() { AppStateIOS.addEventListener('change', this._handleAppStateChange); + AppStateIOS.addEventListener('memoryWarning', this._handleMemoryWarning); }, componentWillUnmount: function() { AppStateIOS.removeEventListener('change', this._handleAppStateChange); + AppStateIOS.removeEventListener('memoryWarning', this._handleMemoryWarning); + }, + _handleMemoryWarning: function() { + this.setState({memoryWarnings: this.state.memoryWarnings + 1}) }, _handleAppStateChange: function(appState) { var previousAppStates = this.state.previousAppStates.slice(); @@ -45,6 +51,13 @@ var AppStateSubscription = React.createClass({ }); }, render() { + if (this.props.showMemoryWarnings) { + return ( + + {this.state.memoryWarnings} + + ); + } if (this.props.showCurrentOnly) { return ( @@ -77,4 +90,9 @@ exports.examples = [ title: 'Previous states:', render(): ReactElement { return ; } }, + { + title: 'Memory Warnings', + description: "In the simulator, hit Shift+Command+M to simulate a memory warning.", + render(): ReactElement { return ; } + }, ]; diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index f49329fae..7789459f8 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -80,6 +80,13 @@ var styles = StyleSheet.create({ borderTopLeftRadius: 100, }, + border7: { + borderRadius: 20, + }, + border7_inner: { + backgroundColor: 'blue', + flex: 1, + }, }); exports.title = 'Border'; @@ -134,4 +141,15 @@ exports.examples = [ return ; } }, + { + title: 'Custom Borders', + description: 'borderRadius & clipping', + render() { + return ( + + + + ); + } + }, ]; diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index a8f913a07..12ca22e78 100644 --- a/Examples/UIExplorer/TabBarIOSExample.js +++ b/Examples/UIExplorer/TabBarIOSExample.js @@ -48,7 +48,9 @@ var TabBarExample = React.createClass({ render: function() { return ( - + "]) { - return YES; - } + if ([view.accessibilityLabel isEqualToString:@""]) { + return YES; } return NO; }]; diff --git a/Libraries/AppStateIOS/AppStateIOS.ios.js b/Libraries/AppStateIOS/AppStateIOS.ios.js index 48db8415b..e123d23be 100644 --- a/Libraries/AppStateIOS/AppStateIOS.ios.js +++ b/Libraries/AppStateIOS/AppStateIOS.ios.js @@ -11,15 +11,18 @@ */ 'use strict'; +var Map = require('Map'); var NativeModules = require('NativeModules'); var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var RCTAppState = NativeModules.AppState; var logError = require('logError'); +var invariant = require('invariant'); -var DEVICE_APPSTATE_EVENT = 'appStateDidChange'; - -var _appStateHandlers = {}; +var _eventHandlers = { + change: new Map(), + memoryWarning: new Map(), +}; /** * `AppStateIOS` can tell you if the app is in the foreground or background, @@ -82,12 +85,23 @@ var AppStateIOS = { type: string, handler: Function ) { - _appStateHandlers[handler] = RCTDeviceEventEmitter.addListener( - DEVICE_APPSTATE_EVENT, - (appStateData) => { - handler(appStateData.app_state); - } + invariant( + ['change', 'memoryWarning'].indexOf(type) !== -1, + 'Trying to subscribe to unknown event: "%s"', type ); + if (type === 'change') { + _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( + 'appStateDidChange', + (appStateData) => { + handler(appStateData.app_state); + } + )); + } else if (type === 'memoryWarning') { + _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( + 'memoryWarning', + handler + )); + } }, /** @@ -97,11 +111,15 @@ var AppStateIOS = { type: string, handler: Function ) { - if (!_appStateHandlers[handler]) { + invariant( + ['change', 'memoryWarning'].indexOf(type) !== -1, + 'Trying to remove listener for unknown event: "%s"', type + ); + if (!_eventHandlers[type].has(handler)) { return; } - _appStateHandlers[handler].remove(); - _appStateHandlers[handler] = null; + _eventHandlers[type].get(handler).remove(); + _eventHandlers[type].delete(handler); }, currentState: (null : ?String), @@ -109,7 +127,7 @@ var AppStateIOS = { }; RCTDeviceEventEmitter.addListener( - DEVICE_APPSTATE_EVENT, + 'appStateDidChange', (appStateData) => { AppStateIOS.currentState = appStateData.app_state; } diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js index 4163b2d78..1910c67ca 100644 --- a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js @@ -25,11 +25,22 @@ var TabBarIOS = React.createClass({ propTypes: { style: View.propTypes.style, + /** + * Color of the currently selected tab icon + */ + tintColor: React.PropTypes.string, + /** + * Background color of the tab bar + */ + barTintColor: React.PropTypes.string }, render: function() { return ( - + {this.props.children} ); diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index b96d8d0d0..533652f65 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -70,6 +70,14 @@ var TouchableHighlight = React.createClass({ */ underlayColor: React.PropTypes.string, style: View.propTypes.style, + /** + * Called immediately after the underlay is shown + */ + onShowUnderlay: React.PropTypes.func, + /** + * Called immediately after the underlay is hidden + */ + onHideUnderlay: React.PropTypes.func, }, mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin], @@ -159,6 +167,7 @@ var TouchableHighlight = React.createClass({ _showUnderlay: function() { this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps); this.refs[CHILD_REF].setNativeProps(this.state.activeProps); + this.props.onShowUnderlay && this.props.onShowUnderlay(); }, _hideUnderlay: function() { @@ -170,6 +179,7 @@ var TouchableHighlight = React.createClass({ ...INACTIVE_UNDERLAY_PROPS, style: this.state.underlayStyle, }); + this.props.onHideUnderlay && this.props.onHideUnderlay(); } }, diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index abec1a6e0..17f1e477d 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -108,7 +108,7 @@ var ListView = React.createClass({ * You must provide a renderRow function. If you omit any of the other render * functions, ListView will simply skip rendering them. * - * - renderRow(rowData, sectionID, rowID); + * - renderRow(rowData, sectionID, rowID, highlightRow); * - renderSectionHeader(sectionData, sectionID); */ propTypes: { @@ -116,11 +116,22 @@ var ListView = React.createClass({ dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired, /** - * (rowData, sectionID, rowID) => renderable + * (sectionID, rowID, adjacentRowHighlighted) => renderable + * If provided, a renderable component to be rendered as the separator + * below each row but not the last row if there is a section header below. + * Take a sectionID and rowID of the row above and whether its adjacent row + * is highlighted. + */ + renderSeparator: PropTypes.func, + /** + * (rowData, sectionID, rowID, highlightRow) => renderable * Takes a data entry from the data source and its ids and should return * a renderable component to be rendered as the row. By default the data * is exactly what was put into the data source, but it's also possible to - * provide custom extractors. + * provide custom extractors. ListView can be notified when a row is + * being highlighted by calling highlightRow function. The separators above and + * below will be hidden when a row is highlighted. The highlighted state of + * a row can be reset by calling highlightRow(null). */ renderRow: PropTypes.func.isRequired, /** @@ -227,6 +238,7 @@ var ListView = React.createClass({ return { curRenderedRowsCount: this.props.initialListSize, prevRenderedRowsCount: 0, + highlightedRow: {}, }; }, @@ -256,6 +268,10 @@ var ListView = React.createClass({ } }, + onRowHighlighted: function(sectionID, rowID) { + this.setState({highlightedRow: {sectionID, rowID}}); + }, + render: function() { var bodyComponents = []; @@ -305,11 +321,28 @@ var ListView = React.createClass({ null, dataSource.getRowData(sectionIdx, rowIdx), sectionID, - rowID + rowID, + this.onRowHighlighted )} />; bodyComponents.push(row); totalIndex++; + + if (this.props.renderSeparator && + (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length -1)) { + var adjacentRowHighlighted = + this.state.highlightedRow.sectionID === sectionID && ( + this.state.highlightedRow.rowID === rowID || + this.state.highlightedRow.rowID === rowIDs[rowIdx + 1] + ); + var separator = this.props.renderSeparator( + sectionID, + rowID, + adjacentRowHighlighted + ); + bodyComponents.push(separator); + totalIndex++; + } if (++rowCount === this.state.curRenderedRowsCount) { break; } diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index b951d7582..1ebbc70aa 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -332,7 +332,7 @@ var Navigator = React.createClass({ this._subRouteFocus = []; this.navigatorContext = { // Actions for child navigators or interceptors: - setHandlerForRoute: this.setHandlerForRoute, + setHandlerForIndex: this.setHandlerForIndex, request: this.request, // Contextual utilities @@ -340,14 +340,13 @@ var Navigator = React.createClass({ getCurrentRoutes: this.getCurrentRoutes, // `route` is injected by NavigatorStaticContextContainer - // Contextual nav actions + // Contextual nav action pop: this.requestPop, - popToRoute: this.requestPopTo, - // Legacy, imperitive nav actions. Will transition these to contextual actions jumpBack: this.jumpBack, jumpForward: this.jumpForward, jumpTo: this.jumpTo, + popToRoute: this.popToRoute, push: this.push, replace: this.replace, replaceAtIndex: this.replaceAtIndex, @@ -410,8 +409,6 @@ var Navigator = React.createClass({ switch (action) { case 'pop': return this._handlePop(arg1); - case 'popTo': - return this._handlePopTo(arg1); case 'push': return this._handlePush(arg1); default: @@ -440,30 +437,13 @@ var Navigator = React.createClass({ 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; - }, - _handlePush: function(route) { this.push(route); return true; }, - setHandlerForRoute: function(route, handler) { - this._handlers[this.state.routeStack.indexOf(route)] = handler; + setHandlerForIndex: function(index, handler) { + this._handlers[index] = handler; }, componentDidMount: function() { @@ -1155,17 +1135,13 @@ var Navigator = React.createClass({ this.popToRoute(this.state.routeStack[0]); }, - _getNumToPopForRoute: function(route) { + popToRoute: function(route) { var indexOfRoute = this.state.routeStack.indexOf(route); invariant( indexOfRoute !== -1, - 'Calling pop to route for a route that doesn\'t exist!' + 'Calling popToRoute for a route that doesn\'t exist!' ); - return this.state.presentedIndex - indexOfRoute; - }, - - popToRoute: function(route) { - var numToPop = this._getNumToPopForRoute(route); + var numToPop = this.state.presentedIndex - indexOfRoute; this._popN(numToPop); }, @@ -1262,7 +1238,7 @@ var Navigator = React.createClass({ ...this.navigatorContext, route, setHandler: (handler) => { - this.navigatorContext.setHandlerForRoute(route, handler); + this.navigatorContext.setHandlerForIndex(i, handler); }, onWillFocus: (childRoute) => { this._subRouteFocus[i] = childRoute; diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index f21233dbf..0fe5fed75 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -85,7 +85,9 @@ static NSDictionary *RCTPositionError(RCTPositionErrorCode code, NSString *msg / - (void)dealloc { - [_timeoutTimer invalidate]; + if (_timeoutTimer.valid) { + [_timeoutTimer invalidate]; + } } @end @@ -273,6 +275,7 @@ RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options // Fire all queued callbacks for (RCTLocationRequest *request in _pendingRequests) { request.successBlock(@[_lastLocationEvent]); + [request.timeoutTimer invalidate]; } [_pendingRequests removeAllObjects]; @@ -311,6 +314,7 @@ RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options // Fire all queued error callbacks for (RCTLocationRequest *request in _pendingRequests) { request.errorBlock(@[jsError]); + [request.timeoutTimer invalidate]; } [_pendingRequests removeAllObjects]; diff --git a/Libraries/Inspector.js b/Libraries/Inspector.js new file mode 100644 index 000000000..5c70be2e8 --- /dev/null +++ b/Libraries/Inspector.js @@ -0,0 +1,62 @@ +/** + * 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 Inspector + */ +'use strict'; + +var ReactInstanceHandles = require('ReactInstanceHandles'); +var ReactInstanceMap = require('ReactInstanceMap'); +var ReactNativeMount = require('ReactNativeMount'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); + +function traverseOwnerTreeUp(hierarchy, instance) { + if (instance) { + hierarchy.unshift(instance); + traverseOwnerTreeUp(hierarchy, instance._currentElement._owner); + } +} + +function findInstance(component, targetID) { + if (targetID === findRootNodeID(component)) { + return component; + } + if (component._renderedComponent) { + return findInstance(component._renderedComponent, targetID); + } else { + for (var key in component._renderedChildren) { + var child = component._renderedChildren[key]; + if (ReactInstanceHandles.isAncestorIDOf(findRootNodeID(child), targetID)) { + var instance = findInstance(child, targetID); + if (instance) { + return instance; + } + } + } + } +} + +function findRootNodeID(component) { + var internalInstance = ReactInstanceMap.get(component); + return internalInstance ? internalInstance._rootNodeID : component._rootNodeID; +} + +function findInstanceByNativeTag(rootTag, nativeTag) { + var containerID = ReactNativeTagHandles.tagToRootNodeID[rootTag]; + var rootInstance = ReactNativeMount._instancesByContainerID[containerID]; + var targetID = ReactNativeTagHandles.tagToRootNodeID[nativeTag]; + return findInstance(rootInstance, targetID); +} + +function getOwnerHierarchy(instance) { + var hierarchy = []; + traverseOwnerTreeUp(hierarchy, instance); + return hierarchy; +} + +module.exports = {findInstanceByNativeTag, getOwnerHierarchy}; diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequestBase.js index 99a327b5f..3570e4bf2 100644 --- a/Libraries/Network/XMLHttpRequestBase.js +++ b/Libraries/Network/XMLHttpRequestBase.js @@ -23,7 +23,7 @@ class XMLHttpRequestBase { readyState: number; responseHeaders: ?Object; responseText: ?string; - status: ?string; + status: number; _method: ?string; _url: ?string; @@ -43,7 +43,7 @@ class XMLHttpRequestBase { this.readyState = this.UNSENT; this.responseHeaders = undefined; this.responseText = undefined; - this.status = undefined; + this.status = 0; this._method = null; this._url = null; @@ -73,7 +73,7 @@ class XMLHttpRequestBase { } setRequestHeader(header: string, value: any): void { - this._headers[header] = value; + this._headers[header.toLowerCase()] = value; } open(method: string, url: string, async: ?boolean): void { @@ -127,7 +127,7 @@ class XMLHttpRequestBase { this._aborted = true; } - callback(status: string, responseHeaders: ?Object, responseText: string): void { + callback(status: number, responseHeaders: ?Object, responseText: string): void { if (this._aborted) { return; } diff --git a/Libraries/ReactIOS/InspectorOverlay.js b/Libraries/ReactIOS/InspectorOverlay.js new file mode 100644 index 000000000..a70d3b85d --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay.js @@ -0,0 +1,112 @@ +/** + * 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 InspectorOverlay + */ +'use strict'; + +var Dimensions = require('Dimensions'); +var Inspector = require('Inspector'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var UIManager = require('NativeModules').UIManager; +var View = require('View'); + +var InspectorOverlay = React.createClass({ + getInitialState: function() { + return { + frame: null, + hierarchy: [], + }; + }, + + findViewForTouchEvent: function(e) { + var {locationX, locationY} = e.nativeEvent.touches[0]; + UIManager.findSubviewIn( + this.props.inspectedViewTag, + [locationX, locationY], + (nativeViewTag, left, top, width, height) => { + var instance = Inspector.findInstanceByNativeTag(this.props.rootTag, nativeViewTag); + var hierarchy = Inspector.getOwnerHierarchy(instance); + this.setState({ + hierarchy, + frame: {left, top, width, height} + }); + } + ); + }, + + shouldSetResponser: function(e) { + this.findViewForTouchEvent(e); + return true; + }, + + render: function() { + var content = []; + + if (this.state.frame) { + var distanceToTop = this.state.frame.top; + var distanceToBottom = Dimensions.get('window').height - + (this.state.frame.top + this.state.frame.height); + + var justifyContent = distanceToTop > distanceToBottom + ? 'flex-start' + : 'flex-end'; + + content.push(); + content.push(); + } + return ( + + {content} + + ); + } +}); + +var ElementProperties = React.createClass({ + render: function() { + var path = this.props.hierarchy.map((instance) => instance.getName()).join(' > '); + return ( + + + {path} + + + ); + } +}); + +var styles = StyleSheet.create({ + inspector: { + backgroundColor: 'rgba(255,255,255,0.8)', + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + frame: { + position: 'absolute', + backgroundColor: 'rgba(155,155,255,0.3)', + }, + info: { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + padding: 10, + }, + path: { + color: 'white', + fontSize: 9, + } +}); + +module.exports = InspectorOverlay; diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js index 16052c6fa..1cb24aa71 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.ios.js @@ -7,17 +7,59 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule renderApplication - * @flow */ 'use strict'; +var InspectorOverlay = require('InspectorOverlay'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var React = require('React'); var StyleSheet = require('StyleSheet'); +var Subscribable = require('Subscribable'); var View = require('View'); var WarningBox = require('WarningBox'); var invariant = require('invariant'); +var AppContainer = React.createClass({ + mixins: [Subscribable.Mixin], + + getInitialState: function() { + return { inspector: null }; + }, + + toggleElementInspector: function() { + var inspector = this.state.inspector + ? null + : ; + this.setState({inspector}); + }, + + componentDidMount: function() { + this.addListenerOn( + RCTDeviceEventEmitter, + 'toggleElementInspector', + this.toggleElementInspector + ); + }, + + render: function() { + var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled; + var warningBox = shouldRenderWarningBox ? : null; + return ( + + + {this.props.children} + + {warningBox} + {this.state.inspector} + + ); + } +}); + function renderApplication( RootComponent: ReactClass, initialProps: P, @@ -27,15 +69,12 @@ 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 ); } diff --git a/Libraries/ReactIOS/requireNativeComponent.js b/Libraries/ReactIOS/requireNativeComponent.js index c1c3b1376..825739738 100644 --- a/Libraries/ReactIOS/requireNativeComponent.js +++ b/Libraries/ReactIOS/requireNativeComponent.js @@ -21,6 +21,7 @@ var pointsDiffer = require('pointsDiffer'); var matricesDiffer = require('matricesDiffer'); var sizesDiffer = require('sizesDiffer'); var verifyPropTypes = require('verifyPropTypes'); +var warning = require('warning'); /** * Used to create React components that directly wrap native component @@ -42,12 +43,13 @@ function requireNativeComponent( wrapperComponent: ?Function ): Function { var viewConfig = RCTUIManager[viewName]; - if (!viewConfig || !viewConfig.nativeProps) { + if (!viewConfig || !viewConfig.NativeProps) { + warning(false, 'Native component for "%s" does not exist', viewName); return UnimplementedView; } var nativeProps = { - ...RCTUIManager.RCTView.nativeProps, - ...viewConfig.nativeProps, + ...RCTUIManager.RCTView.NativeProps, + ...viewConfig.NativeProps, }; viewConfig.uiViewClassName = viewName; viewConfig.validAttributes = {}; diff --git a/Libraries/ReactIOS/verifyPropTypes.js b/Libraries/ReactIOS/verifyPropTypes.js index 5fd3ea1b8..73cb57e9f 100644 --- a/Libraries/ReactIOS/verifyPropTypes.js +++ b/Libraries/ReactIOS/verifyPropTypes.js @@ -29,7 +29,7 @@ function verifyPropTypes( ); } - var nativeProps = viewConfig.nativeProps; + var nativeProps = viewConfig.NativeProps; for (var prop in nativeProps) { if (!component.propTypes[prop] && !View.propTypes[prop] && diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index 7a7b44cf8..c93f13184 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -14,8 +14,6 @@ extern NSString *const RCTReactTagAttributeName; @interface RCTShadowText : RCTShadowView -@property (nonatomic, assign) NSWritingDirection writingDirection; -@property (nonatomic, strong) UIColor *textBackgroundColor; @property (nonatomic, strong) UIColor *color; @property (nonatomic, copy) NSString *fontFamily; @property (nonatomic, assign) CGFloat fontSize; @@ -24,17 +22,12 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, assign) BOOL isHighlighted; @property (nonatomic, assign) CGFloat letterSpacing; @property (nonatomic, assign) CGFloat lineHeight; -@property (nonatomic, assign) NSUInteger maximumNumberOfLines; +@property (nonatomic, assign) NSUInteger numberOfLines; @property (nonatomic, assign) CGSize shadowOffset; @property (nonatomic, assign) NSTextAlignment textAlign; +@property (nonatomic, strong) UIColor *textBackgroundColor; +@property (nonatomic, assign) NSWritingDirection writingDirection; -// Not exposed to JS -@property (nonatomic, strong) UIFont *font; -@property (nonatomic, assign) NSLineBreakMode truncationMode; -@property (nonatomic, assign) CGFloat effectiveLetterSpacing; - -@property (nonatomic, copy, readonly) NSAttributedString *attributedString; -@property (nonatomic, strong, readonly) NSLayoutManager *layoutManager; -@property (nonatomic, strong, readonly) NSTextContainer *textContainer; +- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width; @end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index b7e6f997e..dd880340d 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -17,62 +17,61 @@ NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; +@implementation RCTShadowText +{ + NSAttributedString *_cachedAttributedString; + CGFloat _effectiveLetterSpacing; +} + 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 ensureLayoutForTextContainer:shadowText.textContainer]; - - CGSize computedSize = [shadowText.layoutManager usedRectForTextContainer:shadowText.textContainer].size; - - [textStorage removeLayoutManager:shadowText.layoutManager]; - if (previousTextStorage) { - [previousTextStorage addLayoutManager:shadowText.layoutManager]; - } + NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width]; + NSLayoutManager *layoutManager = [textStorage.layoutManagers firstObject]; + NSTextContainer *textContainer = [layoutManager.textContainers firstObject]; + CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size; css_dim_t result; result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width); - if (shadowText.effectiveLetterSpacing < 0) { - result.dimensions[CSS_WIDTH] -= shadowText.effectiveLetterSpacing; + if (shadowText->_effectiveLetterSpacing < 0) { + result.dimensions[CSS_WIDTH] -= shadowText->_effectiveLetterSpacing; } result.dimensions[CSS_HEIGHT] = RCTCeilPixelValue(computedSize.height); return result; } -@implementation RCTShadowText -{ - NSLayoutManager *_layoutManager; - NSTextContainer *_textContainer; - NSAttributedString *_cachedAttributedString; - UIFont *_font; -} - - (instancetype)init { if ((self = [super init])) { _fontSize = NAN; _letterSpacing = NAN; _isHighlighted = NO; - - _textContainer = [[NSTextContainer alloc] init]; - _textContainer.lineBreakMode = NSLineBreakByTruncatingTail; - _textContainer.lineFragmentPadding = 0.0; - - _layoutManager = [[NSLayoutManager alloc] init]; - [_layoutManager addTextContainer:_textContainer]; } - return self; } +- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width +{ + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString]; + [textStorage addLayoutManager:layoutManager]; + + NSTextContainer *textContainer = [[NSTextContainer alloc] init]; + textContainer.lineFragmentPadding = 0.0; + textContainer.lineBreakMode = _numberOfLines > 0 ? NSLineBreakByTruncatingTail : NSLineBreakByClipping; + textContainer.maximumNumberOfLines = _numberOfLines; + + UIEdgeInsets padding = self.paddingAsInsets; + width -= (padding.left + padding.right); + textContainer.size = (CGSize){isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX}; + + [layoutManager addTextContainer:textContainer]; + [layoutManager ensureLayoutForTextContainer:textContainer]; + + return textStorage; +} + - (NSAttributedString *)attributedString { return [self _attributedStringWithFontFamily:nil @@ -135,8 +134,8 @@ static css_dim_t RCTMeasure(void *context, float width) [self _addAttribute:NSBackgroundColorAttributeName withValue:self.textBackgroundColor toAttributedString:attributedString]; } - _font = [RCTConvert UIFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:fontStyle]; - [self _addAttribute:NSFontAttributeName withValue:_font toAttributedString:attributedString]; + UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:fontStyle]; + [self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString]; [self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString]; [self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString]; [self _setParagraphStyleOnAttributedString:attributedString]; @@ -148,11 +147,6 @@ static css_dim_t RCTMeasure(void *context, float width) return _cachedAttributedString; } -- (UIFont *)font -{ - return _font ?: [RCTConvert UIFont:nil withFamily:_fontFamily size:@(_fontSize) weight:_fontWeight style:_fontStyle]; -} - - (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttributedString:(NSMutableAttributedString *)attributedString { [attributedString enumerateAttribute:attribute inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { @@ -231,38 +225,18 @@ static css_dim_t RCTMeasure(void *context, float width) [self dirtyText]; \ } -RCT_TEXT_PROPERTY(TextBackgroundColor, _textBackgroundColor, UIColor *); -RCT_TEXT_PROPERTY(Color, _color, UIColor *); -RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *); -RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat); -RCT_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *); -RCT_TEXT_PROPERTY(LetterSpacing, _letterSpacing, CGFloat); -RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat); -RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize); -RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment); -RCT_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL); -RCT_TEXT_PROPERTY(Font, _font, UIFont *); - -- (NSLineBreakMode)truncationMode -{ - return _textContainer.lineBreakMode; -} - -- (void)setTruncationMode:(NSLineBreakMode)truncationMode -{ - _textContainer.lineBreakMode = truncationMode; - [self dirtyText]; -} - -- (NSUInteger)maximumNumberOfLines -{ - return _textContainer.maximumNumberOfLines; -} - -- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines -{ - _textContainer.maximumNumberOfLines = maximumNumberOfLines; - [self dirtyText]; -} +RCT_TEXT_PROPERTY(Color, _color, UIColor *) +RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *) +RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat) +RCT_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *) +RCT_TEXT_PROPERTY(FontStyle, _fontStyle, NSString *) +RCT_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL) +RCT_TEXT_PROPERTY(LetterSpacing, _letterSpacing, CGFloat) +RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat) +RCT_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger) +RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize) +RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment) +RCT_TEXT_PROPERTY(TextBackgroundColor, _textBackgroundColor, UIColor *) +RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection) @end diff --git a/Libraries/Text/RCTText.h b/Libraries/Text/RCTText.h index fe7803eb5..487954f8a 100644 --- a/Libraries/Text/RCTText.h +++ b/Libraries/Text/RCTText.h @@ -11,9 +11,7 @@ @interface RCTText : UIView -@property (nonatomic, strong) NSLayoutManager *layoutManager; -@property (nonatomic, strong) NSTextContainer *textContainer; -@property (nonatomic, copy) NSAttributedString *attributedText; @property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, strong) NSTextStorage *textStorage; @end diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index b4fcc6951..cdb09d4ff 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -15,9 +15,7 @@ @implementation RCTText { - NSLayoutManager *_layoutManager; NSTextStorage *_textStorage; - NSTextContainer *_textContainer; } - (instancetype)initWithFrame:(CGRect)frame @@ -31,103 +29,42 @@ self.opaque = NO; self.contentMode = UIViewContentModeRedraw; } - return self; } -- (NSAttributedString *)attributedText +- (void)setTextStorage:(NSTextStorage *)textStorage { - return [_textStorage copy]; -} - -- (void)setAttributedText:(NSAttributedString *)attributedText -{ - for (NSLayoutManager *existingLayoutManager in _textStorage.layoutManagers) { - [_textStorage removeLayoutManager:existingLayoutManager]; - } - - _textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedText]; - - if (_layoutManager) { - [_textStorage addLayoutManager:_layoutManager]; - } - + _textStorage = textStorage; [self setNeedsDisplay]; } -- (void)setTextContainer:(NSTextContainer *)textContainer -{ - if ([_textContainer isEqual:textContainer]) { - return; - } - - _textContainer = textContainer; - - for (NSInteger i = _layoutManager.textContainers.count - 1; i >= 0; i--) { - [_layoutManager removeTextContainerAtIndex:i]; - } - - if (_textContainer) { - [_layoutManager addTextContainer:_textContainer]; - } - - [self setNeedsDisplay]; -} - -- (void)setLayoutManager:(NSLayoutManager *)layoutManager -{ - if ([_layoutManager isEqual:layoutManager]) { - return; - } - - _layoutManager = layoutManager; - - for (NSLayoutManager *existingLayoutManager in _textStorage.layoutManagers) { - [_textStorage removeLayoutManager:existingLayoutManager]; - } - - if (_layoutManager) { - [_textStorage addLayoutManager:_layoutManager]; - } - - [self setNeedsDisplay]; -} - -- (CGRect)textFrame -{ - return UIEdgeInsetsInsetRect(self.bounds, _contentInset); -} - - (void)drawRect:(CGRect)rect { - CGRect textFrame = [self textFrame]; - - // 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); - - NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer]; - [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin]; - [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin]; + NSLayoutManager *layoutManager = [_textStorage.layoutManagers firstObject]; + NSTextContainer *textContainer = [layoutManager.textContainers firstObject]; + CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, _contentInset); + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin]; + [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin]; } - (NSNumber *)reactTagAtPoint:(CGPoint)point { - CGFloat fraction; - NSUInteger characterIndex = [_layoutManager characterIndexForPoint:point - inTextContainer:_textContainer - fractionOfDistanceBetweenInsertionPoints:&fraction]; + NSNumber *reactTag = self.reactTag; - NSNumber *reactTag = nil; + CGFloat fraction; + NSLayoutManager *layoutManager = [_textStorage.layoutManagers firstObject]; + NSTextContainer *textContainer = [layoutManager.textContainers firstObject]; + NSUInteger characterIndex = [layoutManager characterIndexForPoint:point + inTextContainer:textContainer + fractionOfDistanceBetweenInsertionPoints:&fraction]; // If the point is not before (fraction == 0.0) the first character and not // after (fraction == 1.0) the last character, then the attribute is valid. if (_textStorage.length > 0 && (fraction > 0 || characterIndex > 0) && (fraction < 1 || characterIndex < _textStorage.length - 1)) { reactTag = [_textStorage attribute:RCTReactTagAttributeName atIndex:characterIndex effectiveRange:NULL]; } - - return reactTag ?: self.reactTag; + return reactTag; } #pragma mark - Accessibility diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index b2832a800..3dbb2ed53 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -47,24 +47,11 @@ RCT_EXPORT_SHADOW_PROPERTY(fontStyle, NSString) RCT_EXPORT_SHADOW_PROPERTY(isHighlighted, BOOL) RCT_EXPORT_SHADOW_PROPERTY(letterSpacing, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat) -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) -RCT_CUSTOM_SHADOW_PROPERTY(containerBackgroundColor, UIColor, RCTShadowText) -{ - view.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; - view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; -} -RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) -{ - NSLineBreakMode truncationMode = NSLineBreakByClipping; - view.maximumNumberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.maximumNumberOfLines; - if (view.maximumNumberOfLines > 0) { - truncationMode = NSLineBreakByTruncatingTail; - } - view.truncationMode = truncationMode; -} +RCT_REMAP_SHADOW_PROPERTY(containerBackgroundColor, backgroundColor, UIColor) +RCT_EXPORT_SHADOW_PROPERTY(numberOfLines, NSUInteger) - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { @@ -81,7 +68,7 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) continue; } - RCTSparseArray *reactTaggedAttributedStrings = [[RCTSparseArray alloc] init]; + RCTSparseArray *reactTaggedTextStorage = [[RCTSparseArray alloc] init]; NSMutableArray *queue = [NSMutableArray arrayWithObject:rootView]; for (NSInteger i = 0; i < [queue count]; i++) { RCTShadowView *shadowView = queue[i]; @@ -89,9 +76,11 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) if ([shadowView isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)shadowView; - reactTaggedAttributedStrings[shadowText.reactTag] = [shadowText attributedString]; + NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:shadowView.frame.size.width]; + reactTaggedTextStorage[shadowText.reactTag] = textStorage; } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { - RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); + RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", + [(RCTShadowRawText *)shadowView text]); } else { for (RCTShadowView *child in [shadowView reactSubviews]) { if ([child isTextDirty]) { @@ -104,9 +93,9 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) } [uiBlocks addObject:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - [reactTaggedAttributedStrings enumerateObjectsUsingBlock:^(NSAttributedString *attributedString, NSNumber *reactTag, BOOL *stop) { + [reactTaggedTextStorage enumerateObjectsUsingBlock:^(NSTextStorage *textStorage, NSNumber *reactTag, BOOL *stop) { RCTText *text = viewRegistry[reactTag]; - text.attributedText = attributedString; + text.textStorage = textStorage; }]; }]; } @@ -126,8 +115,6 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTText *text = viewRegistry[reactTag]; text.contentInset = padding; - text.layoutManager = shadowView.layoutManager; - text.textContainer = shadowView.textContainer; }; } diff --git a/Libraries/vendor/crypto/crc32.js b/Libraries/vendor/crypto/crc32.js index 1ded3fddf..540d60aa8 100644 --- a/Libraries/vendor/crypto/crc32.js +++ b/Libraries/vendor/crypto/crc32.js @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<9bce659a43d6f6115b20a18f6c995d8a>> + * @generated SignedSource<<77bdeb858138636c96c405d64b6be55c>> * * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! * !! This file is a check-in of a static_upstream project! !! @@ -20,57 +20,73 @@ /* jslint bitwise: true */ /** - * Copyright (c) 2006 Andrea Ercolino - * http://www.opensource.org/licenses/mit-license.php + * Modified from the original for performance improvements. + * + * @see http://create.stephan-brumme.com/crc32/ + * @see http://stackoverflow.com/questions/18638900/ + * @copyright 2006 Andrea Ercolino + * @license MIT */ -var table = '00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 ' + - '9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 ' + - '90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 ' + - '83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 ' + - '8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD ' + - 'A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ' + - 'ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 ' + - 'B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB ' + - 'B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 ' + - 'E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 ' + - 'E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 ' + - 'F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 ' + - 'FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D ' + - 'D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F ' + - 'DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 ' + - 'CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B ' + - 'C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 ' + - '73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 ' + - '7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 ' + - '6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 ' + - '60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD ' + - '48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF ' + - '4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 ' + - '5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B ' + - '5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 ' + - '05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 ' + - '0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 ' + - '18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 ' + - '166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D ' + - '3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F ' + - '30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 ' + - '23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B ' + - '2D02EF8D'; + +var table = [ + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D +]; + +if (global.Int32Array !== undefined) { + table = new Int32Array(table); +} /** * @returns Number */ function crc32(str) { - var crc = 0; - var n = 0; - var x = 0; - crc = crc ^ (-1); - for (var i = 0, iTop = str.length; i < iTop; i++) { - n = (crc ^ str.charCodeAt(i)) & 0xFF; - x = "0x" + table.substr(n * 9, 8); - crc = (crc >>> 8) ^ x; + var crc = -1; + for (var i = 0, len = str.length; i < len; i++) { + crc = (crc >>> 8) ^ table[(crc ^ str.charCodeAt(i)) & 0xFF]; } - return crc ^ (-1); + return ~crc; } module.exports = crc32; diff --git a/React/Base/RCTAssert.h b/React/Base/RCTAssert.h index b0a3c5c52..1fc5b9d32 100644 --- a/React/Base/RCTAssert.h +++ b/React/Base/RCTAssert.h @@ -62,3 +62,31 @@ RCT_EXTERN RCTAssertFunction RCTGetAssertFunction(void); * assert info to an extra service without changing the default behavior. */ RCT_EXTERN void RCTAddAssertFunction(RCTAssertFunction assertFunction); + +/** + * Get the current thread's name (or the current queue, if in debug mode) + */ +RCT_EXTERN NSString *RCTCurrentThreadName(void); + +/** + * Convenience macro to assert which thread is currently running (DEBUG mode only) + */ +#if DEBUG + +#define RCTAssertThread(thread, format...) \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") \ +RCTAssert( \ + [(id)thread isKindOfClass:[NSString class]] ? \ + [RCTCurrentThreadName() isEqualToString:(NSString *)thread] : \ + [(id)thread isKindOfClass:[NSThread class]] ? \ + [NSThread currentThread] == (NSThread *)thread : \ + dispatch_get_current_queue() == (dispatch_queue_t)thread, \ + format); \ +_Pragma("clang diagnostic pop") + +#else + +#define RCTAssertThread(thread, format...) + +#endif diff --git a/React/Base/RCTAssert.m b/React/Base/RCTAssert.m index 86d71cd80..41369406c 100644 --- a/React/Base/RCTAssert.m +++ b/React/Base/RCTAssert.m @@ -60,3 +60,20 @@ void RCTAddAssertFunction(RCTAssertFunction assertFunction) RCTCurrentAssertFunction = assertFunction; } } + +NSString *RCTCurrentThreadName(void) +{ + NSThread *thread = [NSThread currentThread]; + 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; +} diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 7c8af00cc..5f84ed9da 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -95,6 +95,11 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method; /** * The event dispatcher is a wrapper around -enqueueJSCall:args: that provides a * higher-level interface for sending UI events such as touches and text input. + * + * NOTE: RCTEventDispatcher is now a bridge module, this is implemented as a + * category but remains declared in the bridge to avoid breaking changes + * + * To be moved. */ @property (nonatomic, readonly) RCTEventDispatcher *eventDispatcher; diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 523333aac..47c7f5942 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -18,7 +18,6 @@ #import "RCTContextExecutor.h" #import "RCTConvert.h" -#import "RCTEventDispatcher.h" #import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" @@ -211,7 +210,6 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) @property (nonatomic, strong) RCTBatchedBridge *batchedBridge; @property (nonatomic, strong) RCTBridgeModuleProviderBlock moduleProvider; -@property (nonatomic, strong, readwrite) RCTEventDispatcher *eventDispatcher; - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method @@ -875,11 +873,6 @@ static id _latestJSExecutor; return _batchedBridge.modules; } -- (RCTEventDispatcher *)eventDispatcher -{ - return _eventDispatcher ?: _batchedBridge.eventDispatcher; -} - #define RCT_INNER_BRIDGE_ONLY(...) \ - (void)__VA_ARGS__ \ { \ @@ -943,11 +936,6 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin _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 diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 421380e5b..2416e974c 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -11,6 +11,7 @@ #import "RCTBridge.h" #import "RCTDefines.h" +#import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" #import "RCTPerfStats.h" @@ -241,6 +242,8 @@ RCT_EXPORT_METHOD(show) destructiveButtonTitle:nil otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil]; + [actionSheet addButtonWithTitle:@"Inspect Element"]; + if (_liveReloadURL) { NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; @@ -300,10 +303,14 @@ RCT_EXPORT_METHOD(reload) break; } case 4: { - self.liveReloadEnabled = !_liveReloadEnabled; + [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; break; } case 5: { + self.liveReloadEnabled = !_liveReloadEnabled; + break; + } + case 6: { self.profilingEnabled = !_profilingEnabled; break; } diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 15cb18021..5576df64f 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -9,7 +9,7 @@ #import -@class RCTBridge; +#import "RCTBridge.h" typedef NS_ENUM(NSInteger, RCTTextEventType) { RCTTextEventTypeFocus, @@ -28,14 +28,36 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { RCTScrollEventTypeEndAnimation, }; +@protocol RCTEvent + +@required + +@property (nonatomic, strong, readonly) NSNumber *viewTag; +@property (nonatomic, copy, readonly) NSString *eventName; +@property (nonatomic, copy, readonly) NSDictionary *body; +@property (nonatomic, assign, readonly) uint16_t coalescingKey; + +- (BOOL)canCoalesce; +- (id)coalesceWithEvent:(id)newEvent; + ++ (NSString *)moduleDotMethod; + +@end + +@interface RCTBaseEvent : NSObject + +- (instancetype)initWithViewTag:(NSNumber *)viewTag + eventName:(NSString *)eventName + body:(NSDictionary *)body NS_DESIGNATED_INITIALIZER; + +@end + /** * This class wraps the -[RCTBridge enqueueJSCall:args:] method, and * provides some convenience methods for generating event calls. */ @interface RCTEventDispatcher : NSObject -- (instancetype)initWithBridge:(RCTBridge *)bridge; - /** * Send an application-specific event that does not relate to a specific * view, e.g. a navigation or data update notification. @@ -61,13 +83,6 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { reactTag:(NSNumber *)reactTag text:(NSString *)text; -/** - * Send a scroll event. - * (You can send a fake scroll event by passing nil for scrollView). - */ -- (void)sendScrollEventWithType:(RCTScrollEventType)type - reactTag:(NSNumber *)reactTag - scrollView:(UIScrollView *)scrollView - userData:(NSDictionary *)userData; +- (void)sendEvent:(id)event; @end diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 8487556e5..e6ed698d9 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -11,16 +11,76 @@ #import "RCTAssert.h" #import "RCTBridge.h" +#import "RCTSparseArray.h" + +static uint64_t RCTGetEventID(id event) +{ + return ( + [event.viewTag intValue] | + (((uint64_t)event.eventName.hash & 0xFFFF) << 32) | + (((uint64_t)event.coalescingKey) << 48) + ); +} + +@implementation RCTBaseEvent + +@synthesize viewTag = _viewTag; +@synthesize eventName = _eventName; +@synthesize body = _body; + +- (instancetype)initWithViewTag:(NSNumber *)viewTag + eventName:(NSString *)eventName + body:(NSDictionary *)body +{ + if ((self = [super init])) { + _viewTag = viewTag; + _eventName = eventName; + _body = body; + } + return self; +} + +- (uint16_t)coalescingKey +{ + return 0; +} + +- (BOOL)canCoalesce +{ + return YES; +} + +- (id)coalesceWithEvent:(id)newEvent +{ + return newEvent; +} + ++ (NSString *)moduleDotMethod +{ + return nil; +} + +@end + +@interface RCTEventDispatcher() + +@end @implementation RCTEventDispatcher { - RCTBridge __weak *_bridge; + RCTSparseArray *_eventQueue; + NSLock *_eventQueueLock; } -- (instancetype)initWithBridge:(RCTBridge *)bridge +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (instancetype)init { if ((self = [super init])) { - _bridge = bridge; + _eventQueue = [[RCTSparseArray alloc] init]; + _eventQueueLock = [[NSLock alloc] init]; } return self; } @@ -70,58 +130,71 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveEvent); }]; } -/** - * TODO: throttling - * NOTE: the old system used a per-scrollview throttling - * which would be fairly easy to re-implement if needed, - * but this is non-optimal as it leads to degradation in - * scroll responsiveness. A better solution would be to - * coalesce multiple scroll events into a single batch. - */ -- (void)sendScrollEventWithType:(RCTScrollEventType)type - reactTag:(NSNumber *)reactTag - scrollView:(UIScrollView *)scrollView - userData:(NSDictionary *)userData +- (void)sendEvent:(id)event { - static NSString *events[] = { - @"topScrollBeginDrag", - @"topScroll", - @"topScrollEndDrag", - @"topMomentumScrollBegin", - @"topMomentumScrollEnd", - @"topScrollAnimationEnd", - }; - - NSDictionary *body = @{ - @"contentOffset": @{ - @"x": @(scrollView.contentOffset.x), - @"y": @(scrollView.contentOffset.y) - }, - @"contentInset": @{ - @"top": @(scrollView.contentInset.top), - @"left": @(scrollView.contentInset.left), - @"bottom": @(scrollView.contentInset.bottom), - @"right": @(scrollView.contentInset.right) - }, - @"contentSize": @{ - @"width": @(scrollView.contentSize.width), - @"height": @(scrollView.contentSize.height) - }, - @"layoutMeasurement": @{ - @"width": @(scrollView.frame.size.width), - @"height": @(scrollView.frame.size.height) - }, - @"zoomScale": @(scrollView.zoomScale ?: 1), - @"target": reactTag - }; - - if (userData) { - NSMutableDictionary *mutableBody = [body mutableCopy]; - [mutableBody addEntriesFromDictionary:userData]; - body = mutableBody; + if (!event.canCoalesce) { + [self dispatchEvent:event]; + return; } - [self sendInputEventWithName:events[type] body:body]; + [_eventQueueLock lock]; + + uint64_t eventID = RCTGetEventID(event); + id previousEvent = _eventQueue[eventID]; + + if (previousEvent) { + event = [previousEvent coalesceWithEvent:event]; + } + + _eventQueue[eventID] = event; + + [_eventQueueLock unlock]; +} + +- (void)dispatchEvent:(id)event +{ + NSMutableArray *arguments = [[NSMutableArray alloc] init]; + + if (event.viewTag) { + [arguments addObject:event.viewTag]; + } + + [arguments addObject:event.eventName]; + + if (event.body) { + [arguments addObject:event.body]; + } + + [_bridge enqueueJSCall:[[event class] moduleDotMethod] + args:arguments]; +} + +- (dispatch_queue_t)methodQueue +{ + return RCTJSThread; +} + +- (void)didUpdateFrame:(RCTFrameUpdate *)update +{ + RCTSparseArray *eventQueue; + + [_eventQueueLock lock]; + eventQueue = _eventQueue; + _eventQueue = [[RCTSparseArray alloc] init]; + [_eventQueueLock unlock]; + + for (id event in eventQueue.allObjects) { + [self dispatchEvent:event]; + } +} + +@end + +@implementation RCTBridge (RCTEventDispatcher) + +- (RCTEventDispatcher *)eventDispatcher +{ + return self.modules[RCTBridgeModuleNameForClass([RCTEventDispatcher class])]; } @end diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 75cbe722e..5dead10be 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -42,18 +42,12 @@ 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. */ RCT_EXTERN NSString *RCTFormatLog( NSDate *timestamp, - NSThread *thread, RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index fb70fe6d3..d99bb4fe1 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -53,7 +53,7 @@ RCTLogFunction RCTDefaultLogFunction = ^( ) { NSString *log = RCTFormatLog( - [NSDate date], [NSThread currentThread], level, fileName, lineNumber, message + [NSDate date], level, fileName, lineNumber, message ); fprintf(stderr, "%s\n", log.UTF8String); fflush(stderr); @@ -99,25 +99,8 @@ 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, RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, @@ -137,9 +120,9 @@ NSString *RCTFormatLog( if (level) { [log appendFormat:@"[%s]", RCTLogLevels[level - 1]]; } - if (thread) { - [log appendFormat:@"[tid:%@]", RCTThreadName(thread)]; - } + + [log appendFormat:@"[tid:%@]", RCTCurrentThreadName()]; + if (fileName) { fileName = [fileName lastPathComponent]; if (lineNumber) { diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index 19c6900c7..29e606e86 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -13,8 +13,8 @@ #import +#import "RCTAssert.h" #import "RCTDefines.h" -#import "RCTLog.h" #import "RCTUtils.h" #if RCT_DEV @@ -43,7 +43,7 @@ NSLock *_RCTProfileLock; #define RCTProfileAddEvent(type, props...) \ [RCTProfileInfo[type] addObject:@{ \ @"pid": @([[NSProcessInfo processInfo] processIdentifier]), \ - @"tid": RCTThreadName([NSThread currentThread]), \ + @"tid": RCTCurrentThreadName(), \ props \ }]; diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 980d253d3..e9a8170eb 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -51,7 +51,7 @@ RCTRootContentView *_contentView; } - - (instancetype)initWithBridge:(RCTBridge *)bridge +- (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName { RCTAssertMainThread(); @@ -87,6 +87,12 @@ return [self initWithBridge:bridge moduleName:moduleName]; } +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + super.backgroundColor = backgroundColor; + _contentView.backgroundColor = backgroundColor; +} + - (UIViewController *)backingViewController { return _backingViewController ?: [super backingViewController]; @@ -100,7 +106,6 @@ RCT_IMPORT_METHOD(AppRegistry, runApplication) RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) - - (void)javaScriptDidLoad:(NSNotification *)notification { RCTBridge *bridge = notification.userInfo[@"bridge"]; @@ -124,6 +129,7 @@ RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) [_contentView removeFromSuperview]; _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds bridge:bridge]; + _contentView.backgroundColor = self.backgroundColor; [self addSubview:_contentView]; NSString *moduleName = _moduleName ?: @""; @@ -172,6 +178,7 @@ RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) { __weak RCTBridge *_bridge; RCTTouchHandler *_touchHandler; + UIColor *_backgroundColor; } - (instancetype)initWithFrame:(CGRect)frame @@ -181,18 +188,32 @@ RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) _bridge = bridge; [self setUp]; self.frame = frame; + self.layer.backgroundColor = NULL; } return self; } - (void)setFrame:(CGRect)frame { - [super setFrame:frame]; + super.frame = frame; if (self.reactTag && _bridge.isValid) { - [_bridge.uiManager setFrame:self.bounds forRootView:self]; + [_bridge.uiManager setFrame:frame forRootView:self]; } } +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + _backgroundColor = backgroundColor; + if (self.reactTag && _bridge.isValid) { + [_bridge.uiManager setBackgroundColor:backgroundColor forRootView:self]; + } +} + +- (UIColor *)backgroundColor +{ + return _backgroundColor; +} + - (void)setUp { /** diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index ab5efe5a6..09cfd758f 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -66,6 +66,7 @@ { RCTJavaScriptContext *_context; NSThread *_javaScriptThread; + JSValueRef _undefined; } /** @@ -237,6 +238,9 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) JSContextGroupRelease(group); } + // Constant value used for comparison + _undefined = JSValueMakeUndefined(ctx); + strongSelf->_context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx]; [strongSelf _addNativeHook:RCTNativeLoggingHook withName:"nativeLoggingHook"]; [strongSelf _addNativeHook:RCTNoop withName:"noop"]; @@ -291,21 +295,77 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) return; } NSError *error; - NSString *argsString = RCTJSONStringify(arguments, &error); + NSString *argsString = (arguments.count == 1) ? RCTJSONStringify(arguments[0], &error) : RCTJSONStringify(arguments, &error); if (!argsString) { RCTLogError(@"Cannot convert argument to string: %@", error); onComplete(nil, error); return; } - NSString *execString = [NSString stringWithFormat:@"require('%@').%@.apply(null, %@);", name, method, argsString]; - JSValueRef jsError = NULL; - JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)execString); - JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, NULL, 0, &jsError); - JSStringRelease(execJSString); + JSValueRef errorJSRef = NULL; + JSValueRef resultJSRef = NULL; + JSGlobalContextRef contextJSRef = JSContextGetGlobalContext(strongSelf->_context.ctx); + JSObjectRef globalObjectJSRef = JSContextGetGlobalObject(strongSelf->_context.ctx); - if (!result) { - onComplete(nil, RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError)); + // get require + JSStringRef requireNameJSStringRef = JSStringCreateWithUTF8CString("require"); + JSValueRef requireJSRef = JSObjectGetProperty(contextJSRef, globalObjectJSRef, requireNameJSStringRef, &errorJSRef); + JSStringRelease(requireNameJSStringRef); + + if (requireJSRef != NULL && requireJSRef != _undefined && errorJSRef == NULL) { + + // get module + JSStringRef moduleNameJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)name); + JSValueRef moduleNameJSRef = JSValueMakeString(contextJSRef, moduleNameJSStringRef); + JSValueRef moduleJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)requireJSRef, NULL, 1, (const JSValueRef *)&moduleNameJSRef, &errorJSRef); + JSStringRelease(moduleNameJSStringRef); + + if (moduleJSRef != NULL && errorJSRef == NULL) { + + // get method + JSStringRef methodNameJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)method); + JSValueRef methodJSRef = JSObjectGetProperty(contextJSRef, (JSObjectRef)moduleJSRef, methodNameJSStringRef, &errorJSRef); + JSStringRelease(methodNameJSStringRef); + + if (methodJSRef != NULL && errorJSRef == NULL) { + + // direct method invoke with no arguments + if (arguments.count == 0) { + resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, 0, NULL, &errorJSRef); + } + + // direct method invoke with 1 argument + else if(arguments.count == 1) { + JSStringRef argsJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)argsString); + JSValueRef argsJSRef = JSValueMakeFromJSONString(contextJSRef, argsJSStringRef); + resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, 1, &argsJSRef, &errorJSRef); + JSStringRelease(argsJSStringRef); + + } else { + // apply invoke with array of arguments + JSStringRef applyNameJSStringRef = JSStringCreateWithUTF8CString("apply"); + JSValueRef applyJSRef = JSObjectGetProperty(contextJSRef, (JSObjectRef)methodJSRef, applyNameJSStringRef, &errorJSRef); + JSStringRelease(applyNameJSStringRef); + + if (applyJSRef != NULL && errorJSRef == NULL) { + // invoke apply + JSStringRef argsJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)argsString); + JSValueRef argsJSRef = JSValueMakeFromJSONString(contextJSRef, argsJSStringRef); + + JSValueRef args[2]; + args[0] = JSValueMakeNull(contextJSRef); + args[1] = argsJSRef; + + resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)applyJSRef, (JSObjectRef)methodJSRef, 2, args, &errorJSRef); + JSStringRelease(argsJSStringRef); + } + } + } + } + } + + if (!resultJSRef) { + onComplete(nil, RCTNSErrorFromJSError(contextJSRef, errorJSRef)); return; } @@ -315,8 +375,8 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) id objcValue; // We often return `null` from JS when there is nothing for native side. JSONKit takes an extra hundred microseconds // to handle this simple case, so we are adding a shortcut to make executeJSCall method even faster - if (!JSValueIsNull(strongSelf->_context.ctx, result)) { - JSStringRef jsJSONString = JSValueCreateJSONString(strongSelf->_context.ctx, result, 0, nil); + if (!JSValueIsNull(contextJSRef, resultJSRef)) { + JSStringRef jsJSONString = JSValueCreateJSONString(contextJSRef, resultJSRef, 0, nil); if (jsJSONString) { NSString *objcJSONString = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, jsJSONString); JSStringRelease(jsJSONString); diff --git a/React/Modules/RCTAppState.m b/React/Modules/RCTAppState.m index 9743e5042..8c46655c6 100644 --- a/React/Modules/RCTAppState.m +++ b/React/Modules/RCTAppState.m @@ -53,11 +53,21 @@ RCT_EXPORT_MODULE() selector:@selector(handleAppStateDidChange) name:name object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleMemoryWarning) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; } } return self; } +- (void)handleMemoryWarning +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"memoryWarning" + body:nil]; +} - (void)dealloc { diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index 6fa0e3249..829c1aeff 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -36,10 +36,16 @@ /** * Update the frame of a root view. This might be in response to a screen rotation - * or some other layout event outsde of the React-managed view hierarchy. + * or some other layout event outside of the React-managed view hierarchy. */ - (void)setFrame:(CGRect)frame forRootView:(UIView *)rootView; +/** + * Update the background color of a root view. This is usually triggered by + * manually setting the background color of the root view with native code. + */ +- (void)setBackgroundColor:(UIColor *)color forRootView:(UIView *)rootView; + /** * Schedule a block to be executed on the UI thread. Useful if you need to execute * view logic after all currently queued view updates have completed. diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index e9734c204..0edca85d9 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -338,17 +338,14 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa _viewRegistry[reactTag] = rootView; CGRect frame = rootView.frame; - // Register manager (TODO: should we do this, or leave it nil?) - _viewManagerRegistry[reactTag] = _viewManagers[@"RCTView"]; - // Register shadow view dispatch_async(_shadowQueue, ^{ RCTShadowView *shadowView = [[RCTShadowView alloc] init]; shadowView.reactTag = reactTag; shadowView.frame = frame; - shadowView.backgroundColor = [UIColor whiteColor]; + shadowView.backgroundColor = rootView.backgroundColor; + shadowView.viewName = NSStringFromClass([rootView class]); _shadowViewRegistry[shadowView.reactTag] = shadowView; - [_rootViewTags addObject:reactTag]; }); } @@ -372,6 +369,22 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa }); } +- (void)setBackgroundColor:(UIColor *)color forRootView:(UIView *)rootView +{ + RCTAssertMainThread(); + + NSNumber *reactTag = rootView.reactTag; + RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag); + + dispatch_async(_shadowQueue, ^{ + RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; + RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); + rootShadowView.backgroundColor = color; + [self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootShadowView]; + [self flushUIBlocks]; + }); +} + /** * Unregisters views from registries */ @@ -390,6 +403,14 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa - (void)addUIBlock:(RCTViewManagerUIBlock)block { + RCTAssertThread(_shadowQueue, + @"-[RCTUIManager addUIBlock:] should only be called from the " + "UIManager's _shadowQueue (it may be accessed via `bridge.uiManager.methodQueue`)"); + + if (!block) { + return; + } + if (!self.isValid) { return; } @@ -795,6 +816,11 @@ RCT_EXPORT_METHOD(createView:(NSNumber *)reactTag } _shadowViewRegistry[reactTag] = shadowView; + // Shadow view is the source of truth for background color this is a little + // bit counter-intuitive if people try to set background color when setting up + // the view, but it's the only way that makes sense given our threading model + UIColor *backgroundColor = shadowView.backgroundColor; + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTAssertMainThread(); @@ -803,14 +829,15 @@ RCT_EXPORT_METHOD(createView:(NSNumber *)reactTag // Generate default view, used for resetting default props if (!uiManager->_defaultViews[viewName]) { - // Note the default is setup after the props are read for the first time ever - // for this className - this is ok because we only use the default for restoring - // defaults, which never happens on first creation. + // Note the default is setup after the props are read for the first time + // ever for this className - this is ok because we only use the default + // for restoring defaults, which never happens on first creation. uiManager->_defaultViews[viewName] = [manager view]; } // Set properties view.reactTag = reactTag; + view.backgroundColor = backgroundColor; if ([view isKindOfClass:[UIView class]]) { view.multipleTouchEnabled = YES; view.userInteractionEnabled = YES; // required for touch handling @@ -859,17 +886,33 @@ RCT_EXPORT_METHOD(blur:(NSNumber *)reactTag) }]; } -- (void)batchDidComplete -{ - // Gather blocks to be executed now that all view hierarchy manipulations have - // been completed (note that these may still take place before layout has finished) - for (RCTViewManager *manager in _viewManagers.allValues) { - RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; - if (uiBlock) { - [self addUIBlock:uiBlock]; - } +RCT_EXPORT_METHOD(findSubviewIn:(NSNumber *)reactTag atPoint:(CGPoint)point callback:(RCTResponseSenderBlock)callback) { + if (!reactTag) { + callback(@[[NSNull null]]); + return; } + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + UIView *view = viewRegistry[reactTag]; + UIView *target = [view hitTest:point withEvent:nil]; + CGRect frame = [target convertRect:target.bounds toView:view]; + + while (target.reactTag == nil && target.superview != nil) { + target = [target superview]; + } + + callback(@[ + target.reactTag ?: [NSNull null], + @(frame.origin.x), + @(frame.origin.y), + @(frame.size.width), + @(frame.size.height), + ]); + }]; +} + +- (void)batchDidComplete +{ // Set up next layout animation if (_nextLayoutAnimation) { RCTLayoutAnimation *layoutAnimation = _nextLayoutAnimation; @@ -893,6 +936,12 @@ RCT_EXPORT_METHOD(blur:(NSNumber *)reactTag) _nextLayoutAnimation = nil; } + // Gather blocks to be executed now that layout is completed + for (RCTViewManager *manager in _viewManagers.allValues) { + RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; + [self addUIBlock:uiBlock]; + } + [self flushUIBlocks]; } @@ -1433,7 +1482,7 @@ RCT_EXPORT_METHOD(clearJSResponder) } // Add native props - constantsNamespace[@"nativeProps"] = _viewConfigs[name]; + constantsNamespace[@"NativeProps"] = _viewConfigs[name]; allJSConstants[name] = [constantsNamespace copy]; }]; diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 57415fbb7..381846569 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -15,6 +15,7 @@ #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTNavItem.h" +#import "RCTScrollView.h" #import "RCTUtils.h" #import "RCTView.h" #import "RCTWrapperViewController.h" diff --git a/React/Views/RCTScrollView.h b/React/Views/RCTScrollView.h index 0333a38a7..e5c1d550a 100644 --- a/React/Views/RCTScrollView.h +++ b/React/Views/RCTScrollView.h @@ -10,13 +10,12 @@ #import #import "RCTAutoInsetsProtocol.h" +#import "RCTEventDispatcher.h" #import "RCTScrollableProtocol.h" #import "RCTView.h" @protocol UIScrollViewDelegate; -@class RCTEventDispatcher; - @interface RCTScrollView : RCTView - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; @@ -48,3 +47,16 @@ @property (nonatomic, copy) NSIndexSet *stickyHeaderIndices; @end + +@interface RCTEventDispatcher (RCTScrollView) + +/** + * Send a scroll event. + * (You can send a fake scroll event by passing nil for scrollView). + */ +- (void)sendScrollEventWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData; + +@end diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index d4a2ad5e9..e6fdb83ca 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -21,6 +21,107 @@ CGFloat const ZINDEX_DEFAULT = 0; CGFloat const ZINDEX_STICKY_HEADER = 50; +@interface RCTScrollEvent : NSObject + +- (instancetype)initWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData NS_DESIGNATED_INITIALIZER; + +@end + +@implementation RCTScrollEvent +{ + RCTScrollEventType _type; + UIScrollView *_scrollView; + NSDictionary *_userData; +} + +@synthesize viewTag = _viewTag; + +- (instancetype)initWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData +{ + if (self = [super init]) { + _type = type; + _viewTag = reactTag; + _scrollView = scrollView; + _userData = userData; + } + return self; +} + +- (uint16_t)coalescingKey +{ + return 0; +} + +- (NSDictionary *)body +{ + NSDictionary *body = @{ + @"contentOffset": @{ + @"x": @(_scrollView.contentOffset.x), + @"y": @(_scrollView.contentOffset.y) + }, + @"contentInset": @{ + @"top": @(_scrollView.contentInset.top), + @"left": @(_scrollView.contentInset.left), + @"bottom": @(_scrollView.contentInset.bottom), + @"right": @(_scrollView.contentInset.right) + }, + @"contentSize": @{ + @"width": @(_scrollView.contentSize.width), + @"height": @(_scrollView.contentSize.height) + }, + @"layoutMeasurement": @{ + @"width": @(_scrollView.frame.size.width), + @"height": @(_scrollView.frame.size.height) + }, + @"zoomScale": @(_scrollView.zoomScale ?: 1), + }; + + if (_userData) { + NSMutableDictionary *mutableBody = [body mutableCopy]; + [mutableBody addEntriesFromDictionary:_userData]; + body = mutableBody; + } + + return body; +} + +- (NSString *)eventName +{ + static NSString *events[] = { + @"topScrollBeginDrag", + @"topScroll", + @"topScrollEndDrag", + @"topMomentumScrollBegin", + @"topMomentumScrollEnd", + @"topScrollAnimationEnd", + }; + + return events[_type]; +} + +- (BOOL)canCoalesce +{ + return YES; +} + +- (id)coalesceWithEvent:(id)newEvent +{ + return newEvent; +} + ++ (NSString *)moduleDotMethod +{ + return @"RCTEventEmitter.receiveEvent"; +} + +@end + /** * Include a custom scroll view subclass because we want to limit certain * default UIKit behaviors such as textFields automatically scrolling @@ -223,6 +324,24 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; } } +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + __block UIView *stickyHeader; + + [_stickyHeaderIndices enumerateIndexesWithOptions:0 usingBlock:^(NSUInteger idx, BOOL *stop) { + stickyHeader = [self contentView].reactSubviews[idx]; + CGPoint convertedPoint = [stickyHeader convertPoint:point fromView:self]; + + if ([stickyHeader hitTest:convertedPoint withEvent:event]) { + *stop = YES; + } else { + stickyHeader = nil; + } + }]; + + return stickyHeader ?: [super hitTest:point withEvent:event]; +} + @end @implementation RCTScrollView @@ -424,6 +543,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) reactTag:self.reactTag scrollView:scrollView userData:userData]; + // Update dispatch time _lastScrollDispatchTime = now; _allowNextScrollNoMatterWhat = NO; @@ -612,3 +732,19 @@ RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, UIEdgeInsets); } @end + +@implementation RCTEventDispatcher (RCTScrollView) + +- (void)sendScrollEventWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData +{ + RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithType:type + reactTag:reactTag + scrollView:scrollView + userData:userData]; + [self sendEvent:scrollEvent]; +} + +@end diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 83350ac46..c2e10750d 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -38,7 +38,6 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); @property (nonatomic, weak, readonly) RCTShadowView *superview; @property (nonatomic, assign, readonly) css_node_t *cssNode; @property (nonatomic, copy) NSString *viewName; -@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; diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index e5975dff2..6fc7f58b1 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -38,6 +38,7 @@ typedef enum { NSMutableArray *_reactSubviews; BOOL _recomputePadding; BOOL _recomputeMargin; + BOOL _isBGColorExplicitlySet; float _paddingMetaProps[META_PROP_COUNT]; float _marginMetaProps[META_PROP_COUNT]; } @@ -180,7 +181,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st // Update parent properties for children NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithDictionary:parentProperties]; CGFloat alpha = CGColorGetAlpha(_backgroundColor.CGColor); - if (alpha < 1.0 && alpha > 0.0) { + if (alpha < 1.0) { // If we see partial transparency, start propagating full transparency properties[RCTBackgroundColorProp] = [UIColor clearColor]; } else { @@ -515,6 +516,7 @@ RCT_STYLE_PROPERTY(FlexWrap, flexWrap, flex_wrap, css_wrap_type_t) - (void)setBackgroundColor:(UIColor *)color { _backgroundColor = color; + _isBGColorExplicitlySet = YES; [self dirtyPropagation]; } diff --git a/React/Views/RCTTabBar.h b/React/Views/RCTTabBar.h index 694af9ab3..6f491ca08 100644 --- a/React/Views/RCTTabBar.h +++ b/React/Views/RCTTabBar.h @@ -13,6 +13,9 @@ @interface RCTTabBar : UIView +@property (nonatomic, strong) UIColor *tintColor; +@property (nonatomic, strong) UIColor *barTintColor; + - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; @end diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 8a7fb4a43..b5b6240a4 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -114,6 +114,16 @@ }]; } +- (void)setBarTintColor:(UIColor *)barTintColor +{ + _tabController.tabBar.barTintColor = barTintColor; +} + +- (void)setTintColor:(UIColor *)tintColor +{ + _tabController.tabBar.tintColor = tintColor; +} + #pragma mark - UITabBarControllerDelegate - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController diff --git a/React/Views/RCTTabBarManager.m b/React/Views/RCTTabBarManager.m index c7dfe09e1..0f038c4d7 100644 --- a/React/Views/RCTTabBarManager.m +++ b/React/Views/RCTTabBarManager.m @@ -23,4 +23,7 @@ RCT_EXPORT_MODULE() return [[RCTTabBar alloc] initWithEventDispatcher:_bridge.eventDispatcher]; } +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) + @end diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 61f8240e8..d97eb5567 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -15,6 +15,8 @@ #import "RCTUtils.h" #import "UIView+React.h" +static const CGFloat RCTViewBorderThreshold = 0.001; + static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) { for (UIView *subview in [view.subviews reverseObjectEnumerator]) { @@ -436,19 +438,22 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) if ([_backgroundColor isEqual:backgroundColor]) { return; } + _backgroundColor = backgroundColor; [self.layer setNeedsDisplay]; } -- (UIImage *)generateBorderImage:(out CGRect *)contentsCenter +- (UIImage *)borderImage:(out CGRect *)contentsCenter { - static const CGFloat threshold = 0.001; + const CGFloat maxRadius = ({ + const CGRect bounds = self.bounds; + MIN(bounds.size.height, bounds.size.width); + }); - const CGFloat maxRadius = MIN(self.bounds.size.height, self.bounds.size.width); const CGFloat radius = MAX(0, _borderRadius); - const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); - const CGFloat topRightRadius = MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius); - const CGFloat bottomLeftRadius = MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius); + const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); + const CGFloat topRightRadius = MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius); + const CGFloat bottomLeftRadius = MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius); const CGFloat bottomRightRadius = MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius); const CGFloat borderWidth = MAX(0, _borderWidth); @@ -457,14 +462,19 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth; const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth; - if (topLeftRadius < threshold && - topRightRadius < threshold && - bottomLeftRadius < threshold && - bottomRightRadius < threshold && - topWidth < threshold && - rightWidth < threshold && - bottomWidth < threshold && - leftWidth < threshold) { + const BOOL hasCornerRadii = + topLeftRadius > RCTViewBorderThreshold || + topRightRadius > RCTViewBorderThreshold || + bottomLeftRadius > RCTViewBorderThreshold || + bottomRightRadius > RCTViewBorderThreshold; + + const BOOL hasBorders = + topWidth > RCTViewBorderThreshold || + rightWidth > RCTViewBorderThreshold || + bottomWidth > RCTViewBorderThreshold || + leftWidth > RCTViewBorderThreshold; + + if (!hasCornerRadii && !hasBorders) { return nil; } @@ -483,17 +493,26 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) const UIEdgeInsets edgeInsets = UIEdgeInsetsMake(topWidth + MAX(innerTopLeftRadiusY, innerTopRightRadiusY), leftWidth + MAX(innerTopLeftRadiusX, innerBottomLeftRadiusX), bottomWidth + MAX(innerBottomLeftRadiusY, innerBottomRightRadiusY), rightWidth + + MAX(innerBottomRightRadiusX, innerTopRightRadiusX)); const CGSize size = CGSizeMake(edgeInsets.left + 1 + edgeInsets.right, edgeInsets.top + 1 + edgeInsets.bottom); - UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); + const CGFloat alpha = CGColorGetAlpha(_backgroundColor.CGColor); + const BOOL opaque = (self.clipsToBounds || !hasCornerRadii) && alpha == 1.0; + UIGraphicsBeginImageContextWithOptions(size, opaque, 0.0); CGContextRef ctx = UIGraphicsGetCurrentContext(); - const CGRect rect = {CGPointZero, size}; - CGPathRef path = RCTPathCreateWithRoundedRect(rect, topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius, NULL); + const CGRect rect = {.size = size}; + + CGPathRef path; + const BOOL hasClipping = self.clipsToBounds; + if (hasClipping) { + path = CGPathCreateWithRect(rect, NULL); + } else { + path = RCTPathCreateWithRoundedRect(rect, topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius, NULL); + } if (_backgroundColor) { CGContextSaveGState(ctx); - CGContextAddPath(ctx, path); CGContextSetFillColorWithColor(ctx, _backgroundColor.CGColor); + CGContextAddPath(ctx, path); CGContextFillPath(ctx); CGContextRestoreGState(ctx); @@ -502,29 +521,18 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) CGContextAddPath(ctx, path); CGPathRelease(path); - BOOL hasRadius = topLeftRadius > 0 || topRightRadius > 0 || bottomLeftRadius > 0 || bottomRightRadius > 0; - if (hasRadius && 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, innerTopLeftRadiusX, innerTopLeftRadiusY, innerTopRightRadiusX, innerTopRightRadiusY, innerBottomLeftRadiusX, innerBottomLeftRadiusY, innerBottomRightRadiusX, innerBottomRightRadiusY, NULL); - CGContextAddPath(ctx, insetPath); - CGPathRelease(insetPath); - } + const BOOL hasRadius = topLeftRadius > 0 || topRightRadius > 0 || bottomLeftRadius > 0 || bottomRightRadius > 0; + const UIEdgeInsets insetEdgeInsets = UIEdgeInsetsMake(topWidth, leftWidth, bottomWidth, rightWidth); + CGPathRef insetPath = RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, insetEdgeInsets), innerTopLeftRadiusX, innerTopLeftRadiusY, innerTopRightRadiusX, innerTopRightRadiusY, innerBottomLeftRadiusX, innerBottomLeftRadiusY, innerBottomRightRadiusX, innerBottomRightRadiusY, NULL); + CGContextAddPath(ctx, insetPath); CGContextEOClip(ctx); BOOL hasEqualColor = !_borderTopColor && !_borderRightColor && !_borderBottomColor && !_borderLeftColor; - BOOL hasEqualBorder = _borderWidth >= 0 && _borderTopWidth < 0 && _borderRightWidth < 0 && _borderBottomWidth < 0 && _borderLeftWidth < 0; - if (!hasRadius && hasEqualBorder && hasEqualColor) { - CGContextSetStrokeColorWithColor(ctx, _borderColor); - CGContextSetLineWidth(ctx, 2 * _borderWidth); - CGContextClipToRect(ctx, rect); - CGContextStrokeRect(ctx, rect); - } else if (!hasRadius && hasEqualColor) { + if ((hasClipping || !hasRadius) && hasEqualColor) { CGContextSetFillColorWithColor(ctx, _borderColor); CGContextAddRect(ctx, rect); - const CGRect insetRect = UIEdgeInsetsInsetRect(rect, edgeInsets); - CGContextAddRect(ctx, insetRect); + CGContextAddPath(ctx, insetPath); CGContextEOFillPath(ctx); } else { BOOL didSet = NO; @@ -660,6 +668,8 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) } } + CGPathRelease(insetPath); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); @@ -669,8 +679,8 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) - (void)displayLayer:(CALayer *)layer { - CGRect contentsCenter = (CGRect){CGPointZero, {1, 1}}; - UIImage *image = [self generateBorderImage:&contentsCenter]; + CGRect contentsCenter = {.size = {1, 1}}; + UIImage *image = [self borderImage:&contentsCenter]; if (image && RCTRunningInTestEnvironment()) { const CGSize size = self.bounds.size; @@ -685,6 +695,46 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) layer.contentsCenter = contentsCenter; layer.contentsScale = image.scale ?: 1.0; layer.magnificationFilter = kCAFilterNearest; + layer.needsDisplayOnBoundsChange = image != nil; + + [self updateClippingForLayer:layer]; +} + +- (void)updateClippingForLayer:(CALayer *)layer +{ + CALayer *mask = nil; + CGFloat cornerRadius = 0; + + if (self.clipsToBounds) { + if (_borderRadius > 0 && _borderTopLeftRadius < 0 && _borderTopRightRadius < 0 && _borderBottomLeftRadius < 0 && _borderBottomRightRadius < 0) { + cornerRadius = _borderRadius; + } else { + const CGRect bounds = layer.bounds; + const CGFloat maxRadius = MIN(bounds.size.height, bounds.size.width); + const CGFloat radius = MAX(0, _borderRadius); + const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); + const CGFloat topRightRadius = MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius); + const CGFloat bottomLeftRadius = MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius); + const CGFloat bottomRightRadius = MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius); + + if (ABS(topLeftRadius - topRightRadius) < RCTViewBorderThreshold && + ABS(topLeftRadius - bottomLeftRadius) < RCTViewBorderThreshold && + ABS(topLeftRadius - bottomRightRadius) < RCTViewBorderThreshold) { + cornerRadius = topLeftRadius; + } else { + CAShapeLayer *shapeLayer = [CAShapeLayer layer]; + + CGPathRef path = RCTPathCreateWithRoundedRect(bounds, topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius, NULL); + shapeLayer.path = path; + CGPathRelease(path); + + mask = shapeLayer; + } + } + } + + layer.cornerRadius = cornerRadius; + layer.mask = mask; } #pragma mark Border Color diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 8230e398d..311167761 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -228,36 +228,38 @@ RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomRight) #pragma mark - ShadowView properties -RCT_EXPORT_SHADOW_PROPERTY(top, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(right, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(bottom, CGFloat); +RCT_EXPORT_SHADOW_PROPERTY(backgroundColor, UIColor) + +RCT_EXPORT_SHADOW_PROPERTY(top, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(right, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(bottom, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(left, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(width, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(height, CGFloat); +RCT_EXPORT_SHADOW_PROPERTY(width, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(height, CGFloat) -RCT_EXPORT_SHADOW_PROPERTY(borderTopWidth, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(borderRightWidth, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(borderBottomWidth, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(borderLeftWidth, CGFloat); +RCT_EXPORT_SHADOW_PROPERTY(borderTopWidth, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(borderRightWidth, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(borderBottomWidth, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(borderLeftWidth, 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); -RCT_EXPORT_SHADOW_PROPERTY(marginBottom, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(marginLeft, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(marginVertical, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(marginHorizontal, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(margin, CGFloat); +RCT_EXPORT_SHADOW_PROPERTY(marginTop, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(marginRight, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(marginBottom, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(marginLeft, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(marginVertical, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(marginHorizontal, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(margin, CGFloat) -RCT_EXPORT_SHADOW_PROPERTY(paddingTop, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(paddingRight, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(paddingBottom, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(paddingLeft, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(paddingVertical, CGFloat); -RCT_EXPORT_SHADOW_PROPERTY(paddingHorizontal, CGFloat); +RCT_EXPORT_SHADOW_PROPERTY(paddingTop, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(paddingRight, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(paddingBottom, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(paddingLeft, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(paddingVertical, CGFloat) +RCT_EXPORT_SHADOW_PROPERTY(paddingHorizontal, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(padding, CGFloat); RCT_EXPORT_SHADOW_PROPERTY(flex, CGFloat) @@ -268,12 +270,6 @@ RCT_EXPORT_SHADOW_PROPERTY(alignItems, css_align_t) RCT_EXPORT_SHADOW_PROPERTY(alignSelf, css_align_t) RCT_REMAP_SHADOW_PROPERTY(position, positionType, css_position_type_t) -RCT_CUSTOM_SHADOW_PROPERTY(backgroundColor, UIColor, RCTShadowView) -{ - view.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; - view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; -} - RCT_REMAP_SHADOW_PROPERTY(onLayout, hasOnLayout, BOOL) @end diff --git a/lint/linterTransform.js b/lint/linterTransform.js deleted file mode 100644 index 669baf556..000000000 --- a/lint/linterTransform.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -var eslint = require('eslint'); - -var ignoredStylisticRules = { - 'key-spacing': false, - 'comma-spacing': true, - 'no-multi-spaces': true, - 'brace-style': true, - 'camelcase': true, - 'consistent-this': true, - 'eol-last': true, - 'func-names': true, - 'func-style': true, - 'new-cap': true, - 'new-parens': true, - 'no-nested-ternary': true, - 'no-array-constructor': true, - 'no-lonely-if': true, - 'no-new-object': true, - 'no-spaced-func': true, - 'no-space-before-semi': true, - 'no-ternary': true, - 'no-trailing-spaces': true, - 'no-underscore-dangle': true, - 'no-wrap-func': true, - 'no-mixed-spaces-and-tabs': true, - 'quotes': true, - 'quote-props': true, - 'semi': true, - 'sort-vars': true, - 'space-after-keywords': true, - 'space-in-brackets': true, - 'space-in-parens': true, - 'space-infix-ops': true, - 'space-return-throw-case': true, - 'space-unary-word-ops': true, - 'max-nested-callbacks': true, - 'one-var': true, - 'wrap-regex': true, - 'curly': true, - 'no-mixed-requires': true, -}; - -function setLinterTransform(transformSource) { - var originalVerify = eslint.linter.verify; - eslint.linter.verify = function(text, config, filename, saveState) { - var transformedText; - try { - transformedText = transformSource(text, filename); - } catch (e) { - return [{ - severity: 2, - line: e.lineNumber, - message: e.message, - source: text - }]; - } - var originalLines = text.split('\n'); - var transformedLines = transformedText.split('\n'); - var warnings = originalVerify.call(eslint.linter, transformedText, config, filename, saveState); - - // JSX and ES6 transforms usually generate pretty ugly code. Let's skip lint warnings - // about code style for lines that have been changed by transform step. - // Note that more important issues, like use of undefined vars, will still be reported. - return warnings.filter(function(error) { - var lineHasBeenTransformed = originalLines[error.line - 1] !== transformedLines[error.line - 1]; - var shouldIgnore = ignoredStylisticRules[error.ruleId] && lineHasBeenTransformed; - return !shouldIgnore; - }); - }; -} - -module.exports = { - setLinterTransform: setLinterTransform, -}; diff --git a/linter.js b/linter.js deleted file mode 100644 index f243a2d2f..000000000 --- a/linter.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -var transformSource = require('./jestSupport/scriptPreprocess.js').transformSource; -var linterTransform = require('./lint/linterTransform'); - -linterTransform.setLinterTransform(transformSource); - -// Run the original CLI -require('eslint/bin/eslint'); diff --git a/package.json b/package.json index a2c7a38b4..1f47a6d34 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "absolute-path": "0.0.0", + "babel": "5.4.3", "bluebird": "^2.9.21", "chalk": "^1.0.0", "connect": "2.8.3", @@ -58,7 +59,7 @@ "react-timer-mixin": "^0.13.1", "react-tools": "0.13.2", "rebound": "^0.0.12", - "sane": "^1.1.2", + "sane": "git://github.com/tadeuzagallo/sane.git#a029f8b04a", "source-map": "0.1.31", "stacktrace-parser": "git://github.com/frantic/stacktrace-parser.git#493c5e5638", "uglify-js": "~2.4.16", @@ -69,6 +70,8 @@ }, "devDependencies": { "jest-cli": "0.4.5", - "eslint": "0.9.2" + "babel-eslint": "3.1.5", + "eslint": "0.21.2", + "eslint-plugin-react": "2.3.0" } } diff --git a/packager/packager.js b/packager/packager.js index eb90b04e0..d630d06e3 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -36,14 +36,18 @@ var webSocketProxy = require('./webSocketProxy.js'); var options = parseCommandLine([{ command: 'port', default: 8081, + type: 'string', }, { command: 'root', + type: 'string', description: 'add another root(s) to be used by the packager in this project', }, { command: 'assetRoots', + type: 'string', description: 'specify the root directories of app assets' }, { command: 'platform', + type: 'string', default: 'ios', description: 'Specify the platform-specific blacklist (ios, android, web).' }, { @@ -65,13 +69,13 @@ if (options.projectRoots) { } if (options.root) { - if (typeof options.root === 'string') { - options.projectRoots.push(path.resolve(options.root)); - } else { - options.root.forEach(function(root) { - options.projectRoots.push(path.resolve(root)); - }); + if (!Array.isArray(options.root)) { + options.root = options.root.split(','); } + + options.root.forEach(function(root) { + options.projectRoots.push(path.resolve(root)); + }); } if (options.assetRoots) { diff --git a/packager/react-packager/index.js b/packager/react-packager/index.js index a7b6264b7..6be111997 100644 --- a/packager/react-packager/index.js +++ b/packager/react-packager/index.js @@ -18,14 +18,17 @@ exports.middleware = function(options) { return server.processRequest.bind(server); }; -exports.buildPackageFromUrl = function(options, reqUrl) { - Activity.disable(); - // Don't start the filewatcher or the cache. - if (options.nonPersistent == null) { - options.nonPersistent = true; - } +exports.buildPackage = function(options, packageOptions) { + var server = createServer(options); + return server.buildPackage(packageOptions) + .then(function(p) { + server.end(); + return p; + }); +}; - var server = new Server(options); +exports.buildPackageFromUrl = function(options, reqUrl) { + var server = createServer(options); return server.buildPackageFromUrl(reqUrl) .then(function(p) { server.end(); @@ -34,13 +37,7 @@ exports.buildPackageFromUrl = function(options, reqUrl) { }; exports.getDependencies = function(options, main) { - Activity.disable(); - // Don't start the filewatcher or the cache. - if (options.nonPersistent == null) { - options.nonPersistent = true; - } - - var server = new Server(options); + var server = createServer(options); return server.getDependencies(main) .then(function(r) { server.end(); @@ -60,3 +57,13 @@ function useGracefulFs() { } }); } + +function createServer(options) { + Activity.disable(); + // Don't start the filewatcher or the cache. + if (options.nonPersistent == null) { + options.nonPersistent = true; + } + + return new Server(options); +} diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js index cd1a28e55..d90de452d 100644 --- a/packager/react-packager/src/FileWatcher/index.js +++ b/packager/react-packager/src/FileWatcher/index.js @@ -67,6 +67,7 @@ function createWatcher(rootConfig) { var watcher = new Watcher(rootConfig.dir, { glob: rootConfig.globs, dot: false, + ignore: '**/node_modules/**/*', }); return new Promise(function(resolve, reject) { diff --git a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js index 5948916a7..22ca53e63 100644 --- a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js +++ b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js @@ -57,25 +57,34 @@ describe('Transformer', function() { }); pit('should add file info to parse errors', function() { + var message = 'message'; + var snippet = 'snippet'; + require('fs').readFile.mockImpl(function(file, callback) { callback(null, 'var x;\nvar answer = 1 = x;'); }); workers.mockImpl(function(data, callback) { - var esprimaError = new Error('Error: Line 2: Invalid left-hand side in assignment'); - esprimaError.description = 'Invalid left-hand side in assignment'; - esprimaError.lineNumber = 2; - esprimaError.column = 15; - callback(null, {error: esprimaError}); + var babelError = new SyntaxError(message); + babelError.type = 'SyntaxError'; + babelError.description = message; + babelError.loc = { + line: 2, + column: 15, + }; + babelError.codeFrame = snippet; + callback(babelError); }); return new Transformer(OPTIONS).loadFileAndTransform('foo-file.js') .catch(function(error) { expect(error.type).toEqual('TransformError'); - expect(error.snippet).toEqual([ - 'var answer = 1 = x;', - ' ^', - ].join('\n')); + expect(error.message).toBe('SyntaxError ' + message); + expect(error.lineNumber).toBe(2); + expect(error.column).toBe(15); + expect(error.filename).toBe('foo-file.js'); + expect(error.description).toBe(message); + expect(error.snippet).toBe(snippet); }); }); }); diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index 2dc5e20bd..513d4394e 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -99,7 +99,12 @@ Transformer.prototype.loadFileAndTransform = function(filePath) { }).then( function(res) { if (res.error) { - throw formatError(res.error, filePath, sourceCode); + console.warn( + 'Error property on the result value form the transformer', + 'module is deprecated and will be removed in future versions.', + 'Please pass an error object as the first argument to the callback' + ); + throw formatError(res.error, filePath); } return new ModuleTransport({ @@ -110,6 +115,8 @@ Transformer.prototype.loadFileAndTransform = function(filePath) { }); } ); + }).catch(function(err) { + throw formatError(err, filePath); }); }); }; @@ -118,8 +125,8 @@ function TransformError() {} util.inherits(TransformError, SyntaxError); function formatError(err, filename, source) { - if (err.lineNumber && err.column) { - return formatEsprimaError(err, filename, source); + if (err.loc) { + return formatBabelError(err, filename, source); } else { return formatGenericError(err, filename, source); } @@ -136,26 +143,16 @@ function formatGenericError(err, filename) { return error; } -function formatEsprimaError(err, filename, source) { - var stack = err.stack.split('\n'); - stack.shift(); - - var msg = 'TransformError: ' + err.description + ' ' + filename + ':' + - err.lineNumber + ':' + err.column; - var sourceLine = source.split('\n')[err.lineNumber - 1]; - var snippet = sourceLine + '\n' + new Array(err.column - 1).join(' ') + '^'; - - stack.unshift(msg); - +function formatBabelError(err, filename) { var error = new TransformError(); - error.message = msg; error.type = 'TransformError'; - error.stack = stack.join('\n'); - error.snippet = snippet; + error.message = (err.type || error.type) + ' ' + err.message; + error.stack = err.stack; + error.snippet = err.codeFrame; + error.lineNumber = err.loc.line; + error.column = err.loc.column; error.filename = filename; - error.lineNumber = err.lineNumber; - error.column = err.column; - error.description = err.description; + error.description = err.message; return error; } diff --git a/packager/react-packager/src/Packager/Package.js b/packager/react-packager/src/Packager/Package.js index 70f57c4ab..6b5389461 100644 --- a/packager/react-packager/src/Packager/Package.js +++ b/packager/react-packager/src/Packager/Package.js @@ -15,6 +15,8 @@ var ModuleTransport = require('../lib/ModuleTransport'); module.exports = Package; +var SOURCEMAPPING_URL = '\n\/\/@ sourceMappingURL='; + function Package(sourceMapUrl) { this._finalized = false; this._modules = []; @@ -96,12 +98,11 @@ Package.prototype.getSource = function(options) { } var source = this._getSource(); - source += '\n\/\/@ sourceMappingURL='; if (options.inlineSourceMap) { - source += this._getInlineSourceMap(); - } else { - source += this._sourceMapUrl; + source += SOURCEMAPPING_URL + this._getInlineSourceMap(); + } else if (this._sourceMapUrl) { + source += SOURCEMAPPING_URL + this._sourceMapUrl; } return source; diff --git a/packager/react-packager/src/Packager/__tests__/Package-test.js b/packager/react-packager/src/Packager/__tests__/Package-test.js index 1d8c3dd12..d43c65c0f 100644 --- a/packager/react-packager/src/Packager/__tests__/Package-test.js +++ b/packager/react-packager/src/Packager/__tests__/Package-test.js @@ -47,6 +47,26 @@ describe('Package', function() { ].join('\n')); }); + it('should be ok to leave out the source map url', function() { + var p = new Package(); + p.addModule(new ModuleTransport({ + code: 'transformed foo;', + sourceCode: 'source foo', + sourcePath: 'foo path', + })); + p.addModule(new ModuleTransport({ + code: 'transformed bar;', + sourceCode: 'source bar', + sourcePath: 'bar path', + })); + + p.finalize({}); + expect(p.getSource()).toBe([ + 'transformed foo;', + 'transformed bar;', + ].join('\n')); + }); + it('should create a package and add run module code', function() { ppackage.addModule(new ModuleTransport({ code: 'transformed foo;', diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index 58a1aca79..e4e7b5088 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -230,7 +230,7 @@ describe('processRequest', function() { }); }); - describe.only('/assets endpoint', function() { + describe('/assets endpoint', function() { var AssetServer; beforeEach(function() { AssetServer = require('../../AssetServer'); @@ -257,4 +257,30 @@ describe('processRequest', function() { }); }); + + describe('buildPackage(options)', function() { + it('Calls the packager with the correct args', function() { + server.buildPackage({ + entryFile: 'foo file' + }); + expect(Packager.prototype.package).toBeCalledWith( + 'foo file', + true, + undefined, + true + ); + }); + }); + + describe('buildPackageFromUrl(options)', function() { + it('Calls the packager with the correct args', function() { + server.buildPackageFromUrl('/path/to/foo.bundle?dev=false&runModule=false'); + expect(Packager.prototype.package).toBeCalledWith( + 'path/to/foo.js', + false, + '/path/to/foo.map', + false + ); + }); + }); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index d4da5ae10..0ce5c5847 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -129,7 +129,7 @@ Server.prototype._onFileChange = function(type, filepath, root) { }; Server.prototype._rebuildPackages = function() { - var buildPackage = this._buildPackage.bind(this); + var buildPackage = this.buildPackage.bind(this); var packages = this._packages; Object.keys(packages).forEach(function(key) { var options = getOptionsFromUrl(key); @@ -171,18 +171,47 @@ Server.prototype.end = function() { ]); }; -Server.prototype._buildPackage = function(options) { +var packageOpts = declareOpts({ + sourceMapUrl: { + type: 'string', + required: false, + }, + entryFile: { + type: 'string', + required: true, + }, + dev: { + type: 'boolean', + default: true, + }, + minify: { + type: 'boolean', + default: false, + }, + runModule: { + type: 'boolean', + default: true, + }, + inlineSourceMap: { + type: 'boolean', + default: false, + }, +}); + +Server.prototype.buildPackage = function(options) { + var opts = packageOpts(options); + return this._packager.package( - options.main, - options.runModule, - options.sourceMapUrl, - options.dev + opts.entryFile, + opts.runModule, + opts.sourceMapUrl, + opts.dev ); }; Server.prototype.buildPackageFromUrl = function(reqUrl) { var options = getOptionsFromUrl(reqUrl); - return this._buildPackage(options); + return this.buildPackage(options); }; Server.prototype.getDependencies = function(main) { @@ -321,7 +350,7 @@ Server.prototype.processRequest = function(req, res, next) { var startReqEventId = Activity.startEvent('request:' + req.url); var options = getOptionsFromUrl(req.url); - var building = this._packages[req.url] || this._buildPackage(options); + var building = this._packages[req.url] || this.buildPackage(options); this._packages[req.url] = building; building.then( @@ -363,7 +392,7 @@ function getOptionsFromUrl(reqUrl) { return { sourceMapUrl: pathname.replace(/\.bundle$/, '.map'), - main: entryFile, + entryFile: entryFile, dev: getBoolOptionFromQuery(urlObj.query, 'dev', true), minify: getBoolOptionFromQuery(urlObj.query, 'minify'), runModule: getBoolOptionFromQuery(urlObj.query, 'runModule', true), diff --git a/packager/transformer.js b/packager/transformer.js index 00beeadde..a2efcea3b 100644 --- a/packager/transformer.js +++ b/packager/transformer.js @@ -10,40 +10,37 @@ */ 'use strict'; -var jstransform = require('jstransform').transform; +var babel = require('babel'); -var reactVisitors = - require('react-tools/vendor/fbtransform/visitors').getAllVisitors(); -var staticTypeSyntax = - require('jstransform/visitors/type-syntax').visitorList; -var trailingCommaVisitors = - require('jstransform/visitors/es7-trailing-comma-visitors.js').visitorList; - -// Note that reactVisitors now handles ES6 classes, rest parameters, arrow -// functions, template strings, and object short notation. -var visitorList = reactVisitors.concat(trailingCommaVisitors); - -function transform(srcTxt, filename) { - var options = { - es3: true, - sourceType: 'nonStrictModule', +function transform(srcTxt, filename, options) { + var result = babel.transform(srcTxt, { + retainLines: true, + compact: true, + comments: false, filename: filename, + whitelist: [ + 'es6.arrowFunctions', + 'es6.blockScoping', + 'es6.classes', + 'es6.destructuring', + 'es6.parameters.rest', + 'es6.properties.computed', + 'es6.properties.shorthand', + 'es6.spread', + 'es6.templateLiterals', + 'es7.trailingFunctionCommas', + 'es7.objectRestSpread', + 'flow', + 'react', + ], + sourceFileName: filename, + sourceMaps: false, + extra: options || {}, + }); + + return { + code: result.code, }; - - // These tranforms mostly just erase type annotations and static typing - // related statements, but they were conflicting with other tranforms. - // Running them first solves that problem - var staticTypeSyntaxResult = jstransform( - staticTypeSyntax, - srcTxt, - options - ); - - return jstransform( - visitorList, - staticTypeSyntaxResult.code, - options - ); } module.exports = function(data, callback) { @@ -54,15 +51,8 @@ module.exports = function(data, callback) { data.filename ); } catch (e) { - return callback(null, { - error: { - lineNumber: e.lineNumber, - column: e.column, - message: e.message, - stack: e.stack, - description: e.description - } - }); + callback(e); + return; } callback(null, result); diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index dcafbf2be..226dedf7f 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -65,6 +65,6 @@ cd EndToEndTest # Make sure we installed local version of react-native ls `basename $MARKER` > /dev/null -flow +flow --retries 10 xctool -scheme EndToEndTest -sdk iphonesimulator test diff --git a/scripts/objc-test.sh b/scripts/objc-test.sh index 6cf9a7592..e02f0fc22 100755 --- a/scripts/objc-test.sh +++ b/scripts/objc-test.sh @@ -25,5 +25,5 @@ trap cleanup EXIT xctool \ -project Examples/UIExplorer/UIExplorer.xcodeproj \ - -scheme UIExplorer -sdk iphonesimulator8.1 \ + -scheme UIExplorer -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 5,OS=8.1' \ test