diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index 05ab80187..e7eb7f2b4 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -281,7 +281,7 @@ var SearchScreen = React.createClass({ renderRow={this.renderRow} onEndReached={this.onEndReached} automaticallyAdjustContentInsets={false} - keyboardDismissMode="onDrag" + keyboardDismissMode="on-drag" keyboardShouldPersistTaps={true} showsVerticalScrollIndicator={false} />; diff --git a/Examples/UIExplorer/ProgressViewIOSExample.js b/Examples/UIExplorer/ProgressViewIOSExample.js new file mode 100644 index 000000000..f0a17a7c6 --- /dev/null +++ b/Examples/UIExplorer/ProgressViewIOSExample.js @@ -0,0 +1,83 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + ProgressViewIOS, + StyleSheet, + View, +} = React; +var TimerMixin = require('react-timer-mixin'); + +var ProgressViewExample = React.createClass({ + mixins: [TimerMixin], + + getInitialState() { + return { + progress: 0, + }; + }, + + componentDidMount() { + this.updateProgress(); + }, + + updateProgress() { + var progress = this.state.progress + 0.01; + this.setState({ progress }); + this.requestAnimationFrame(() => this.updateProgress()); + }, + + getProgress(offset) { + var progress = this.state.progress + offset; + return Math.sin(progress % Math.PI) % 1; + }, + + render() { + return ( + + + + + + + + ); + }, +}); + +exports.framework = 'React'; +exports.title = 'ProgressViewIOS'; +exports.description = 'ProgressViewIOS'; +exports.examples = [{ + title: 'ProgressViewIOS', + render() { + return ( + + ); + } +}]; + +var styles = StyleSheet.create({ + container: { + marginTop: -20, + backgroundColor: 'transparent', + }, + progressView: { + marginTop: 20, + } +}); diff --git a/Examples/UIExplorer/StatusBarIOSExample.js b/Examples/UIExplorer/StatusBarIOSExample.js index 545136c49..ab649197e 100644 --- a/Examples/UIExplorer/StatusBarIOSExample.js +++ b/Examples/UIExplorer/StatusBarIOSExample.js @@ -32,11 +32,11 @@ exports.examples = [{ render() { return ( - {Object.keys(StatusBarIOS.Style).map((key) => + {['default', 'light-content'].map((style) => StatusBarIOS.setStyle(StatusBarIOS.Style[key])}> + onPress={() => StatusBarIOS.setStyle(style)}> - setStyle(StatusBarIOS.Style.{key}) + setStyle('{style}') )} @@ -48,11 +48,11 @@ exports.examples = [{ render() { return ( - {Object.keys(StatusBarIOS.Style).map((key) => + {['default', 'light-content'].map((style) => StatusBarIOS.setStyle(StatusBarIOS.Style[key], true)}> + onPress={() => StatusBarIOS.setStyle(style, true)}> - setStyle(StatusBarIOS.Style.{key}, true) + setStyle('{style}', true) )} @@ -64,18 +64,18 @@ exports.examples = [{ render() { return ( - {Object.keys(StatusBarIOS.Animation).map((key) => + {['none', 'fade', 'slide'].map((animation) => StatusBarIOS.setHidden(true, StatusBarIOS.Animation[key])}> + onPress={() => StatusBarIOS.setHidden(true, animation)}> - setHidden(true, StatusBarIOS.Animation.{key}) + setHidden(true, '{animation}') StatusBarIOS.setHidden(false, StatusBarIOS.Animation[key])}> + onPress={() => StatusBarIOS.setHidden(false, animation)}> - setHidden(false, StatusBarIOS.Animation.{key}) + setHidden(false, '{animation}') diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 45a679b31..acbba3629 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -75,6 +75,14 @@ exports.examples = [ render: function(): ReactElement { return ; }, +}, { + title: 'Touchable delay for events', + description: ' components also accept delayPressIn, ' + + 'delayPressOut, and delayLongPress as props. These props impact the ' + + 'timing of feedback events.', + render: function(): ReactElement { + return ; + }, }]; var TextOnPressBox = React.createClass({ @@ -148,6 +156,44 @@ var TouchableFeedbackEvents = React.createClass({ }, }); +var TouchableDelayEvents = React.createClass({ + getInitialState: function() { + return { + eventLog: [], + }; + }, + render: function() { + return ( + + + this._appendEvent('press')} + delayPressIn={400} + onPressIn={() => this._appendEvent('pressIn - 400ms delay')} + delayPressOut={1000} + onPressOut={() => this._appendEvent('pressOut - 1000ms delay')} + delayLongPress={800} + onLongPress={() => this._appendEvent('longPress - 800ms delay')}> + + Press Me + + + + + {this.state.eventLog.map((e, ii) => {e})} + + + ); + }, + _appendEvent: function(eventName) { + var limit = 6; + var eventLog = this.state.eventLog.slice(0, limit - 1); + eventLog.unshift(eventName); + this.setState({eventLog}); + }, +}); + var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'}; var styles = StyleSheet.create({ diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m index d72262e78..9d3adb2ee 100644 --- a/Examples/UIExplorer/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m @@ -50,6 +50,10 @@ // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#if RUNNING_ON_CI + jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif + RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"UIExplorerApp" launchOptions:launchOptions]; diff --git a/Examples/UIExplorer/UIExplorer/Info.plist b/Examples/UIExplorer/UIExplorer/Info.plist index 245054621..349bd9a28 100644 --- a/Examples/UIExplorer/UIExplorer/Info.plist +++ b/Examples/UIExplorer/UIExplorer/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.facebook.$(PRODUCT_NAME:rfc1034identifier) + com.facebook.internal.uiexplorer.local CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -22,6 +22,8 @@ 1 LSRequiresIPhoneOS + NSLocationWhenInUseUsageDescription + You need to add NSLocationWhenInUseUsageDescription key in Info.plist to enable geolocation, otherwise it is going to *fail silently*! UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -34,8 +36,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSLocationWhenInUseUsageDescription - You need to add NSLocationWhenInUseUsageDescription key in Info.plist to enable geolocation, otherwise it is going to *fail silently*! UIViewControllerBasedStatusBarAppearance diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index a030220ca..cd73e6d06 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -45,6 +45,7 @@ var COMPONENTS = [ require('./NavigatorIOSColorsExample'), require('./NavigatorIOSExample'), require('./PickerIOSExample'), + require('./ProgressViewIOSExample'), require('./ScrollViewExample'), require('./SegmentedControlIOSExample'), require('./SliderIOSExample'), @@ -156,7 +157,7 @@ class UIExplorerList extends React.Component { renderSectionHeader={this._renderSectionHeader} keyboardShouldPersistTaps={true} automaticallyAdjustContentInsets={false} - keyboardDismissMode="onDrag" + keyboardDismissMode="on-drag" /> ); diff --git a/IntegrationTests/AsyncStorageTest.js b/IntegrationTests/AsyncStorageTest.js index 6d13bb6e9..911887d3e 100644 --- a/IntegrationTests/AsyncStorageTest.js +++ b/IntegrationTests/AsyncStorageTest.js @@ -16,12 +16,19 @@ var { View, } = React; +var deepDiffer = require('deepDiffer'); + var DEBUG = false; var KEY_1 = 'key_1'; var VAL_1 = 'val_1'; var KEY_2 = 'key_2'; var VAL_2 = 'val_2'; +var KEY_MERGE = 'key_merge'; +var VAL_MERGE_1 = {'foo': 1, 'bar': {'hoo': 1, 'boo': 1}, 'moo': {'a': 3}}; +var VAL_MERGE_2 = {'bar': {'hoo': 2}, 'baz': 2, 'moo': {'a': 3}}; +var VAL_MERGE_EXPECT = + {'foo': 1, 'bar': {'hoo': 2, 'boo': 1}, 'baz': 2, 'moo': {'a': 3}}; // setup in componentDidMount var done; @@ -40,8 +47,9 @@ function expectTrue(condition, message) { function expectEqual(lhs, rhs, testname) { expectTrue( - lhs === rhs, - 'Error in test ' + testname + ': expected ' + rhs + ', got ' + lhs + !deepDiffer(lhs, rhs), + 'Error in test ' + testname + ': expected\n' + JSON.stringify(rhs) + + '\ngot\n' + JSON.stringify(lhs) ); } @@ -93,25 +101,25 @@ function testRemoveItem() { 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')' ); updateMessage('testRemoveItem - add two items'); - AsyncStorage.removeItem(KEY_1, (err) => { - expectAsyncNoError(err); + AsyncStorage.removeItem(KEY_1, (err2) => { + expectAsyncNoError(err2); updateMessage('delete successful '); - AsyncStorage.getItem(KEY_1, (err, result) => { - expectAsyncNoError(err); + AsyncStorage.getItem(KEY_1, (err3, result2) => { + expectAsyncNoError(err3); expectEqual( - result, + result2, null, 'testRemoveItem: key_1 present after delete' ); updateMessage('key properly removed '); - AsyncStorage.getAllKeys((err, result2) => { - expectAsyncNoError(err); + AsyncStorage.getAllKeys((err4, result3) => { + expectAsyncNoError(err4); expectTrue( - result2.indexOf(KEY_1) === -1, - 'Unexpected: KEY_1 present in ' + result2 + result3.indexOf(KEY_1) === -1, + 'Unexpected: KEY_1 present in ' + result3 ); - updateMessage('proper length returned.\nDone!'); - done(); + updateMessage('proper length returned.'); + runTestCase('should merge values', testMerge); }); }); }); @@ -120,6 +128,21 @@ function testRemoveItem() { }); } +function testMerge() { + AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), (err1) => { + expectAsyncNoError(err1); + AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), (err2) => { + expectAsyncNoError(err2); + AsyncStorage.getItem(KEY_MERGE, (err3, result) => { + expectAsyncNoError(err3); + expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge'); + updateMessage('objects deeply merged\nDone!'); + done(); + }); + }); + }); +} + var AsyncStorageTest = React.createClass({ getInitialState() { return { diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js b/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js index 0245cc144..c8e289431 100644 --- a/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js +++ b/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js @@ -7,7 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule POPAnimation - * @flow */ 'use strict'; @@ -17,7 +16,7 @@ if (!RCTPOPAnimationManager) { // workaround to enable its availability to be determined at runtime. // For Flow let's pretend like we always export POPAnimation // so all our users don't need to do null checks - module.exports = ((null: any): typeof POPAnimation); + module.exports = null; } else { var ReactPropTypes = require('ReactPropTypes'); diff --git a/Libraries/Components/DatePicker/DatePickerIOS.ios.js b/Libraries/Components/DatePicker/DatePickerIOS.ios.js index 41fc9b877..f184c6f79 100644 --- a/Libraries/Components/DatePicker/DatePickerIOS.ios.js +++ b/Libraries/Components/DatePicker/DatePickerIOS.ios.js @@ -120,7 +120,7 @@ var DatePickerIOS = React.createClass({ + + ProgressViewIOS is not supported on this platform! + + + ); + }, +}); + +var styles = StyleSheet.create({ + dummy: { + width: 120, + height: 20, + backgroundColor: '#ffbcbc', + borderWidth: 1, + borderColor: 'red', + alignItems: 'center', + justifyContent: 'center', + }, + text: { + color: '#333333', + margin: 5, + fontSize: 10, + } +}); + +module.exports = DummyProgressViewIOS; diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js new file mode 100644 index 000000000..cbdf43ae5 --- /dev/null +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js @@ -0,0 +1,83 @@ +/** + * 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 ProgressViewIOS + * @flow + */ +'use strict'; + +var Image = require('Image'); +var NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeModules = require('NativeModules'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); + +var requireNativeComponent = require('requireNativeComponent'); +var verifyPropTypes = require('verifyPropTypes'); + +/** + * Use `ProgressViewIOS` to render a UIProgressView on iOS. + */ +var ProgressViewIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The progress bar style. + */ + progressViewStyle: PropTypes.oneOf(['default', 'bar']), + + /** + * The progress value (between 0 and 1). + */ + progress: PropTypes.number, + + /** + * The tint color of the progress bar itself. + */ + progressTintColor: PropTypes.string, + + /** + * The tint color of the progress bar track. + */ + trackTintColor: PropTypes.string, + + /** + * A stretchable image to display as the progress bar. + */ + progressImage: Image.propTypes.source, + + /** + * A stretchable image to display behind the progress bar. + */ + trackImage: Image.propTypes.source, + }, + + render: function() { + return ( + + ); + } +}); + +var styles = StyleSheet.create({ + progressView: { + height: NativeModules.ProgressViewManager.ComponentHeight + }, +}); + +var RCTProgressView = requireNativeComponent( + 'RCTProgressView', + ProgressViewIOS +); + +module.exports = ProgressViewIOS; diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 3a87d05a8..8d274bdc5 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -38,12 +38,6 @@ var PropTypes = React.PropTypes; var SCROLLVIEW = 'ScrollView'; var INNERVIEW = 'InnerScrollView'; -var keyboardDismissModeConstants = { - 'none': RCTScrollViewConsts.KeyboardDismissMode.None, // default - 'interactive': RCTScrollViewConsts.KeyboardDismissMode.Interactive, - 'onDrag': RCTScrollViewConsts.KeyboardDismissMode.OnDrag, -}; - /** * Component that wraps platform ScrollView while providing * integration with touch locking "responder" system. @@ -147,7 +141,7 @@ var ScrollView = React.createClass({ keyboardDismissMode: PropTypes.oneOf([ 'none', // default 'interactive', - 'onDrag', + 'on-drag', ]), /** * When false, tapping outside of the focused text input when the keyboard @@ -287,9 +281,6 @@ var ScrollView = React.createClass({ ...this.props, alwaysBounceHorizontal, alwaysBounceVertical, - keyboardDismissMode: this.props.keyboardDismissMode ? - keyboardDismissModeConstants[this.props.keyboardDismissMode] : - undefined, style: ([styles.base, this.props.style]: ?Array), onTouchStart: this.scrollResponderHandleTouchStart, onTouchMove: this.scrollResponderHandleTouchMove, @@ -308,7 +299,7 @@ var ScrollView = React.createClass({ onResponderRelease: this.scrollResponderHandleResponderRelease, onResponderReject: this.scrollResponderHandleResponderReject, }; - + var ScrollViewClass; if (Platform.OS === 'ios') { ScrollViewClass = RCTScrollView; @@ -318,6 +309,13 @@ var ScrollView = React.createClass({ } else { ScrollViewClass = AndroidScrollView; } + var keyboardDismissModeConstants = { + 'none': RCTScrollViewConsts.KeyboardDismissMode.None, // default + 'interactive': RCTScrollViewConsts.KeyboardDismissMode.Interactive, + 'on-drag': RCTScrollViewConsts.KeyboardDismissMode.OnDrag, + }; + props.keyboardDismissMode = props.keyboardDismissMode ? + keyboardDismissModeConstants[props.keyboardDismissMode] : undefined; } invariant( ScrollViewClass !== undefined, diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js index 28fbea027..848144bff 100644 --- a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js @@ -17,7 +17,7 @@ var StyleSheet = require('StyleSheet'); var Text = require('Text'); var View = require('View'); -var Dummy = React.createClass({ +var DummySegmentedControlIOS = React.createClass({ render: function() { return ( @@ -46,4 +46,4 @@ var styles = StyleSheet.create({ } }); -module.exports = Dummy; +module.exports = DummySegmentedControlIOS; diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js index 23d952776..ec3b6c614 100644 --- a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js @@ -108,13 +108,7 @@ var styles = StyleSheet.create({ var RCTSegmentedControl = requireNativeComponent( 'RCTSegmentedControl', - null + SegmentedControlIOS ); -if (__DEV__) { - verifyPropTypes( - RCTSegmentedControl, - RCTSegmentedControl.viewConfig - ); -} module.exports = SegmentedControlIOS; diff --git a/Libraries/Components/StatusBar/StatusBarIOS.ios.js b/Libraries/Components/StatusBar/StatusBarIOS.ios.js index 14a5cecf2..adfed78b7 100644 --- a/Libraries/Components/StatusBar/StatusBarIOS.ios.js +++ b/Libraries/Components/StatusBar/StatusBarIOS.ios.js @@ -13,26 +13,26 @@ var RCTStatusBarManager = require('NativeModules').StatusBarManager; +type StatusBarStyle = $Enum<{ + 'default': string, + 'light-content': string, +}>; + +type StatusBarAnimation = $Enum<{ + 'none': string, + 'fade': string, + 'slide': string, +}>; + var StatusBarIOS = { - Style: { - default: RCTStatusBarManager.Style.default, - lightContent: RCTStatusBarManager.Style.lightContent - }, - - Animation: { - none: RCTStatusBarManager.Animation.none, - fade: RCTStatusBarManager.Animation.fade, - slide: RCTStatusBarManager.Animation.slide, - }, - - setStyle(style: number, animated?: boolean) { + setStyle(style: StatusBarStyle, animated?: boolean) { animated = animated || false; RCTStatusBarManager.setStyle(style, animated); }, - setHidden(hidden: boolean, animation: number) { - animation = animation || StatusBarIOS.Animation.none; + setHidden(hidden: boolean, animation?: StatusBarAnimation) { + animation = animation || 'none'; RCTStatusBarManager.setHidden(hidden, animation); }, }; diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 03f374b67..7d3f04b33 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -31,10 +31,6 @@ var emptyFunction = require('emptyFunction'); var invariant = require('invariant'); var merge = require('merge'); -var autoCapitalizeConsts = RCTUIManager.UIText.AutocapitalizationType; -var keyboardTypeConsts = RCTUIManager.UIKeyboardType; -var returnKeyTypeConsts = RCTUIManager.UIReturnKeyType; - var RCTTextViewAttributes = merge(ReactNativeViewAttributes.UIView, { autoCorrect: true, autoCapitalize: true, @@ -96,10 +92,6 @@ var viewConfigAndroid = { validAttributes: AndroidTextInputAttributes, }; -var crossPlatformKeyboardTypeMap = { - 'numeric': 'decimal-pad', -}; - type DefaultProps = { bufferDelay: number; }; @@ -171,8 +163,11 @@ var TextInput = React.createClass({ * Determines which keyboard to open, e.g.`numeric`. */ keyboardType: PropTypes.oneOf([ - 'default', - // iOS + // Cross-platform + 'default', + 'numeric', + 'email-address', + // iOS-only 'ascii-capable', 'numbers-and-punctuation', 'url', @@ -182,9 +177,6 @@ var TextInput = React.createClass({ 'decimal-pad', 'twitter', 'web-search', - // Cross-platform - 'numeric', - 'email-address', ]), /** * Determines how the return key should look. @@ -426,18 +418,12 @@ var TextInput = React.createClass({ _renderIOS: function() { var textContainer; - var autoCapitalize = autoCapitalizeConsts[this.props.autoCapitalize]; - var clearButtonMode = RCTUIManager.UITextField.clearButtonMode[this.props.clearButtonMode]; + var props = this.props; + props.style = [styles.input, this.props.style]; - var keyboardType = keyboardTypeConsts[ - crossPlatformKeyboardTypeMap[this.props.keyboardType] || - this.props.keyboardType - ]; - var returnKeyType = returnKeyTypeConsts[this.props.returnKeyType]; - - if (!this.props.multiline) { + if (!props.multiline) { for (var propKey in onlyMultiline) { - if (this.props[propKey]) { + if (props[propKey]) { throw new Error( 'TextInput prop `' + propKey + '` is only supported with multiline.' ); @@ -446,77 +432,48 @@ var TextInput = React.createClass({ textContainer = true} - onLayout={this.props.onLayout} - placeholder={this.props.placeholder} - placeholderTextColor={this.props.placeholderTextColor} text={this.state.bufferedValue} - autoCapitalize={autoCapitalize} - autoCorrect={this.props.autoCorrect} - clearButtonMode={clearButtonMode} - clearTextOnFocus={this.props.clearTextOnFocus} - selectTextOnFocus={this.props.selectTextOnFocus} />; } else { for (var propKey in notMultiline) { - if (this.props[propKey]) { + if (props[propKey]) { throw new Error( 'TextInput prop `' + propKey + '` cannot be used with multiline.' ); } } - var children = this.props.children; + var children = props.children; var childCount = 0; ReactChildren.forEach(children, () => ++childCount); invariant( - !(this.props.value && childCount), + !(props.value && childCount), 'Cannot specify both value and children.' ); if (childCount > 1) { children = {children}; } - if (this.props.inputView) { - children = [children, this.props.inputView]; + if (props.inputView) { + children = [children, props.inputView]; } textContainer = ; } @@ -524,14 +481,14 @@ var TextInput = React.createClass({ + testID={props.testID}> {textContainer} ); }, _renderAndroid: function() { - var autoCapitalize = autoCapitalizeConsts[this.props.autoCapitalize]; + var autoCapitalize = RCTUIManager.UIText.AutocapitalizationType[this.props.autoCapitalize]; var children = this.props.children; var childCount = 0; ReactChildren.forEach(children, () => ++childCount); diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 533652f65..dcbfbeee1 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -23,6 +23,7 @@ var View = require('View'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var keyOf = require('keyOf'); var merge = require('merge'); var onlyChild = require('onlyChild'); @@ -111,6 +112,7 @@ var TouchableHighlight = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -119,6 +121,7 @@ var TouchableHighlight = React.createClass({ }, componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); if (nextProps.activeOpacity !== this.props.activeOpacity || nextProps.underlayColor !== this.props.underlayColor || nextProps.style !== this.props.style) { @@ -152,7 +155,8 @@ var TouchableHighlight = React.createClass({ touchableHandlePress: function() { this.clearTimeout(this._hideTimeout); this._showUnderlay(); - this._hideTimeout = this.setTimeout(this._hideUnderlay, 100); + this._hideTimeout = this.setTimeout(this._hideUnderlay, + this.props.delayPressOut || 100); this.props.onPress && this.props.onPress(); }, @@ -164,6 +168,18 @@ var TouchableHighlight = React.createClass({ return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! }, + touchableGetHighlightDelayMS: function() { + return this.props.delayPressIn; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + _showUnderlay: function() { this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps); this.refs[CHILD_REF].setNativeProps(this.state.activeProps); diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index d99bf7380..a0891714f 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -15,11 +15,13 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var POPAnimationMixin = require('POPAnimationMixin'); var React = require('React'); +var TimerMixin = require('react-timer-mixin'); var Touchable = require('Touchable'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var flattenStyle = require('flattenStyle'); var keyOf = require('keyOf'); var onlyChild = require('onlyChild'); @@ -50,7 +52,7 @@ var onlyChild = require('onlyChild'); */ var TouchableOpacity = React.createClass({ - mixins: [Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], + mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], propTypes: { ...TouchableWithoutFeedback.propTypes, @@ -72,6 +74,7 @@ var TouchableOpacity = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -79,6 +82,10 @@ var TouchableOpacity = React.createClass({ ensureComponentIsNative(this.refs[CHILD_REF]); }, + componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); + }, + setOpacityTo: function(value) { if (POPAnimationMixin) { // Reset with animation if POP is available @@ -86,6 +93,7 @@ var TouchableOpacity = React.createClass({ var anim = { type: this.AnimationTypes.linear, property: this.AnimationProperties.opacity, + duration: 0.15, toValue: value, }; this.startAnimation(CHILD_REF, anim); @@ -102,20 +110,26 @@ var TouchableOpacity = React.createClass({ * defined on your component. */ touchableHandleActivePressIn: function() { - this.refs[CHILD_REF].setNativeProps({ - opacity: this.props.activeOpacity - }); + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this._opacityActive(); this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { - var child = onlyChild(this.props.children); - var childStyle = flattenStyle(child.props.style) || {}; - this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); + if (!this._hideTimeout) { + this._opacityInactive(); + } this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { + this.clearTimeout(this._hideTimeout); + this._opacityActive(); + this._hideTimeout = this.setTimeout( + this._opacityInactive, + this.props.delayPressOut || 100 + ); this.props.onPress && this.props.onPress(); }, @@ -128,7 +142,30 @@ var TouchableOpacity = React.createClass({ }, touchableGetHighlightDelayMS: function() { - return 0; + return this.props.delayPressIn || 0; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress === 0 ? 0 : + this.props.delayLongPress || 500; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + + _opacityActive: function() { + this.setOpacityTo(this.props.activeOpacity); + }, + + _opacityInactive: function() { + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + var child = onlyChild(this.props.children); + var childStyle = flattenStyle(child.props.style) || {}; + this.setOpacityTo( + childStyle.opacity === undefined ? 1 : childStyle.opacity + ); }, render: function() { diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index cd9ea02fd..227cbeae2 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -12,7 +12,9 @@ 'use strict'; var React = require('React'); +var TimerMixin = require('react-timer-mixin'); var Touchable = require('Touchable'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var onlyChild = require('onlyChild'); /** @@ -31,23 +33,44 @@ type Event = Object; * one of the primary reason a "web" app doesn't feel "native". */ var TouchableWithoutFeedback = React.createClass({ - mixins: [Touchable.Mixin], + mixins: [TimerMixin, Touchable.Mixin], propTypes: { /** * Called when the touch is released, but not if cancelled (e.g. by a scroll * that steals the responder lock). */ + accessible: React.PropTypes.bool, onPress: React.PropTypes.func, onPressIn: React.PropTypes.func, onPressOut: React.PropTypes.func, onLongPress: React.PropTypes.func, + /** + * Delay in ms, from the start of the touch, before onPressIn is called. + */ + delayPressIn: React.PropTypes.number, + /** + * Delay in ms, from the release of the touch, before onPressOut is called. + */ + delayPressOut: React.PropTypes.number, + /** + * Delay in ms, from onPressIn, before onLongPress is called. + */ + delayLongPress: React.PropTypes.number, }, getInitialState: function() { return this.touchableGetInitialState(); }, + componentDidMount: function() { + ensurePositiveDelayProps(this.props); + }, + + componentWillReceiveProps: function(nextProps: Object) { + ensurePositiveDelayProps(nextProps); + }, + /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. @@ -73,13 +96,22 @@ var TouchableWithoutFeedback = React.createClass({ }, touchableGetHighlightDelayMS: function(): number { - return 0; + return this.props.delayPressIn || 0; + }, + + touchableGetLongPressDelayMS: function(): number { + return this.props.delayLongPress === 0 ? 0 : + this.props.delayLongPress || 500; + }, + + touchableGetPressOutDelayMS: function(): number { + return this.props.delayPressOut || 0; }, render: function(): ReactElement { // Note(avik): remove dynamic typecast once Flow has been upgraded return (React: any).cloneElement(onlyChild(this.props.children), { - accessible: true, + accessible: this.props.accessible !== false, testID: this.props.testID, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, diff --git a/Libraries/Components/Touchable/ensurePositiveDelayProps.js b/Libraries/Components/Touchable/ensurePositiveDelayProps.js new file mode 100644 index 000000000..4c6525a54 --- /dev/null +++ b/Libraries/Components/Touchable/ensurePositiveDelayProps.js @@ -0,0 +1,24 @@ +/** + * 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 ensurePositiveDelayProps + * @flow + */ +'use strict'; + +var invariant = require('invariant'); + +var ensurePositiveDelayProps = function(props: any) { + invariant( + !(props.delayPressIn < 0 || props.delayPressOut < 0 || + props.delayLongPress < 0), + 'Touchable components cannot have negative delay properties' + ); +}; + +module.exports = ensurePositiveDelayProps; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 9b84937af..23e2c7a63 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -146,20 +146,11 @@ var Image = React.createClass({ if (this.props.style && this.props.style.tintColor) { warning(RawImage === RCTStaticImage, 'tintColor style only supported on static images.'); } - var resizeMode = this.props.resizeMode || style.resizeMode; - var contentModes = NativeModules.UIManager.UIView.ContentMode; - var contentMode; - if (resizeMode === ImageResizeMode.stretch) { - contentMode = contentModes.ScaleToFill; - } else if (resizeMode === ImageResizeMode.contain) { - contentMode = contentModes.ScaleAspectFit; - } else { // ImageResizeMode.cover or undefined - contentMode = contentModes.ScaleAspectFill; - } + var resizeMode = this.props.resizeMode || style.resizeMode || 'cover'; var nativeProps = merge(this.props, { style, - contentMode, + resizeMode, tintColor: style.tintColor, }); if (isStored) { @@ -187,7 +178,7 @@ var nativeOnlyProps = { src: true, defaultImageSrc: true, imageTag: true, - contentMode: true, + resizeMode: true, }; if (__DEV__) { verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps); diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index aa524ef56..7ff8c6379 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -82,8 +82,8 @@ static NSString *RCTCacheKeyForURL(NSURL *url) RCTImageDownloader *strongSelf = weakSelf; NSArray *blocks = strongSelf->_pendingBlocks[cacheKey]; [strongSelf->_pendingBlocks removeObjectForKey:cacheKey]; - for (RCTCachedDataDownloadBlock block in blocks) { - block(cached, data, error); + for (RCTCachedDataDownloadBlock cacheDownloadBlock in blocks) { + cacheDownloadBlock(cached, data, error); } }); }; diff --git a/Libraries/Image/RCTNetworkImageViewManager.m b/Libraries/Image/RCTNetworkImageViewManager.m index 2ecf69971..005b726cf 100644 --- a/Libraries/Image/RCTNetworkImageViewManager.m +++ b/Libraries/Image/RCTNetworkImageViewManager.m @@ -29,6 +29,6 @@ RCT_EXPORT_MODULE() RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage) RCT_REMAP_VIEW_PROPERTY(src, imageURL, NSURL) -RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode) +RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) @end diff --git a/Libraries/Image/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m index ce6aab187..bdc6f0596 100644 --- a/Libraries/Image/RCTStaticImageManager.m +++ b/Libraries/Image/RCTStaticImageManager.m @@ -26,7 +26,7 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets) -RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode) +RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) RCT_CUSTOM_VIEW_PROPERTY(src, NSURL, RCTStaticImage) { if (json) { diff --git a/Libraries/Inspector.js b/Libraries/Inspector.js index 5c70be2e8..e0017c3cf 100644 --- a/Libraries/Inspector.js +++ b/Libraries/Inspector.js @@ -50,6 +50,9 @@ function findInstanceByNativeTag(rootTag, nativeTag) { var containerID = ReactNativeTagHandles.tagToRootNodeID[rootTag]; var rootInstance = ReactNativeMount._instancesByContainerID[containerID]; var targetID = ReactNativeTagHandles.tagToRootNodeID[nativeTag]; + if (!targetID) { + return undefined; + } return findInstance(rootInstance, targetID); } diff --git a/Libraries/Picker/PickerIOS.ios.js b/Libraries/Picker/PickerIOS.ios.js index b2c3c3927..42302fa26 100644 --- a/Libraries/Picker/PickerIOS.ios.js +++ b/Libraries/Picker/PickerIOS.ios.js @@ -20,8 +20,7 @@ var RCTPickerIOSConsts = require('NativeModules').UIManager.RCTPicker.Constants; var StyleSheet = require('StyleSheet'); var View = require('View'); -var createReactNativeComponentClass = - require('createReactNativeComponentClass'); +var requireNativeComponent = require('requireNativeComponent'); var merge = require('merge'); var PICKER = 'picker'; @@ -60,7 +59,7 @@ var PickerIOS = React.createClass({ { - handler(new PushNotificationIOS(notifData)); - } + type === 'notification' || type === 'register', + 'PushNotificationIOS only supports `notification` and `register` events' ); + if (type === 'notification') { + _notifHandlers[handler] = RCTDeviceEventEmitter.addListener( + DEVICE_NOTIF_EVENT, + (notifData) => { + handler(new PushNotificationIOS(notifData)); + } + ); + } else if (type === 'register') { + _notifHandlers[handler] = RCTDeviceEventEmitter.addListener( + NOTIF_REGISTER_EVENT, + (registrationInfo) => { + handler(registrationInfo.deviceToken); + } + ); + } } /** - * Requests all notification permissions from iOS, prompting the user's - * dialog box. + * Requests notification permissions from iOS, prompting the user's + * dialog box. By default, it will request all notification permissions, but + * a subset of these can be requested by passing a map of requested + * permissions. + * The following permissions are supported: + * + * - `alert` + * - `badge` + * - `sound` + * + * If a map is provided to the method, only the permissions with truthy values + * will be requested. */ - static requestPermissions() { - RCTPushNotificationManager.requestPermissions(); + static requestPermissions(permissions?: { + alert?: boolean, + badge?: boolean, + sound?: boolean + }) { + var requestedPermissions = {}; + if (permissions) { + requestedPermissions = { + alert: !!permissions.alert, + badge: !!permissions.badge, + sound: !!permissions.sound + }; + } else { + requestedPermissions = { + alert: true, + badge: true, + sound: true + }; + } + RCTPushNotificationManager.requestPermissions(requestedPermissions); } /** @@ -97,8 +140,8 @@ class PushNotificationIOS { */ static removeEventListener(type: string, handler: Function) { invariant( - type === 'notification', - 'PushNotificationIOS only supports `notification` events' + type === 'notification' || type === 'register', + 'PushNotificationIOS only supports `notification` and `register` events' ); if (!_notifHandlers[handler]) { return; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index ef1ba1496..194bbc5dd 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -14,6 +14,7 @@ @interface RCTPushNotificationManager : NSObject + (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; ++ (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; + (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification; @end diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 4846c885e..1966c6045 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -12,7 +12,18 @@ #import "RCTBridge.h" #import "RCTEventDispatcher.h" +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + +#define UIUserNotificationTypeAlert UIRemoteNotificationTypeAlert +#define UIUserNotificationTypeBadge UIRemoteNotificationTypeBadge +#define UIUserNotificationTypeSound UIRemoteNotificationTypeSound +#define UIUserNotificationTypeNone UIRemoteNotificationTypeNone +#define UIUserNotificationType UIRemoteNotificationType + +#endif + NSString *const RCTRemoteNotificationReceived = @"RemoteNotificationReceived"; +NSString *const RCTRemoteNotificationsRegistered = @"RemoteNotificationsRegistered"; @implementation RCTPushNotificationManager { @@ -30,6 +41,10 @@ RCT_EXPORT_MODULE() selector:@selector(handleRemoteNotificationReceived:) name:RCTRemoteNotificationReceived object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRemoteNotificationsRegistered:) + name:RCTRemoteNotificationsRegistered + object:nil]; } return self; } @@ -52,6 +67,21 @@ RCT_EXPORT_MODULE() } } ++ (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + NSMutableString *hexString = [NSMutableString string]; + const unsigned char *bytes = [deviceToken bytes]; + for (int i = 0; i < [deviceToken length]; i++) { + [hexString appendFormat:@"%02x", bytes[i]]; + } + NSDictionary *userInfo = @{ + @"deviceToken" : [hexString copy] + }; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationsRegistered + object:self + userInfo:userInfo]; +} + + (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification { [[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationReceived @@ -65,6 +95,12 @@ RCT_EXPORT_MODULE() body:[notification userInfo]]; } +- (void)handleRemoteNotificationsRegistered:(NSNotification *)notification +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationsRegistered" + body:[notification userInfo]]; +} + /** * Update the application icon badge number on the home screen */ @@ -83,36 +119,35 @@ RCT_EXPORT_METHOD(getApplicationIconBadgeNumber:(RCTResponseSenderBlock)callback ]); } -RCT_EXPORT_METHOD(requestPermissions) +RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions) { - Class _UIUserNotificationSettings; - if ((_UIUserNotificationSettings = NSClassFromString(@"UIUserNotificationSettings"))) { - UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert; - UIUserNotificationSettings *notificationSettings = [_UIUserNotificationSettings settingsForTypes:types categories:nil]; - [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; + UIUserNotificationType types = UIRemoteNotificationTypeNone; + if (permissions) { + if ([permissions[@"alert"] boolValue]) { + types |= UIUserNotificationTypeAlert; + } + if ([permissions[@"badge"] boolValue]) { + types |= UIUserNotificationTypeBadge; + } + if ([permissions[@"sound"] boolValue]) { + types |= UIUserNotificationTypeSound; + } } else { + types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; + } -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - - [[UIApplication sharedApplication] registerForRemoteNotificationTypes: - UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert]; - +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 + id notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; + [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; + [[UIApplication sharedApplication] registerForRemoteNotifications]; +#else + [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types]; #endif - } } RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) { - -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - -#define UIUserNotificationTypeAlert UIRemoteNotificationTypeAlert -#define UIUserNotificationTypeBadge UIRemoteNotificationTypeBadge -#define UIUserNotificationTypeSound UIRemoteNotificationTypeSound - -#endif - NSUInteger types = 0; if ([UIApplication instancesRespondToSelector:@selector(currentUserNotificationSettings)]) { types = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 9c0cacf70..8a7e739bb 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -35,7 +35,11 @@ sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"]; _testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName]; _testController.referenceImagesDirectory = referenceDir; +#if RUNNING_ON_CI + _scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; +#else _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]]; +#endif } return self; } diff --git a/Libraries/ReactIOS/InspectorOverlay/BorderBox.js b/Libraries/ReactIOS/InspectorOverlay/BorderBox.js new file mode 100644 index 000000000..caee8ccd6 --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/BorderBox.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule BorderBox + * @flow + */ +'use strict'; + +var React = require('React'); +var View = require('View'); + +class BorderBox extends React.Component { + render() { + var box = this.props.box; + if (!box) { + return this.props.children; + } + var style = { + borderTopWidth: box.top, + borderBottomWidth: box.bottom, + borderLeftWidth: box.left, + borderRightWidth: box.right, + }; + return ( + + {this.props.children} + + ); + } +} + +module.exports = BorderBox; + diff --git a/Libraries/ReactIOS/InspectorOverlay/BoxInspector.js b/Libraries/ReactIOS/InspectorOverlay/BoxInspector.js new file mode 100644 index 000000000..e50d9869a --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/BoxInspector.js @@ -0,0 +1,113 @@ +/** + * 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 BoxInspector + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); +var resolveBoxStyle = require('resolveBoxStyle'); + +var blank = { + top: 0, + left: 0, + right: 0, + bottom: 0, +}; + +class BoxInspector extends React.Component { + render() { + var frame = this.props.frame; + var style = this.props.style; + var margin = style && resolveBoxStyle('margin', style) || blank; + var padding = style && resolveBoxStyle('padding', style) || blank; + return ( + + + + + ({frame.left}, {frame.top}) + + + {frame.width} × {frame.height} + + + + + ); + } +} + +class BoxContainer extends React.Component { + render() { + var box = this.props.box; + return ( + + + {this.props.title} + {box.top} + + + {box.left} + {this.props.children} + {box.right} + + {box.bottom} + + ); + } +} + +var styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + }, + marginLabel: { + width: 60, + }, + label: { + fontSize: 10, + color: 'rgb(255,100,0)', + marginLeft: 5, + flex: 1, + textAlign: 'left', + top: -3, + }, + buffer: { + fontSize: 10, + color: 'yellow', + flex: 1, + textAlign: 'center', + }, + innerText: { + color: 'yellow', + fontSize: 12, + textAlign: 'center', + width: 70, + }, + box: { + borderWidth: 1, + borderColor: 'grey', + }, + boxText: { + color: 'white', + fontSize: 12, + marginHorizontal: 3, + marginVertical: 2, + textAlign: 'center', + }, +}); + +module.exports = BoxInspector; + diff --git a/Libraries/ReactIOS/InspectorOverlay/ElementBox.js b/Libraries/ReactIOS/InspectorOverlay/ElementBox.js new file mode 100644 index 000000000..a3851001c --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/ElementBox.js @@ -0,0 +1,74 @@ +/** + * 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 ElementBox + * @flow + */ +'use strict'; + +var React = require('React'); +var View = require('View'); +var StyleSheet = require('StyleSheet'); +var BorderBox = require('BorderBox'); +var resolveBoxStyle = require('resolveBoxStyle'); + +var flattenStyle = require('flattenStyle'); + +class ElementBox extends React.Component { + render() { + var style = flattenStyle(this.props.style) || {}; + var margin = resolveBoxStyle('margin', style); + var padding = resolveBoxStyle('padding', style); + var frameStyle = this.props.frame; + if (margin) { + frameStyle = { + top: frameStyle.top - margin.top, + left: frameStyle.left - margin.left, + height: frameStyle.height + margin.top + margin.bottom, + width: frameStyle.width + margin.left + margin.right, + }; + } + var contentStyle = { + width: this.props.frame.width, + height: this.props.frame.height, + }; + if (padding) { + contentStyle = { + width: contentStyle.width - padding.left - padding.right, + height: contentStyle.height - padding.top - padding.bottom, + }; + } + return ( + + + + + + + + ); + } +} + +var styles = StyleSheet.create({ + frame: { + position: 'absolute', + }, + content: { + backgroundColor: 'rgba(200, 230, 255, 0.8)', + }, + padding: { + borderColor: 'rgba(77, 255, 0, 0.3)', + }, + margin: { + borderColor: 'rgba(255, 132, 0, 0.3)', + }, +}); + +module.exports = ElementBox; + diff --git a/Libraries/ReactIOS/InspectorOverlay/ElementProperties.js b/Libraries/ReactIOS/InspectorOverlay/ElementProperties.js new file mode 100644 index 000000000..310374fb1 --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/ElementProperties.js @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ElementProperties + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); +var PropTypes = require('ReactPropTypes'); +var BoxInspector = require('BoxInspector'); +var StyleInspector = require('StyleInspector'); +var TouchableHighlight = require('TouchableHighlight'); +var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); + +var flattenStyle = require('flattenStyle'); +var mapWithSeparator = require('mapWithSeparator'); + +var ElementProperties = React.createClass({ + propTypes: { + hierarchy: PropTypes.array.isRequired, + style: PropTypes.array.isRequired, + }, + + render: function() { + var style = flattenStyle(this.props.style); + var selection = this.props.selection; + // Without the `TouchableWithoutFeedback`, taps on this inspector pane + // would change the inspected element to whatever is under the inspector + return ( + + + + {mapWithSeparator( + this.props.hierarchy, + (item, i) => ( + this.props.setSelection(i)}> + + {item.getName ? item.getName() : 'Unknown'} + + + ), + () => + )} + + + + + + + + ); + } +}); + +var styles = StyleSheet.create({ + breadSep: { + fontSize: 8, + color: 'white', + }, + breadcrumb: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 5, + }, + selected: { + borderColor: 'white', + borderRadius: 5, + }, + breadItem: { + borderWidth: 1, + borderColor: 'transparent', + marginHorizontal: 2, + }, + breadItemText: { + fontSize: 10, + color: 'white', + marginHorizontal: 5, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + info: { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + padding: 10, + }, + path: { + color: 'white', + fontSize: 9, + }, +}); + +module.exports = ElementProperties; diff --git a/Libraries/ReactIOS/InspectorOverlay.js b/Libraries/ReactIOS/InspectorOverlay/InspectorOverlay.js similarity index 53% rename from Libraries/ReactIOS/InspectorOverlay.js rename to Libraries/ReactIOS/InspectorOverlay/InspectorOverlay.js index eeb6e7965..3b391502f 100644 --- a/Libraries/ReactIOS/InspectorOverlay.js +++ b/Libraries/ReactIOS/InspectorOverlay/InspectorOverlay.js @@ -17,12 +17,16 @@ var StyleSheet = require('StyleSheet'); var Text = require('Text'); var UIManager = require('NativeModules').UIManager; var View = require('View'); +var ElementBox = require('ElementBox'); +var ElementProperties = require('ElementProperties'); var InspectorOverlay = React.createClass({ getInitialState: function() { return { frame: null, + pointerY: 0, hierarchy: [], + selection: -1, }; }, @@ -33,15 +37,34 @@ var InspectorOverlay = React.createClass({ [locationX, locationY], (nativeViewTag, left, top, width, height) => { var instance = Inspector.findInstanceByNativeTag(this.props.rootTag, nativeViewTag); + if (!instance) { + return; + } var hierarchy = Inspector.getOwnerHierarchy(instance); + var publicInstance = instance.getPublicInstance(); this.setState({ hierarchy, - frame: {left, top, width, height} + pointerY: locationY, + selection: hierarchy.length - 1, + frame: {left, top, width, height}, + style: publicInstance.props ? publicInstance.props.style : {}, }); } ); }, + setSelection(i) { + var instance = this.state.hierarchy[i]; + var publicInstance = instance.getPublicInstance(); + UIManager.measure(React.findNodeHandle(instance), (x, y, width, height, left, top) => { + this.setState({ + frame: {left, top, width, height}, + style: publicInstance.props ? publicInstance.props.style : {}, + selection: i, + }); + }); + }, + shouldSetResponser: function(e) { this.findViewForTouchEvent(e); return true; @@ -49,18 +72,32 @@ var InspectorOverlay = React.createClass({ render: function() { var content = []; + var justifyContent = 'flex-end'; 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 distanceToTop = this.state.pointerY; + var distanceToBottom = Dimensions.get('window').height - distanceToTop; - var justifyContent = distanceToTop > distanceToBottom + justifyContent = distanceToTop > distanceToBottom ? 'flex-start' : 'flex-end'; - content.push(); - content.push(); + content.push(); + content.push( + + ); + } else { + content.push( + + Welcome to the inspector! Tap something to inspect it. + + ); } return ( { - return instance.getName ? instance.getName() : 'Unknown'; - }).join(' > '); - return ( - - - {path} - - - ); - } -}); - var styles = StyleSheet.create({ + welcomeMessage: { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + padding: 10, + paddingVertical: 50, + }, + welcomeText: { + color: 'white', + }, inspector: { - backgroundColor: 'rgba(255,255,255,0.8)', + backgroundColor: 'rgba(255,255,255,0.0)', 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/InspectorOverlay/StyleInspector.js b/Libraries/ReactIOS/InspectorOverlay/StyleInspector.js new file mode 100644 index 000000000..702d01e1d --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/StyleInspector.js @@ -0,0 +1,63 @@ +/** + * 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 StyleInspector + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); + +class StyleInspector extends React.Component { + render() { + if (!this.props.style) { + return No style; + } + var names = Object.keys(this.props.style); + return ( + + + {names.map(name => {name}:)} + + + {names.map(name => {this.props.style[name]})} + + + ); + } +} + +var styles = StyleSheet.create({ + container: { + flexDirection: 'row', + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + }, + attr: { + fontSize: 10, + color: '#ccc', + }, + value: { + fontSize: 10, + color: 'white', + marginLeft: 10, + }, + noStyle: { + color: 'white', + fontSize: 10, + }, +}); + +module.exports = StyleInspector; + diff --git a/Libraries/ReactIOS/InspectorOverlay/resolveBoxStyle.js b/Libraries/ReactIOS/InspectorOverlay/resolveBoxStyle.js new file mode 100644 index 000000000..e0bfb601c --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/resolveBoxStyle.js @@ -0,0 +1,59 @@ +/** + * 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 resolveBoxStyle + * @flow + */ +'use strict'; + +/** + * Resolve a style property into it's component parts, e.g. + * + * resolveProperties('margin', {margin: 5, marginBottom: 10}) + * -> + * {top: 5, left: 5, right: 5, bottom: 10} + * + * If none are set, returns false. + */ +function resolveBoxStyle(prefix: String, style: Object): ?Object { + var res = {}; + var subs = ['top', 'left', 'bottom', 'right']; + var set = false; + subs.forEach(sub => { + res[sub] = style[prefix] || 0; + }); + if (style[prefix]) { + set = true; + } + if (style[prefix + 'Vertical']) { + res.top = res.bottom = style[prefix + 'Vertical']; + set = true; + } + if (style[prefix + 'Horizontal']) { + res.left = res.right = style[prefix + 'Horizontal']; + set = true; + } + subs.forEach(sub => { + var val = style[prefix + capFirst(sub)]; + if (val) { + res[sub] = val; + set = true; + } + }); + if (!set) { + return; + } + return res; +} + +function capFirst(text) { + return text[0].toUpperCase() + text.slice(1); +} + +module.exports = resolveBoxStyle; + diff --git a/Libraries/Utilities/mapWithSeparator.js b/Libraries/Utilities/mapWithSeparator.js new file mode 100644 index 000000000..4aa8665c3 --- /dev/null +++ b/Libraries/Utilities/mapWithSeparator.js @@ -0,0 +1,19 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule mapWithSeparator + */ +'use strict'; + +function mapWithSeparator(array, valueFunction, separatorFunction) { + var results = []; + for (var i = 0; i < array.length; i++) { + results.push(valueFunction(array[i], i, array)); + if (i !== array.length - 1) { + results.push(separatorFunction(i)); + } + } + return results; +} + +module.exports = mapWithSeparator; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index c81183b5b..6b1c992cc 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -24,11 +24,12 @@ var ReactNative = Object.assign(Object.create(require('React')), { Image: require('Image'), ListView: require('ListView'), MapView: require('MapView'), + Navigator: require('Navigator'), NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), - Navigator: require('Navigator'), - SegmentedControlIOS: require('SegmentedControlIOS'), + ProgressViewIOS: require('ProgressViewIOS'), ScrollView: require('ScrollView'), + SegmentedControlIOS: require('SegmentedControlIOS'), SliderIOS: require('SliderIOS'), SwitchIOS: require('SwitchIOS'), TabBarIOS: require('TabBarIOS'), @@ -47,12 +48,12 @@ var ReactNative = Object.assign(Object.create(require('React')), { AsyncStorage: require('AsyncStorage'), CameraRoll: require('CameraRoll'), InteractionManager: require('InteractionManager'), - LinkingIOS: require('LinkingIOS'), LayoutAnimation: require('LayoutAnimation'), + LinkingIOS: require('LinkingIOS'), NetInfo: require('NetInfo'), + PanResponder: require('PanResponder'), PixelRatio: require('PixelRatio'), PushNotificationIOS: require('PushNotificationIOS'), - PanResponder: require('PanResponder'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), VibrationIOS: require('VibrationIOS'), diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index 37c423827..2a0dd1813 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -232,6 +232,8 @@ var PRESS_EXPAND_PX = 20; var LONG_PRESS_THRESHOLD = 500; +var LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS; + var LONG_PRESS_ALLOWED_MOVEMENT = 10; // Default amount "active" region protrudes beyond box @@ -276,7 +278,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; * + * | RESPONDER_GRANT (HitRect) * v - * +---------------------------+ DELAY +-------------------------+ T - DELAY +------------------------------+ + * +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+ * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN| * +---------------------------+ +-------------------------+ +------------------------------+ * + ^ + ^ + ^ @@ -288,7 +290,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT| * +----------------------------+ +--------------------------+ +-------------------------------+ * - * T - DELAY => LONG_PRESS_THRESHOLD - DELAY + * T + DELAY => LONG_PRESS_DELAY_MS + DELAY * * Not drawn are the side effects of each transition. The most important side * effect is the `touchableHandlePress` abstract method invocation that occurs @@ -348,12 +350,16 @@ var TouchableMixin = { // event to make sure it doesn't get reused in the event object pool. e.persist(); + this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); + this.pressOutDelayTimeout = null; + this.state.touchable.touchState = States.NOT_RESPONDER; this.state.touchable.responderID = dispatchID; this._receiveSignal(Signals.RESPONDER_GRANT, e); var delayMS = this.touchableGetHighlightDelayMS !== undefined ? - this.touchableGetHighlightDelayMS() : HIGHLIGHT_DELAY_MS; + Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS; + delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS; if (delayMS !== 0) { this.touchableDelayTimeout = setTimeout( this._handleDelay.bind(this, e), @@ -363,9 +369,13 @@ var TouchableMixin = { this._handleDelay(e); } + var longDelayMS = + this.touchableGetLongPressDelayMS !== undefined ? + Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS; + longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS; this.longPressDelayTimeout = setTimeout( this._handleLongDelay.bind(this, e), - LONG_PRESS_THRESHOLD - delayMS + longDelayMS + delayMS ); }, @@ -632,8 +642,14 @@ var TouchableMixin = { if (newIsHighlight && !curIsHighlight) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(); - } else if (!newIsHighlight && curIsHighlight) { - this.touchableHandleActivePressOut && this.touchableHandleActivePressOut(); + } else if (!newIsHighlight && curIsHighlight && this.touchableHandleActivePressOut) { + if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) { + this.pressOutDelayTimeout = this.setTimeout(function() { + this.touchableHandleActivePressOut(); + }, this.touchableGetPressOutDelayMS()); + } else { + this.touchableHandleActivePressOut(); + } } if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) { diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 70868f721..98808fb3f 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -244,30 +244,19 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) @implementation RCTModuleMethod { - BOOL _isClassMethod; Class _moduleClass; SEL _selector; NSMethodSignature *_methodSignature; NSArray *_argumentBlocks; - NSString *_methodName; dispatch_block_t _methodQueue; } -static NSString *RCTStringUpToFirstArgument(NSString *methodName) -{ - NSRange colonRange = [methodName rangeOfString:@":"]; - if (colonRange.length) { - methodName = [methodName substringToIndex:colonRange.location]; - } - return methodName; -} - - (instancetype)initWithReactMethodName:(NSString *)reactMethodName objCMethodName:(NSString *)objCMethodName JSMethodName:(NSString *)JSMethodName { if ((self = [super init])) { - _methodName = reactMethodName; + NSArray *parts = [[reactMethodName substringWithRange:(NSRange){2, reactMethodName.length - 3}] componentsSeparatedByString:@" "]; // Parse class and method @@ -277,36 +266,33 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) _moduleClassName = [_moduleClassName substringToIndex:categoryRange.location]; } - NSArray *argumentNames = nil; - if ([parts[1] hasPrefix:@"__rct_export__"]) { - // New format - NSString *selectorString = [parts[1] substringFromIndex:14]; - _selector = NSSelectorFromString(selectorString); - _JSMethodName = JSMethodName ?: RCTStringUpToFirstArgument(selectorString); - - static NSRegularExpression *regExp; - if (!regExp) { - NSString *unusedPattern = @"(?:(?:__unused|__attribute__\\(\\(unused\\)\\)))"; - NSString *constPattern = @"(?:const)"; - NSString *constUnusedPattern = [NSString stringWithFormat:@"(?:(?:%@|%@)\\s*)", unusedPattern, constPattern]; - NSString *pattern = [NSString stringWithFormat:@"\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\)", constUnusedPattern]; - regExp = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL]; + NSString *selectorString = [parts[1] substringFromIndex:14]; + _selector = NSSelectorFromString(selectorString); + _JSMethodName = JSMethodName ?: ({ + NSString *methodName = selectorString; + NSRange colonRange = [methodName rangeOfString:@":"]; + if (colonRange.length) { + methodName = [methodName substringToIndex:colonRange.location]; } + methodName; + }); - argumentNames = [NSMutableArray array]; - [regExp enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { - NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]]; - [(NSMutableArray *)argumentNames addObject:argumentName]; - }]; - } else { - // Old format - NSString *selectorString = parts[1]; - _selector = NSSelectorFromString(selectorString); - _JSMethodName = JSMethodName ?: RCTStringUpToFirstArgument(selectorString); + static NSRegularExpression *regExp; + if (!regExp) { + NSString *unusedPattern = @"(?:(?:__unused|__attribute__\\(\\(unused\\)\\)))"; + NSString *constPattern = @"(?:const)"; + NSString *constUnusedPattern = [NSString stringWithFormat:@"(?:(?:%@|%@)\\s*)", unusedPattern, constPattern]; + NSString *pattern = [NSString stringWithFormat:@"\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\)", constUnusedPattern]; + regExp = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL]; } + NSMutableArray *argumentNames = [NSMutableArray array]; + [regExp enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]]; + [argumentNames addObject:argumentName]; + }]; + // Extract class and method details - _isClassMethod = [reactMethodName characterAtIndex:0] == '+'; _moduleClass = NSClassFromString(_moduleClassName); if (RCT_DEBUG) { @@ -318,9 +304,7 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) } // Get method signature - _methodSignature = _isClassMethod ? - [_moduleClass methodSignatureForSelector:_selector] : - [_moduleClass instanceMethodSignatureForSelector:_selector]; + _methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector]; // Process arguments NSUInteger numberOfArguments = _methodSignature.numberOfArguments; @@ -363,119 +347,64 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) for (NSUInteger i = 2; i < numberOfArguments; i++) { const char *argumentType = [_methodSignature getArgumentTypeAtIndex:i]; - BOOL useFallback = YES; - if (argumentNames) { - NSString *argumentName = argumentNames[i - 2]; - SEL selector = NSSelectorFromString([argumentName stringByAppendingString:@":"]); - if ([RCTConvert respondsToSelector:selector]) { - useFallback = NO; - switch (argumentType[0]) { - -#define RCT_CONVERT_CASE(_value, _type) \ - case _value: { \ - _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ - RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ - break; \ - } - - RCT_CONVERT_CASE(':', SEL) - RCT_CONVERT_CASE('*', const char *) - RCT_CONVERT_CASE('c', char) - RCT_CONVERT_CASE('C', unsigned char) - RCT_CONVERT_CASE('s', short) - RCT_CONVERT_CASE('S', unsigned short) - RCT_CONVERT_CASE('i', int) - RCT_CONVERT_CASE('I', unsigned int) - RCT_CONVERT_CASE('l', long) - RCT_CONVERT_CASE('L', unsigned long) - RCT_CONVERT_CASE('q', long long) - RCT_CONVERT_CASE('Q', unsigned long long) - RCT_CONVERT_CASE('f', float) - RCT_CONVERT_CASE('d', double) - RCT_CONVERT_CASE('B', BOOL) - RCT_CONVERT_CASE('@', id) - RCT_CONVERT_CASE('^', void *) - - case '{': { - [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { - NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; - void *returnValue = malloc(methodSignature.methodReturnLength); - NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - [_invocation setTarget:[RCTConvert class]]; - [_invocation setSelector:selector]; - [_invocation setArgument:&json atIndex:2]; - [_invocation invoke]; - [_invocation getReturnValue:returnValue]; - - [invocation setArgument:returnValue atIndex:index]; - - free(returnValue); - }]; - break; - } - - default: - defaultCase(argumentType); - } - } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { - addBlockArgument(); - useFallback = NO; - } - } - - if (useFallback) { + NSString *argumentName = argumentNames[i - 2]; + SEL selector = NSSelectorFromString([argumentName stringByAppendingString:@":"]); + if ([RCTConvert respondsToSelector:selector]) { switch (argumentType[0]) { -#define RCT_CASE(_value, _class, _logic) \ - case _value: { \ - RCT_ARG_BLOCK( \ - if (RCT_DEBUG && json && ![json isKindOfClass:[_class class]]) { \ - RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \ - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \ - return; \ - } \ - _logic \ - ) \ - break; \ - } +#define RCT_CONVERT_CASE(_value, _type) \ +case _value: { \ + _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ + RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ + break; \ +} - RCT_CASE(':', NSString, SEL value = NSSelectorFromString(json); ) - RCT_CASE('*', NSString, const char *value = [json UTF8String]; ) + RCT_CONVERT_CASE(':', SEL) + RCT_CONVERT_CASE('*', const char *) + RCT_CONVERT_CASE('c', char) + RCT_CONVERT_CASE('C', unsigned char) + RCT_CONVERT_CASE('s', short) + RCT_CONVERT_CASE('S', unsigned short) + RCT_CONVERT_CASE('i', int) + RCT_CONVERT_CASE('I', unsigned int) + RCT_CONVERT_CASE('l', long) + RCT_CONVERT_CASE('L', unsigned long) + RCT_CONVERT_CASE('q', long long) + RCT_CONVERT_CASE('Q', unsigned long long) + RCT_CONVERT_CASE('f', float) + RCT_CONVERT_CASE('d', double) + RCT_CONVERT_CASE('B', BOOL) + RCT_CONVERT_CASE('@', id) + RCT_CONVERT_CASE('^', void *) -#define RCT_SIMPLE_CASE(_value, _type, _selector) \ - case _value: { \ - RCT_ARG_BLOCK( \ - if (RCT_DEBUG && json && ![json respondsToSelector:@selector(_selector)]) { \ - RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \ - index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \ - return; \ - } \ - _type value = [json _selector]; \ - ) \ - break; \ - } + case '{': { + [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { + NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; + void *returnValue = malloc(methodSignature.methodReturnLength); + NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [_invocation setTarget:[RCTConvert class]]; + [_invocation setSelector:selector]; + [_invocation setArgument:&json atIndex:2]; + [_invocation invoke]; + [_invocation getReturnValue:returnValue]; - RCT_SIMPLE_CASE('c', char, charValue) - RCT_SIMPLE_CASE('C', unsigned char, unsignedCharValue) - RCT_SIMPLE_CASE('s', short, shortValue) - RCT_SIMPLE_CASE('S', unsigned short, unsignedShortValue) - RCT_SIMPLE_CASE('i', int, intValue) - RCT_SIMPLE_CASE('I', unsigned int, unsignedIntValue) - RCT_SIMPLE_CASE('l', long, longValue) - RCT_SIMPLE_CASE('L', unsigned long, unsignedLongValue) - RCT_SIMPLE_CASE('q', long long, longLongValue) - RCT_SIMPLE_CASE('Q', unsigned long long, unsignedLongLongValue) - RCT_SIMPLE_CASE('f', float, floatValue) - RCT_SIMPLE_CASE('d', double, doubleValue) - RCT_SIMPLE_CASE('B', BOOL, boolValue) + [invocation setArgument:returnValue atIndex:index]; - case '{': - RCTLogMustFix(@"Cannot convert JSON to struct %s", argumentType); - break; + free(returnValue); + }]; + break; + } default: defaultCase(argumentType); } + } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { + addBlockArgument(); + } else { + + // Unknown argument type + RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" + " to support this type.", argumentName, [self methodName]); } } @@ -494,7 +423,7 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) // Sanity check RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \ - %@ on a module of class %@", _methodName, [module class]); + %@ on a module of class %@", [self methodName], [module class]); // Safety check if (arguments.count != _argumentBlocks.count) { @@ -520,12 +449,19 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) } // Invoke method - [invocation invokeWithTarget:_isClassMethod ? [module class] : module]; + [invocation invokeWithTarget:module]; +} + +- (NSString *)methodName +{ + return [NSString stringWithFormat:@"-[%@ %@]", _moduleClass, + NSStringFromSelector(_selector)]; } - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName]; + return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@();>", + [self class], self, [self methodName], _JSMethodName]; } @end @@ -562,19 +498,10 @@ static RCTSparseArray *RCTExportedMethodsByModuleID(void) const char **entries = (const char **)(mach_header + addr); // Create method - RCTModuleMethod *moduleMethod; - if (entries[2] == NULL) { - - // Legacy support for RCT_EXPORT() - moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0]) - objCMethodName:@(entries[0]) - JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil]; - } else { - moduleMethod = [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0]) - objCMethodName:strlen(entries[1]) ? @(entries[1]) : nil - JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil]; - } - + RCTModuleMethod *moduleMethod = + [[RCTModuleMethod alloc] initWithReactMethodName:@(entries[0]) + objCMethodName:@(entries[1]) + JSMethodName:strlen(entries[2]) ? @(entries[2]) : nil]; // Cache method NSArray *methods = methodsByModuleClassName[moduleMethod.moduleClassName]; methodsByModuleClassName[moduleMethod.moduleClassName] = @@ -726,7 +653,7 @@ static NSDictionary *RCTLocalModulesConfig() @"methodID": @(methods.count), @"type": @"local" }; - [RCTLocalMethodNames addObject:methodName]; + [RCTLocalMethodNames addObject:methodName]; } // Add module and method lookup @@ -1313,7 +1240,12 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin #pragma mark - Payload Generation -- (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID +- (void)dispatchBlock:(dispatch_block_t)block forModule:(id)module +{ + [self dispatchBlock:block forModuleID:RCTModuleIDsByName[RCTBridgeModuleNameForClass([module class])]]; +} + +- (void)dispatchBlock:(dispatch_block_t)block forModuleID:(NSNumber *)moduleID { RCTAssertJSThread(); @@ -1458,7 +1390,7 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin if ([module respondsToSelector:@selector(batchDidComplete)]) { [self dispatchBlock:^{ [module batchDidComplete]; - } forModule:moduleID]; + } forModuleID:moduleID]; } }]; } @@ -1526,7 +1458,7 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin @"selector": NSStringFromSelector(method.selector), @"args": RCTJSONStringify(params ?: [NSNull null], NULL), }); - } forModule:@(moduleID)]; + } forModuleID:@(moduleID)]; return YES; } @@ -1546,7 +1478,7 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin RCTProfileBeginEvent(); [observer didUpdateFrame:frameUpdate]; RCTProfileEndEvent(name, @"objc_call,fps", nil); - } forModule:RCTModuleIDsByName[RCTBridgeModuleNameForClass([observer class])]]; + } forModule:(id)observer]; } } @@ -1591,35 +1523,39 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin - (void)startProfiling { - RCTAssertMainThread(); + RCTAssertMainThread(); if (![_parentBridge.bundleURL.scheme isEqualToString:@"http"]) { RCTLogError(@"To run the profiler you must be running from the dev server"); return; } - RCTProfileInit(); + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileInit(self); + }]; } - (void)stopProfiling { - RCTAssertMainThread(); + RCTAssertMainThread(); - NSString *log = RCTProfileEnd(); - NSURL *bundleURL = _parentBridge.bundleURL; - NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; - NSURL *URL = [NSURL URLWithString:URLString]; - NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; - URLRequest.HTTPMethod = @"POST"; - [URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - NSURLSessionTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest - fromData:[log dataUsingEncoding:NSUTF8StringEncoding] - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error) { - RCTLogError(@"%@", error.localizedDescription); - } - }]; - [task resume]; + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + NSString *log = RCTProfileEnd(self); + NSURL *bundleURL = _parentBridge.bundleURL; + NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; + NSURL *URL = [NSURL URLWithString:URLString]; + NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; + URLRequest.HTTPMethod = @"POST"; + [URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + NSURLSessionTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest + fromData:[log dataUsingEncoding:NSUTF8StringEncoding] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + RCTLogError(@"%@", error.localizedDescription); + } + }]; + [task resume]; + }]; } @end diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 34b861ff3..f3a9a5a3e 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -145,15 +145,6 @@ extern const dispatch_queue_t RCTJSThread; static const char *__rct_export_entry__[] = { __func__, #method, #js_name }; \ } -/** - * Deprecated, do not use. - */ -#define RCT_EXPORT(js_name) \ - _Pragma("message(\"RCT_EXPORT is deprecated. Use RCT_EXPORT_METHOD instead.\")") \ - __attribute__((used, section("__DATA,RCTExport"))) \ - __attribute__((__aligned__(1))) \ - static const char *__rct_export_entry__[] = { __func__, #js_name, NULL } - /** * The queue that will be used to call all exported methods. If omitted, this * will call on the default background queue, which is avoids blocking the main diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index ee43c1159..145e88b21 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -23,6 +23,8 @@ */ @interface RCTConvert : NSObject ++ (id)id:(id)json; + + (BOOL)BOOL:(id)json; + (double)double:(id)json; + (float)float:(id)json; @@ -52,7 +54,6 @@ + (NSWritingDirection)NSWritingDirection:(id)json; + (UITextAutocapitalizationType)UITextAutocapitalizationType:(id)json; + (UITextFieldViewMode)UITextFieldViewMode:(id)json; -+ (UIScrollViewKeyboardDismissMode)UIScrollViewKeyboardDismissMode:(id)json; + (UIKeyboardType)UIKeyboardType:(id)json; + (UIReturnKeyType)UIReturnKeyType:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 3c9143c17..3bdf59753 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -21,6 +21,8 @@ void RCTLogConvertError(id json, const char *type) json, [json classForCoder], type); } +RCT_CONVERTER(id, id, self) + RCT_CONVERTER(BOOL, BOOL, boolValue) RCT_NUMBER_CONVERTER(double, doubleValue) RCT_NUMBER_CONVERTER(float, floatValue) @@ -219,12 +221,6 @@ RCT_ENUM_CONVERTER(UITextFieldViewMode, (@{ @"always": @(UITextFieldViewModeAlways), }), UITextFieldViewModeNever, integerValue) -RCT_ENUM_CONVERTER(UIScrollViewKeyboardDismissMode, (@{ - @"none": @(UIScrollViewKeyboardDismissModeNone), - @"on-drag": @(UIScrollViewKeyboardDismissModeOnDrag), - @"interactive": @(UIScrollViewKeyboardDismissModeInteractive), -}), UIScrollViewKeyboardDismissModeNone, integerValue) - RCT_ENUM_CONVERTER(UIKeyboardType, (@{ @"default": @(UIKeyboardTypeDefault), @"ascii-capable": @(UIKeyboardTypeASCIICapable), @@ -237,6 +233,8 @@ RCT_ENUM_CONVERTER(UIKeyboardType, (@{ @"decimal-pad": @(UIKeyboardTypeDecimalPad), @"twitter": @(UIKeyboardTypeTwitter), @"web-search": @(UIKeyboardTypeWebSearch), + // Added for Android compatibility + @"numeric": @(UIKeyboardTypeDecimalPad), }), UIKeyboardTypeDefault, integerValue) RCT_ENUM_CONVERTER(UIReturnKeyType, (@{ @@ -267,7 +265,11 @@ RCT_ENUM_CONVERTER(UIViewContentMode, (@{ @"top-right": @(UIViewContentModeTopRight), @"bottom-left": @(UIViewContentModeBottomLeft), @"bottom-right": @(UIViewContentModeBottomRight), -}), UIViewContentModeScaleToFill, integerValue) + // Cross-platform values + @"cover": @(UIViewContentModeScaleAspectFill), + @"contain": @(UIViewContentModeScaleAspectFit), + @"stretch": @(UIViewContentModeScaleToFill), +}), UIViewContentModeScaleAspectFill, integerValue) RCT_ENUM_CONVERTER(UIBarStyle, (@{ @"default": @(UIBarStyleDefault), diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index b260fca4a..ed1ff90b8 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -51,6 +51,12 @@ */ - (void)reload; +/** + * Add custom item to the development menu. The handler will be called + * when user selects the item. + */ +- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler; + @end /** diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 2416e974c..8a5f23f9d 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -43,6 +43,28 @@ static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu"; @end +@interface RCTDevMenuItem : NSObject + +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) dispatch_block_t handler; + +- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler; + +@end + +@implementation RCTDevMenuItem + +- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler +{ + if (self = [super init]) { + self.title = title; + self.handler = handler; + } + return self; +} + +@end + @interface RCTDevMenu () @property (nonatomic, strong) Class executorClass; @@ -57,6 +79,8 @@ static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu"; NSURLSessionDataTask *_updateTask; NSURL *_liveReloadURL; BOOL _jsLoaded; + NSArray *_presentedItems; + NSMutableArray *_extraMenuItems; } @synthesize bridge = _bridge; @@ -94,6 +118,7 @@ RCT_EXPORT_MODULE() _defaults = [NSUserDefaults standardUserDefaults]; _settings = [[NSMutableDictionary alloc] init]; + _extraMenuItems = [NSMutableArray array]; // Delay setup until after Bridge init [self settingsDidChange]; @@ -110,6 +135,13 @@ RCT_EXPORT_MODULE() [weakSelf toggle]; }]; + // Toggle element inspector + [commands registerKeyCommandWithInput:@"i" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; + }]; + // Reload in normal mode [commands registerKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand @@ -225,32 +257,82 @@ RCT_EXPORT_MODULE() } } +- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler +{ + [_extraMenuItems addObject:[[RCTDevMenuItem alloc] initWithTitle:title handler:handler]]; +} + +- (NSArray *)menuItems +{ + NSMutableArray *items = [NSMutableArray array]; + + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Reload" handler:^{ + [self reload]; + }]]; + + Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); + if (!chromeExecutorClass) { + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Chrome Debugger Unavailable" handler:^{ + [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" + message:@"You need to include the RCTWebSocket library to enable Chrome debugging" + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil] show]; + }]]; + } else { + BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; + NSString *debugTitleChrome = isDebuggingInChrome ? @"Disable Chrome Debugging" : @"Debug in Chrome"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleChrome handler:^{ + self.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; + }]]; + } + + Class safariExecutorClass = NSClassFromString(@"RCTWebViewExecutor"); + BOOL isDebuggingInSafari = _executorClass && _executorClass == safariExecutorClass; + NSString *debugTitleSafari = isDebuggingInSafari ? @"Disable Safari Debugging" : @"Debug in Safari"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleSafari handler:^{ + self.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass; + }]]; + + NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:fpsMonitor handler:^{ + self.showFPS = !_showFPS; + }]]; + + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Inspect Element" handler:^{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; + }]]; + + if (_liveReloadURL) { + NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:liveReloadTitle handler:^{ + self.liveReloadEnabled = !_liveReloadEnabled; + }]]; + + NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:profilingTitle handler:^{ + self.profilingEnabled = !_profilingEnabled; + }]]; + } + + [items addObjectsFromArray:_extraMenuItems]; + + return items; +} + RCT_EXPORT_METHOD(show) { if (_actionSheet || !_bridge) { return; } - NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome"; - NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari"; - NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor"; + UIActionSheet *actionSheet = [[UIActionSheet alloc] init]; + actionSheet.title = @"React Native: Development"; + actionSheet.delegate = self; - UIActionSheet *actionSheet = - [[UIActionSheet alloc] initWithTitle:@"React Native: Development" - delegate:self - cancelButtonTitle:nil - destructiveButtonTitle:nil - otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil]; - - [actionSheet addButtonWithTitle:@"Inspect Element"]; - - if (_liveReloadURL) { - - NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; - NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling"; - - [actionSheet addButtonWithTitle:liveReloadTitle]; - [actionSheet addButtonWithTitle:profilingTitle]; + NSArray *items = [self menuItems]; + for (RCTDevMenuItem *item in items) { + [actionSheet addButtonWithTitle:item.title]; } [actionSheet addButtonWithTitle:@"Cancel"]; @@ -259,13 +341,7 @@ RCT_EXPORT_METHOD(show) actionSheet.actionSheetStyle = UIBarStyleBlack; [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; _actionSheet = actionSheet; -} - -RCT_EXPORT_METHOD(reload) -{ - _jsLoaded = NO; - _liveReloadURL = nil; - [_bridge reload]; + _presentedItems = items; } - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex @@ -275,48 +351,16 @@ RCT_EXPORT_METHOD(reload) return; } - switch (buttonIndex) { - case 0: { - [self reload]; - break; - } - case 1: { - Class cls = NSClassFromString(@"RCTWebSocketExecutor"); - if (!cls) { - [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" - message:@"You need to include the RCTWebSocket library to enable Chrome debugging" - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil] show]; - return; - } - self.executorClass = (_executorClass == cls) ? Nil : cls; - break; - } - case 2: { - Class cls = NSClassFromString(@"RCTWebViewExecutor"); - self.executorClass = (_executorClass == cls) ? Nil : cls; - break; - } - case 3: { - self.showFPS = !_showFPS; - break; - } - case 4: { - [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; - break; - } - case 5: { - self.liveReloadEnabled = !_liveReloadEnabled; - break; - } - case 6: { - self.profilingEnabled = !_profilingEnabled; - break; - } - default: - break; - } + RCTDevMenuItem *item = _presentedItems[buttonIndex]; + item.handler(); + return; +} + +RCT_EXPORT_METHOD(reload) +{ + _jsLoaded = NO; + _liveReloadURL = nil; + [_bridge reload]; } - (void)setShakeToShow:(BOOL)shakeToShow @@ -438,6 +482,7 @@ RCT_EXPORT_METHOD(reload) - (void)show {} - (void)reload {} +- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {} @end diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h index f722b0a02..2718871d2 100644 --- a/React/Base/RCTProfile.h +++ b/React/Base/RCTProfile.h @@ -25,6 +25,8 @@ NSString *const RCTProfileDidEndProfiling; #if RCT_DEV +@class RCTBridge; + #define RCTProfileBeginFlowEvent() \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wshadow\"") \ @@ -45,14 +47,14 @@ RCT_EXTERN BOOL RCTProfileIsProfiling(void); /** * Start collecting profiling information */ -RCT_EXTERN void RCTProfileInit(void); +RCT_EXTERN void RCTProfileInit(RCTBridge *); /** * Stop profiling and return a JSON string of the collected data - The data * returned is compliant with google's trace event format - the format used * as input to trace-viewer */ -RCT_EXTERN NSString *RCTProfileEnd(void); +RCT_EXTERN NSString *RCTProfileEnd(RCTBridge *); /** * Collects the initial event information for the event and returns a reference ID diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index 09989d1cb..1269d7594 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -10,10 +10,13 @@ #import "RCTProfile.h" #import +#import +#import #import #import "RCTAssert.h" +#import "RCTBridge.h" #import "RCTDefines.h" #import "RCTUtils.h" @@ -32,6 +35,7 @@ NSDictionary *RCTProfileGetMemoryUsage(void); NSString const *RCTProfileTraceEvents = @"traceEvents"; NSString const *RCTProfileSamples = @"samples"; +NSString *const RCTProfilePrefix = @"rct_profile_"; #pragma mark - Variables @@ -92,6 +96,111 @@ NSDictionary *RCTProfileGetMemoryUsage(void) } } +#pragma mark - Module hooks + +@interface RCTBridge (Private) + +- (void)dispatchBlock:(dispatch_block_t)block forModule:(id)module; + +@end + +static const char *RCTProfileProxyClassName(Class); +static const char *RCTProfileProxyClassName(Class class) +{ + return [RCTProfilePrefix stringByAppendingString:NSStringFromClass(class)].UTF8String; +} + +static SEL RCTProfileProxySelector(SEL); +static SEL RCTProfileProxySelector(SEL selector) +{ + NSString *selectorName = NSStringFromSelector(selector); + return NSSelectorFromString([RCTProfilePrefix stringByAppendingString:selectorName]); +} + +static void RCTProfileForwardInvocation(NSObject *, SEL, NSInvocation *); +static void RCTProfileForwardInvocation(NSObject *self, SEL cmd, NSInvocation *invocation) +{ + NSString *name = [NSString stringWithFormat:@"-[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(invocation.selector)]; + SEL newSel = RCTProfileProxySelector(invocation.selector); + + if ([object_getClass(self) instancesRespondToSelector:newSel]) { + invocation.selector = newSel; + RCTProfileBeginEvent(); + [invocation invoke]; + RCTProfileEndEvent(name, @"objc_call,modules,auto", nil); + } else { + // Use original selector to don't change error message + [self doesNotRecognizeSelector:invocation.selector]; + } +} + +static IMP RCTProfileMsgForward(NSObject *, SEL); +static IMP RCTProfileMsgForward(NSObject *self, SEL selector) +{ + IMP imp = _objc_msgForward; +#if !defined(__arm64__) + NSMethodSignature *signature = [self methodSignatureForSelector:selector]; + if (signature.methodReturnType[0] == _C_STRUCT_B && signature.methodReturnLength > 8) { + imp = _objc_msgForward_stret; + } +#endif + return imp; +} + +static void RCTProfileHookModules(RCTBridge *); +static void RCTProfileHookModules(RCTBridge *bridge) +{ + [bridge.modules enumerateKeysAndObjectsUsingBlock:^(NSString *className, id module, BOOL *stop) { + [bridge dispatchBlock:^{ + Class moduleClass = object_getClass(module); + Class proxyClass = objc_allocateClassPair(moduleClass, RCTProfileProxyClassName(moduleClass), 0); + + unsigned int methodCount; + Method *methods = class_copyMethodList(moduleClass, &methodCount); + for (NSUInteger i = 0; i < methodCount; i++) { + Method method = methods[i]; + SEL selector = method_getName(method); + if ([NSStringFromSelector(selector) hasPrefix:@"rct"] || [NSObject instancesRespondToSelector:selector]) { + continue; + } + IMP originalIMP = method_getImplementation(method); + const char *returnType = method_getTypeEncoding(method); + class_addMethod(proxyClass, selector, RCTProfileMsgForward(module, selector), returnType); + class_addMethod(proxyClass, RCTProfileProxySelector(selector), originalIMP, returnType); + } + free(methods); + + for (Class cls in @[proxyClass, object_getClass(proxyClass)]) { + Method oldImp = class_getInstanceMethod(cls, @selector(class)); + class_replaceMethod(cls, @selector(class), imp_implementationWithBlock(^{ return moduleClass; }), method_getTypeEncoding(oldImp)); + } + + IMP originalFwd = class_replaceMethod(moduleClass, @selector(forwardInvocation:), (IMP)RCTProfileForwardInvocation, "v@:@"); + if (originalFwd != NULL) { + class_addMethod(proxyClass, RCTProfileProxySelector(@selector(forwardInvocation:)), originalFwd, "v@:@"); + } + + objc_registerClassPair(proxyClass); + object_setClass(module, proxyClass); + } forModule:module]; + }]; +} + +void RCTProfileUnhookModules(RCTBridge *); +void RCTProfileUnhookModules(RCTBridge *bridge) +{ + [bridge.modules enumerateKeysAndObjectsUsingBlock:^(NSString *className, id module, BOOL *stop) { + [bridge dispatchBlock:^{ + Class proxyClass = object_getClass(module); + if (module.class != proxyClass) { + object_setClass(module, module.class); + objc_disposeClassPair(proxyClass); + } + } forModule:module]; + }]; +} + + #pragma mark - Public Functions BOOL RCTProfileIsProfiling(void) @@ -102,8 +211,10 @@ BOOL RCTProfileIsProfiling(void) return profiling; } -void RCTProfileInit(void) +void RCTProfileInit(RCTBridge *bridge) { + RCTProfileHookModules(bridge); + static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _RCTProfileLock = [[NSLock alloc] init]; @@ -121,7 +232,7 @@ void RCTProfileInit(void) object:nil]; } -NSString *RCTProfileEnd(void) +NSString *RCTProfileEnd(RCTBridge *bridge) { [[NSNotificationCenter defaultCenter] postNotificationName:RCTProfileDidEndProfiling object:nil]; @@ -132,6 +243,9 @@ NSString *RCTProfileEnd(void) RCTProfileInfo = nil; RCTProfileOngoingEvents = nil; ); + + RCTProfileUnhookModules(bridge); + return log; } diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index 2d35dcd6f..6330454ab 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -85,11 +85,6 @@ selector:@selector(dismiss) name:RCTReloadNotification object:nil]; - - [notificationCenter addObserver:self - selector:@selector(dismiss) - name:RCTJavaScriptDidLoadNotification - object:nil]; } return self; } diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index f95f134c3..0a2ca61a7 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -36,8 +36,6 @@ BOOL _recordingInteractionTiming; CFTimeInterval _mostRecentEnqueueJS; - NSMutableArray *_pendingTouches; - NSMutableArray *_bridgeInteractionTiming; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -52,9 +50,6 @@ _reactTouches = [[NSMutableArray alloc] init]; _touchViews = [[NSMutableArray alloc] init]; - _pendingTouches = [[NSMutableArray alloc] init]; - _bridgeInteractionTiming = [[NSMutableArray alloc] init]; - // `cancelsTouchesInView` is needed in order to be used as a top level // event delegated recognizer. Otherwise, lower-level components not built // using RCT, will fail to recognize gestures. @@ -94,11 +89,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { return; } - // Get new, unique touch id + // Get new, unique touch identifier for the react touch const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices - NSInteger touchID = ([_reactTouches.lastObject[@"target"] integerValue] + 1) % RCTMaxTouches; + NSInteger touchID = ([_reactTouches.lastObject[@"identifier"] integerValue] + 1) % RCTMaxTouches; for (NSDictionary *reactTouch in _reactTouches) { - NSInteger usedID = [reactTouch[@"target"] integerValue]; + NSInteger usedID = [reactTouch[@"identifier"] integerValue]; if (usedID == touchID) { // ID has already been used, try next value touchID ++; diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 5c34d0e0a..641500b38 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -18,6 +18,8 @@ // Utility functions for JSON object <-> string serialization/deserialization RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error); RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error); +RCT_EXTERN id RCTJSONParseMutable(NSString *jsonString, NSError **error); +RCT_EXTERN id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options); // Strip non JSON-safe values from an object graph RCT_EXTERN id RCTJSONClean(id object); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 712e9724e..613b13163 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -24,7 +24,7 @@ NSString *RCTJSONStringify(id jsonObject, NSError **error) return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil; } -id RCTJSONParse(NSString *jsonString, NSError **error) +id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options) { if (!jsonString) { return nil; @@ -39,7 +39,15 @@ id RCTJSONParse(NSString *jsonString, NSError **error) return nil; } } - return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error]; + return [NSJSONSerialization JSONObjectWithData:jsonData options:options error:error]; +} + +id RCTJSONParse(NSString *jsonString, NSError **error) { + return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingAllowFragments); +} + +id RCTJSONParseMutable(NSString *jsonString, NSError **error) { + return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingMutableContainers|NSJSONReadingMutableLeaves); } id RCTJSONClean(id object) diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index 2c01161d4..76f7fa885 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -61,6 +61,34 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } +// Only merges objects - all other types are just clobbered (including arrays) +static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +{ + for (NSString *key in source) { + id sourceValue = source[key]; + if ([sourceValue isKindOfClass:[NSDictionary class]]) { + id destinationValue = destination[key]; + NSMutableDictionary *nestedDestination; + if ([destinationValue classForCoder] == [NSMutableDictionary class]) { + nestedDestination = destinationValue; + } else { + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + // Ideally we wouldn't eagerly copy here... + nestedDestination = [destinationValue mutableCopy]; + } else { + destination[key] = [sourceValue copy]; + } + } + if (nestedDestination) { + RCTMergeRecursive(nestedDestination, sourceValue); + destination[key] = nestedDestination; + } + } else { + destination[key] = sourceValue; + } + } +} + #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -135,13 +163,19 @@ RCT_EXPORT_MODULE() if (errorOut) { return errorOut; } + id value = [self _getValueForKey:key errorOut:&errorOut]; + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut +{ id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. if (value == [NSNull null]) { NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, &errorOut); + value = RCTReadFile(filePath, key, errorOut); } - [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. - return errorOut; + return value; } - (id)_writeEntry:(NSArray *)entry @@ -198,7 +232,6 @@ RCT_EXPORT_METHOD(multiGet:(NSArray *)keys id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; callback(@[errors ?: [NSNull null], result]); } @@ -221,6 +254,38 @@ RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs } } +RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) +{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (__strong NSArray *entry in kvPairs) { + id keyError; + NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; + if (keyError) { + RCTAppendError(keyError, &errors); + } else { + if (value) { + NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; + RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); + entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + } + if (!keyError) { + keyError = [self _writeEntry:entry]; + } + RCTAppendError(keyError, &errors); + } + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } +} + RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { diff --git a/React/Modules/RCTStatusBarManager.h b/React/Modules/RCTStatusBarManager.h index 40feee5c0..aee9b8642 100644 --- a/React/Modules/RCTStatusBarManager.h +++ b/React/Modules/RCTStatusBarManager.h @@ -10,6 +10,14 @@ #import #import "RCTBridgeModule.h" +#import "RCTConvert.h" + +@interface RCTConvert (UIStatusBar) + ++ (UIStatusBarStyle)UIStatusBarStyle:(id)json; ++ (UIStatusBarAnimation)UIStatusBarAnimation:(id)json; + +@end @interface RCTStatusBarManager : NSObject diff --git a/React/Modules/RCTStatusBarManager.m b/React/Modules/RCTStatusBarManager.m index 04bb39038..cb9ddfe69 100644 --- a/React/Modules/RCTStatusBarManager.m +++ b/React/Modules/RCTStatusBarManager.m @@ -11,6 +11,21 @@ #import "RCTLog.h" +@implementation RCTConvert (UIStatusBar) + +RCT_ENUM_CONVERTER(UIStatusBarStyle, (@{ + @"default": @(UIStatusBarStyleDefault), + @"light-content": @(UIStatusBarStyleLightContent), +}), UIStatusBarStyleDefault, integerValue); + +RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ + @"none": @(UIStatusBarAnimationNone), + @"fade": @(UIStatusBarAnimationFade), + @"slide": @(UIStatusBarAnimationSlide), +}), UIStatusBarAnimationNone, integerValue); + +@end + @implementation RCTStatusBarManager static BOOL RCTViewControllerBasedStatusBarAppearance() @@ -18,7 +33,8 @@ static BOOL RCTViewControllerBasedStatusBarAppearance() static BOOL value; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - value = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"] ?: @YES boolValue]; + value = [[[NSBundle mainBundle] objectForInfoDictionaryKey: + @"UIViewControllerBasedStatusBarAppearance"] ?: @YES boolValue]; }); return value; @@ -55,19 +71,4 @@ RCT_EXPORT_METHOD(setHidden:(BOOL)hidden } } -- (NSDictionary *)constantsToExport -{ - return @{ - @"Style": @{ - @"default": @(UIStatusBarStyleDefault), - @"lightContent": @(UIStatusBarStyleLightContent), - }, - @"Animation": @{ - @"none": @(UIStatusBarAnimationNone), - @"fade": @(UIStatusBarAnimationFade), - @"slide": @(UIStatusBarAnimationSlide), - }, - }; -} - @end diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index cc580e903..570bdfef7 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1397,11 +1397,6 @@ RCT_EXPORT_METHOD(clearJSResponder) NSMutableDictionary *allJSConstants = [@{ @"customBubblingEventTypes": [self customBubblingEventTypes], @"customDirectEventTypes": [self customDirectEventTypes], - @"NSTextAlignment": @{ - @"Left": @(NSTextAlignmentLeft), - @"Center": @(NSTextAlignmentCenter), - @"Right": @(NSTextAlignmentRight), - }, @"Dimensions": @{ @"window": @{ @"width": @(RCTScreenSize().width), @@ -1413,73 +1408,6 @@ RCT_EXPORT_METHOD(clearJSResponder) @"height": @(RCTScreenSize().height), }, }, - @"StyleConstants": @{ - @"PointerEventsValues": @{ - @"none": @(RCTPointerEventsNone), - @"box-none": @(RCTPointerEventsBoxNone), - @"box-only": @(RCTPointerEventsBoxOnly), - @"auto": @(RCTPointerEventsUnspecified), - }, - }, - @"UIText": @{ - @"AutocapitalizationType": @{ - @"characters": @(UITextAutocapitalizationTypeAllCharacters), - @"sentences": @(UITextAutocapitalizationTypeSentences), - @"words": @(UITextAutocapitalizationTypeWords), - @"none": @(UITextAutocapitalizationTypeNone), - }, - }, - @"UITextField": @{ - @"clearButtonMode": @{ - @"never": @(UITextFieldViewModeNever), - @"while-editing": @(UITextFieldViewModeWhileEditing), - @"unless-editing": @(UITextFieldViewModeUnlessEditing), - @"always": @(UITextFieldViewModeAlways), - }, - }, - @"UIKeyboardType": @{ - @"default": @(UIKeyboardTypeDefault), - @"ascii-capable": @(UIKeyboardTypeASCIICapable), - @"numbers-and-punctuation": @(UIKeyboardTypeNumbersAndPunctuation), - @"url": @(UIKeyboardTypeURL), - @"number-pad": @(UIKeyboardTypeNumberPad), - @"phone-pad": @(UIKeyboardTypePhonePad), - @"name-phone-pad": @(UIKeyboardTypeNamePhonePad), - @"decimal-pad": @(UIKeyboardTypeDecimalPad), - @"email-address": @(UIKeyboardTypeEmailAddress), - @"twitter": @(UIKeyboardTypeTwitter), - @"web-search": @(UIKeyboardTypeWebSearch), - }, - @"UIReturnKeyType": @{ - @"default": @(UIReturnKeyDefault), - @"go": @(UIReturnKeyGo), - @"google": @(UIReturnKeyGoogle), - @"join": @(UIReturnKeyJoin), - @"next": @(UIReturnKeyNext), - @"route": @(UIReturnKeyRoute), - @"search": @(UIReturnKeySearch), - @"send": @(UIReturnKeySend), - @"yahoo": @(UIReturnKeyYahoo), - @"done": @(UIReturnKeyDone), - @"emergency-call": @(UIReturnKeyEmergencyCall), - }, - @"UIView": @{ - @"ContentMode": @{ - @"ScaleToFill": @(UIViewContentModeScaleToFill), - @"ScaleAspectFit": @(UIViewContentModeScaleAspectFit), - @"ScaleAspectFill": @(UIViewContentModeScaleAspectFill), - @"Redraw": @(UIViewContentModeRedraw), - @"Center": @(UIViewContentModeCenter), - @"Top": @(UIViewContentModeTop), - @"Bottom": @(UIViewContentModeBottom), - @"Left": @(UIViewContentModeLeft), - @"Right": @(UIViewContentModeRight), - @"TopLeft": @(UIViewContentModeTopLeft), - @"TopRight": @(UIViewContentModeTopRight), - @"BottomLeft": @(UIViewContentModeBottomLeft), - @"BottomRight": @(UIViewContentModeBottomRight), - }, - }, } mutableCopy]; [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTViewManager *manager, BOOL *stop) { diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index c7309989b..3c0cfe722 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; }; 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */; }; 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */; }; + 13513F3C1B1F43F400FCE529 /* RCTProgressViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13513F3B1B1F43F400FCE529 /* RCTProgressViewManager.m */; }; 13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */; }; 1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7091AB030C200659ED6 /* RCTAppState.m */; }; 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E01AA5CF210034F82E /* RCTTabBar.m */; }; @@ -104,6 +105,8 @@ 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutor.m; sourceTree = ""; }; 134FCB3B1A6E7F0800051CC8 /* RCTWebViewExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewExecutor.h; sourceTree = ""; }; 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebViewExecutor.m; sourceTree = ""; }; + 13513F3A1B1F43F400FCE529 /* RCTProgressViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTProgressViewManager.h; sourceTree = ""; }; + 13513F3B1B1F43F400FCE529 /* RCTProgressViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTProgressViewManager.m; sourceTree = ""; }; 13723B4E1A82FD3C00F88898 /* RCTStatusBarManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStatusBarManager.h; sourceTree = ""; }; 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStatusBarManager.m; sourceTree = ""; }; 1372B7081AB030C200659ED6 /* RCTAppState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAppState.h; sourceTree = ""; }; @@ -307,6 +310,8 @@ 58114A141AAE854800E7D092 /* RCTPickerManager.h */, 58114A151AAE854800E7D092 /* RCTPickerManager.m */, 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */, + 13513F3A1B1F43F400FCE529 /* RCTProgressViewManager.h */, + 13513F3B1B1F43F400FCE529 /* RCTProgressViewManager.m */, 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */, 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */, 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */, @@ -524,6 +529,7 @@ 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */, + 13513F3C1B1F43F400FCE529 /* RCTProgressViewManager.m in Sources */, 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */, 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */, diff --git a/React/Views/RCTDatePickerManager.h b/React/Views/RCTDatePickerManager.h index eb424085d..73d88b6dd 100644 --- a/React/Views/RCTDatePickerManager.h +++ b/React/Views/RCTDatePickerManager.h @@ -8,6 +8,13 @@ */ #import "RCTViewManager.h" +#import "RCTConvert.h" + +@interface RCTConvert(UIDatePicker) + ++ (UIDatePickerMode)UIDatePickerMode:(id)json; + +@end @interface RCTDatePickerManager : RCTViewManager diff --git a/React/Views/RCTDatePickerManager.m b/React/Views/RCTDatePickerManager.m index 36397d6e5..d8bbde703 100644 --- a/React/Views/RCTDatePickerManager.m +++ b/React/Views/RCTDatePickerManager.m @@ -10,7 +10,6 @@ #import "RCTDatePickerManager.h" #import "RCTBridge.h" -#import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "UIView+React.h" @@ -20,7 +19,7 @@ RCT_ENUM_CONVERTER(UIDatePickerMode, (@{ @"time": @(UIDatePickerModeTime), @"date": @(UIDatePickerModeDate), @"datetime": @(UIDatePickerModeDateAndTime), - //@"countdown": @(UIDatePickerModeCountDownTimer) // not supported yet + @"countdown": @(UIDatePickerModeCountDownTimer), // not supported yet }), UIDatePickerModeTime, integerValue) @end @@ -31,9 +30,12 @@ RCT_EXPORT_MODULE() - (UIView *)view { + // TODO: we crash here if the RCTDatePickerManager is released + // while the UIDatePicker is still sending onChange events. To + // fix this we should maybe subclass UIDatePicker and make it + // be its own event target. UIDatePicker *picker = [[UIDatePicker alloc] init]; - [picker addTarget:self - action:@selector(onChange:) + [picker addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged]; return picker; } @@ -56,17 +58,10 @@ RCT_REMAP_VIEW_PROPERTY(timeZoneOffsetInMinutes, timeZone, NSTimeZone) - (NSDictionary *)constantsToExport { - UIDatePicker *dp = [[UIDatePicker alloc] init]; - [dp layoutIfNeeded]; - + UIDatePicker *view = [[UIDatePicker alloc] init]; return @{ - @"ComponentHeight": @(CGRectGetHeight(dp.frame)), - @"ComponentWidth": @(CGRectGetWidth(dp.frame)), - @"DatePickerModes": @{ - @"time": @(UIDatePickerModeTime), - @"date": @(UIDatePickerModeDate), - @"datetime": @(UIDatePickerModeDateAndTime), - } + @"ComponentHeight": @(view.intrinsicContentSize.height), + @"ComponentWidth": @(view.intrinsicContentSize.width), }; } diff --git a/React/Views/RCTPickerManager.m b/React/Views/RCTPickerManager.m index 3bbc60b94..de6c1f916 100644 --- a/React/Views/RCTPickerManager.m +++ b/React/Views/RCTPickerManager.m @@ -27,10 +27,10 @@ RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) - (NSDictionary *)constantsToExport { - RCTPicker *pv = [[RCTPicker alloc] init]; + RCTPicker *view = [[RCTPicker alloc] init]; return @{ - @"ComponentHeight": @(CGRectGetHeight(pv.frame)), - @"ComponentWidth": @(CGRectGetWidth(pv.frame)) + @"ComponentHeight": @(view.intrinsicContentSize.height), + @"ComponentWidth": @(view.intrinsicContentSize.width) }; } diff --git a/React/Views/RCTProgressViewManager.h b/React/Views/RCTProgressViewManager.h new file mode 100644 index 000000000..ae8a6a388 --- /dev/null +++ b/React/Views/RCTProgressViewManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewManager.h" + +@interface RCTProgressViewManager : RCTViewManager + +@end diff --git a/React/Views/RCTProgressViewManager.m b/React/Views/RCTProgressViewManager.m new file mode 100644 index 000000000..deb6285a6 --- /dev/null +++ b/React/Views/RCTProgressViewManager.m @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTProgressViewManager.h" + +#import "RCTConvert.h" + +@implementation RCTConvert (RCTProgressViewManager) + +RCT_ENUM_CONVERTER(UIProgressViewStyle, (@{ + @"default": @(UIProgressViewStyleDefault), + @"bar": @(UIProgressViewStyleBar), +}), UIProgressViewStyleDefault, integerValue) + +@end + +@implementation RCTProgressViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[UIProgressView alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(progressViewStyle, UIProgressViewStyle) +RCT_EXPORT_VIEW_PROPERTY(progress, float) +RCT_EXPORT_VIEW_PROPERTY(progressTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(trackTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(progressImage, UIImage) +RCT_EXPORT_VIEW_PROPERTY(trackImage, UIImage) + +- (NSDictionary *)constantsToExport +{ + UIProgressView *view = [[UIProgressView alloc] init]; + return @{ + @"ComponentHeight": @(view.intrinsicContentSize.height), + }; +} + +@end diff --git a/React/Views/RCTScrollViewManager.h b/React/Views/RCTScrollViewManager.h index 9fec3422d..83b3126e8 100644 --- a/React/Views/RCTScrollViewManager.h +++ b/React/Views/RCTScrollViewManager.h @@ -8,6 +8,13 @@ */ #import "RCTViewManager.h" +#import "RCTConvert.h" + +@interface RCTConvert (UIScrollView) + ++ (UIScrollViewKeyboardDismissMode)UIScrollViewKeyboardDismissMode:(id)json; + +@end @interface RCTScrollViewManager : RCTViewManager diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 8441de74d..15803df1e 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -10,11 +10,22 @@ #import "RCTScrollViewManager.h" #import "RCTBridge.h" -#import "RCTConvert.h" #import "RCTScrollView.h" #import "RCTSparseArray.h" #import "RCTUIManager.h" +@implementation RCTConvert (UIScrollView) + +RCT_ENUM_CONVERTER(UIScrollViewKeyboardDismissMode, (@{ + @"none": @(UIScrollViewKeyboardDismissModeNone), + @"on-drag": @(UIScrollViewKeyboardDismissModeOnDrag), + @"interactive": @(UIScrollViewKeyboardDismissModeInteractive), + // Backwards compatibility + @"onDrag": @(UIScrollViewKeyboardDismissModeOnDrag), +}), UIScrollViewKeyboardDismissModeNone, integerValue) + +@end + @implementation RCTScrollViewManager RCT_EXPORT_MODULE() @@ -53,14 +64,10 @@ RCT_DEPRECATED_VIEW_PROPERTY(throttleScrollCallbackMS, scrollEventThrottle) - (NSDictionary *)constantsToExport { return @{ + // TODO: unused - remove these? @"DecelerationRate": @{ - @"Normal": @(UIScrollViewDecelerationRateNormal), - @"Fast": @(UIScrollViewDecelerationRateFast), - }, - @"KeyboardDismissMode": @{ - @"None": @(UIScrollViewKeyboardDismissModeNone), - @"Interactive": @(UIScrollViewKeyboardDismissModeInteractive), - @"OnDrag": @(UIScrollViewKeyboardDismissModeOnDrag), + @"normal": @(UIScrollViewDecelerationRateNormal), + @"fast": @(UIScrollViewDecelerationRateFast), }, }; } diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m index ff401a719..7b867bd0d 100644 --- a/React/Views/RCTTextFieldManager.m +++ b/React/Views/RCTTextFieldManager.m @@ -25,7 +25,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) -RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) +RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(text, NSString) @@ -36,6 +36,7 @@ RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType) RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, BOOL) RCT_EXPORT_VIEW_PROPERTY(secureTextEntry, BOOL) +RCT_REMAP_VIEW_PROPERTY(password, secureTextEntry, BOOL) // backwards compatibility RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) RCT_REMAP_VIEW_PROPERTY(autoCapitalize, autocapitalizationType, UITextAutocapitalizationType) RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextField) diff --git a/packager/README.md b/packager/README.md index 8f9f649bf..c3fae4806 100644 --- a/packager/README.md +++ b/packager/README.md @@ -62,7 +62,7 @@ if the option is boolean `1/0` or `true/false` is accepted. Here are the current options the packager accepts: * `dev` boolean, defaults to true: sets a global `__DEV__` variable - which will effect how the React Nativeg core libraries behave. + which will effect how the React Native core libraries behave. * `minify` boolean, defaults to false: whether to minify the bundle. * `runModule` boolean, defaults to true: whether to require your entry point module. So if you requested `moduleName`, this option will add