diff --git a/Examples/2048/Game2048.js b/Examples/2048/Game2048.js
index 9fd6052c9..e43eefa75 100644
--- a/Examples/2048/Game2048.js
+++ b/Examples/2048/Game2048.js
@@ -136,10 +136,8 @@ class GameEndOverlay extends React.Component {
return (
{message}
-
-
- Try Again?
-
+
+ Try Again?
);
diff --git a/Examples/UIExplorer/UIExplorerApp.ios.js b/Examples/UIExplorer/UIExplorerApp.ios.js
index e5bfd22a3..c2f4734e8 100644
--- a/Examples/UIExplorer/UIExplorerApp.ios.js
+++ b/Examples/UIExplorer/UIExplorerApp.ios.js
@@ -17,7 +17,7 @@
'use strict';
var React = require('react-native');
-var UIExplorerList = require('./UIExplorerList');
+var UIExplorerList = require('./UIExplorerList.ios');
var {
AppRegistry,
NavigatorIOS,
diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js
new file mode 100644
index 000000000..2e1c444f1
--- /dev/null
+++ b/Examples/UIExplorer/UIExplorerList.ios.js
@@ -0,0 +1,176 @@
+/**
+ * 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 {
+ AppRegistry,
+ Settings,
+ StyleSheet,
+} = React;
+
+var { TestModule } = React.addons;
+
+import type { NavigationContext } from 'NavigationContext';
+
+var UIExplorerListBase = require('./UIExplorerListBase');
+
+var COMPONENTS = [
+ require('./ActivityIndicatorIOSExample'),
+ require('./DatePickerIOSExample'),
+ require('./ImageExample'),
+ require('./LayoutEventsExample'),
+ require('./ListViewExample'),
+ require('./ListViewGridLayoutExample'),
+ require('./ListViewPagingExample'),
+ require('./MapViewExample'),
+ require('./Navigator/NavigatorExample'),
+ require('./NavigatorIOSColorsExample'),
+ require('./NavigatorIOSExample'),
+ require('./PickerIOSExample'),
+ require('./ProgressViewIOSExample'),
+ require('./ScrollViewExample'),
+ require('./SegmentedControlIOSExample'),
+ require('./SliderIOSExample'),
+ require('./SwitchIOSExample'),
+ require('./TabBarIOSExample'),
+ require('./TextExample.ios'),
+ require('./TextInputExample'),
+ require('./TouchableExample'),
+ require('./ViewExample'),
+ require('./WebViewExample'),
+];
+
+var APIS = [
+ require('./AccessibilityIOSExample'),
+ require('./ActionSheetIOSExample'),
+ require('./AdSupportIOSExample'),
+ require('./AlertIOSExample'),
+ require('./AnimationExample/AnExApp'),
+ require('./AppStateIOSExample'),
+ require('./AsyncStorageExample'),
+ require('./BorderExample'),
+ require('./CameraRollExample.ios'),
+ require('./GeolocationExample'),
+ require('./LayoutExample'),
+ require('./NetInfoExample'),
+ require('./PanResponderExample'),
+ require('./PointerEventsExample'),
+ require('./PushNotificationIOSExample'),
+ require('./StatusBarIOSExample'),
+ require('./TimerExample'),
+ require('./VibrationIOSExample'),
+ require('./XHRExample'),
+];
+
+// Register suitable examples for snapshot tests
+COMPONENTS.concat(APIS).forEach((Example) => {
+ if (Example.displayName) {
+ var Snapshotter = React.createClass({
+ componentDidMount: function() {
+ // View is still blank after first RAF :\
+ global.requestAnimationFrame(() =>
+ global.requestAnimationFrame(() => TestModule.verifySnapshot(
+ TestModule.markTestPassed
+ )
+ ));
+ },
+ render: function() {
+ var Renderable = UIExplorerListBase.makeRenderable(Example);
+ return ;
+ },
+ });
+ AppRegistry.registerComponent(Example.displayName, () => Snapshotter);
+ }
+});
+
+type Props = {
+ navigator: {
+ navigationContext: NavigationContext,
+ push: (route: {title: string, component: ReactClass}) => void,
+ },
+ onExternalExampleRequested: Function,
+};
+
+class UIExplorerList extends React.Component {
+ props: Props;
+
+ render() {
+ return (
+
+ );
+ }
+
+ componentWillMount() {
+ this.props.navigator.navigationContext.addListener('didfocus', function(event) {
+ if (event.data.route.title === 'UIExplorer') {
+ Settings.set({visibleExample: null});
+ }
+ });
+ }
+
+ componentDidMount() {
+ var visibleExampleTitle = Settings.get('visibleExample');
+ if (visibleExampleTitle) {
+ var predicate = (example) => example.title === visibleExampleTitle;
+ var foundExample = APIS.find(predicate) || COMPONENTS.find(predicate);
+ if (foundExample) {
+ setTimeout(() => this._openExample(foundExample), 100);
+ }
+ }
+ }
+
+ renderAdditionalView(renderRow: Function, renderTextInput: Function): React.Component {
+ return renderTextInput(styles.searchTextInput);
+ }
+
+ search(text: mixed) {
+ Settings.set({searchText: text});
+ }
+
+ _openExample(example: any) {
+ if (example.external) {
+ this.props.onExternalExampleRequested(example);
+ return;
+ }
+
+ var Component = UIExplorerListBase.makeRenderable(example);
+ this.props.navigator.push({
+ title: Component.title,
+ component: Component,
+ });
+ }
+
+ onPressRow(example: any) {
+ Settings.set({visibleExample: example.title});
+ this._openExample(example);
+ }
+}
+
+var styles = StyleSheet.create({
+ searchTextInput: {
+ height: 30,
+ },
+});
+
+module.exports = UIExplorerList;
diff --git a/Examples/UIExplorer/UIExplorerListBase.js b/Examples/UIExplorer/UIExplorerListBase.js
new file mode 100644
index 000000000..4b26a6976
--- /dev/null
+++ b/Examples/UIExplorer/UIExplorerListBase.js
@@ -0,0 +1,195 @@
+/**
+ * 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 {
+ ListView,
+ PixelRatio,
+ StyleSheet,
+ Text,
+ TextInput,
+ TouchableHighlight,
+ View,
+} = React;
+var createExamplePage = require('./createExamplePage');
+
+var ds = new ListView.DataSource({
+ rowHasChanged: (r1, r2) => r1 !== r2,
+ sectionHeaderHasChanged: (h1, h2) => h1 !== h2,
+});
+
+class UIExplorerListBase extends React.Component {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ dataSource: ds.cloneWithRowsAndSections({
+ components: [],
+ apis: [],
+ }),
+ searchText: this.props.searchText,
+ };
+ }
+
+ componentDidMount(): void {
+ this.search(this.state.searchText);
+ }
+
+ render() {
+ var topView = this.props.renderAdditionalView &&
+ this.props.renderAdditionalView(this.renderRow.bind(this), this.renderTextInput.bind(this));
+
+ return (
+
+ {topView}
+
+
+ );
+ }
+
+ renderTextInput(searchTextInputStyle: any) {
+ return (
+
+
+
+ );
+ }
+
+ _renderSectionHeader(data: any, section: string) {
+ return (
+
+
+ {section.toUpperCase()}
+
+
+ );
+ }
+
+ renderRow(example: any, i: number) {
+ return (
+
+ this.onPressRow(example)}>
+
+
+ {example.title}
+
+
+ {example.description}
+
+
+
+
+
+ );
+ }
+
+ search(text: mixed): void {
+ this.props.search && this.props.search(text);
+
+ var regex = new RegExp(text, 'i');
+ var filter = (component) => regex.test(component.title);
+
+ this.setState({
+ dataSource: ds.cloneWithRowsAndSections({
+ components: this.props.components.filter(filter),
+ apis: this.props.apis.filter(filter),
+ }),
+ searchText: text,
+ });
+ }
+
+ onPressRow(example: any): void {
+ this.props.onPressRow && this.props.onPressRow(example);
+ }
+
+ static makeRenderable(example: any): ReactClass {
+ return example.examples ?
+ createExamplePage(null, example) :
+ example;
+ }
+}
+
+var styles = StyleSheet.create({
+ listContainer: {
+ flex: 1,
+ },
+ list: {
+ backgroundColor: '#eeeeee',
+ },
+ sectionHeader: {
+ padding: 5,
+ },
+ group: {
+ backgroundColor: 'white',
+ },
+ sectionHeaderTitle: {
+ fontWeight: '500',
+ fontSize: 11,
+ },
+ row: {
+ backgroundColor: 'white',
+ justifyContent: 'center',
+ paddingHorizontal: 15,
+ paddingVertical: 8,
+ },
+ separator: {
+ height: 1 / PixelRatio.get(),
+ backgroundColor: '#bbbbbb',
+ marginLeft: 15,
+ },
+ rowTitleText: {
+ fontSize: 17,
+ fontWeight: '500',
+ },
+ rowDetailText: {
+ fontSize: 15,
+ color: '#888888',
+ lineHeight: 20,
+ },
+ searchRow: {
+ backgroundColor: '#eeeeee',
+ paddingTop: 75,
+ paddingLeft: 10,
+ paddingRight: 10,
+ paddingBottom: 10,
+ },
+ searchTextInput: {
+ backgroundColor: 'white',
+ borderColor: '#cccccc',
+ borderRadius: 3,
+ borderWidth: 1,
+ paddingLeft: 8,
+ },
+});
+
+module.exports = UIExplorerListBase;
diff --git a/Libraries/Animation/Animated/Animated.js b/Libraries/Animation/Animated/Animated.js
index f99913f50..8f3f38398 100644
--- a/Libraries/Animation/Animated/Animated.js
+++ b/Libraries/Animation/Animated/Animated.js
@@ -479,7 +479,13 @@ class SpringAnimation extends Animation {
if (this._tension !== 0) {
isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
}
+
if (isOvershooting || (isVelocity && isDisplacement)) {
+ if (this._tension !== 0) {
+ // Ensure that we end up with a round value
+ this._onUpdate(this._toValue);
+ }
+
this.__debouncedOnEnd({finished: true});
return;
}
diff --git a/Libraries/Animation/Animated/__tests__/Animated-test.js b/Libraries/Animation/Animated/__tests__/Animated-test.js
index cad752ff0..d27ff920c 100644
--- a/Libraries/Animation/Animated/__tests__/Animated-test.js
+++ b/Libraries/Animation/Animated/__tests__/Animated-test.js
@@ -127,6 +127,19 @@ describe('Animated', () => {
Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback);
expect(callback).toBeCalled();
});
+
+ it('send toValue when a spring stops', () => {
+ var anim = new Animated.Value(0);
+ var listener = jest.genMockFunction();
+ anim.addListener(listener);
+ Animated.spring(anim, {toValue: 15}).start();
+ jest.runAllTimers();
+ var lastValue = listener.mock.calls[listener.mock.calls.length - 2][0].value;
+ expect(lastValue).not.toBe(15);
+ expect(lastValue).toBeCloseTo(15);
+ expect(anim.__getValue()).toBe(15);
+ });
+
});
diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js
index 168cb36fa..ab03e55fb 100644
--- a/Libraries/Components/ScrollResponder.js
+++ b/Libraries/Components/ScrollResponder.js
@@ -470,7 +470,11 @@ var ScrollResponderMixin = {
},
scrollResponderKeyboardDidShow: function(e: Event) {
- this.keyboardWillOpenTo = null;
+ // TODO(7693961): The event for DidShow is not available on iOS yet.
+ // Use the one from WillShow and do not assign.
+ if (e) {
+ this.keyboardWillOpenTo = e;
+ }
this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
},
diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js
index 512d8dbd6..48dd3c465 100644
--- a/Libraries/Components/ScrollView/ScrollView.js
+++ b/Libraries/Components/ScrollView/ScrollView.js
@@ -53,44 +53,54 @@ var INNERVIEW = 'InnerScrollView';
* Doesn't yet support other contained responders from blocking this scroll
* view from becoming the responder.
*/
-
var ScrollView = React.createClass({
propTypes: {
- automaticallyAdjustContentInsets: PropTypes.bool, // true
- contentInset: EdgeInsetsPropType, // zeros
- contentOffset: PointPropType, // zeros
- onScroll: PropTypes.func,
- onScrollAnimationEnd: PropTypes.func,
- scrollEnabled: PropTypes.bool, // true
- scrollIndicatorInsets: EdgeInsetsPropType, // zeros
- showsHorizontalScrollIndicator: PropTypes.bool,
- showsVerticalScrollIndicator: PropTypes.bool,
- style: StyleSheetPropType(ViewStylePropTypes),
- scrollEventThrottle: PropTypes.number, // null
-
+ /**
+ * Controls whether iOS should automatically adjust the content inset
+ * for scroll views that are placed behind a navigation bar or
+ * tab bar/ toolbar. The default value is true.
+ * @platform ios
+ */
+ automaticallyAdjustContentInsets: PropTypes.bool,
+ /**
+ * The amount by which the scroll view content is inset from the edges
+ * of the scroll view. Defaults to `{0, 0, 0, 0}`.
+ * @platform ios
+ */
+ contentInset: EdgeInsetsPropType,
+ /**
+ * Used to manually set the starting scroll offset.
+ * The default value is `{x: 0, y: 0}`.
+ * @platform ios
+ */
+ contentOffset: PointPropType,
/**
* When true, the scroll view bounces when it reaches the end of the
* content if the content is larger then the scroll view along the axis of
* the scroll direction. When false, it disables all bouncing even if
* the `alwaysBounce*` props are true. The default value is true.
+ * @platform ios
*/
bounces: PropTypes.bool,
/**
* When true, gestures can drive zoom past min/max and the zoom will animate
* to the min/max value at gesture end, otherwise the zoom will not exceed
* the limits.
+ * @platform ios
*/
bouncesZoom: PropTypes.bool,
/**
* When true, the scroll view bounces horizontally when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is true when `horizontal={true}` and false otherwise.
+ * @platform ios
*/
alwaysBounceHorizontal: PropTypes.bool,
/**
* When true, the scroll view bounces vertically when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is false when `horizontal={true}` and true otherwise.
+ * @platform ios
*/
alwaysBounceVertical: PropTypes.bool,
/**
@@ -98,6 +108,7 @@ var ScrollView = React.createClass({
* content is smaller than the scroll view bounds; when the content is
* larger than the scroll view, this property has no effect. The default
* value is false.
+ * @platform ios
*/
centerContent: PropTypes.bool,
/**
@@ -121,6 +132,7 @@ var ScrollView = React.createClass({
* decelerates after the user lifts their finger. Reasonable choices include
* - Normal: 0.998 (the default)
* - Fast: 0.9
+ * @platform ios
*/
decelerationRate: PropTypes.number,
/**
@@ -131,17 +143,19 @@ var ScrollView = React.createClass({
/**
* When true, the ScrollView will try to lock to only vertical or horizontal
* scrolling while dragging. The default value is false.
+ * @platform ios
*/
directionalLockEnabled: PropTypes.bool,
/**
* When false, once tracking starts, won't try to drag if the touch moves.
* The default value is true.
+ * @platform ios
*/
canCancelContentTouches: PropTypes.bool,
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default), drags do not dismiss the keyboard.
- * - 'onDrag', the keyboard is dismissed when a drag begins.
+ * - 'on-drag', the keyboard is dismissed when a drag begins.
* - 'interactive', the keyboard is dismissed interactively with the drag
* and moves in synchrony with the touch; dragging upwards cancels the
* dismissal.
@@ -156,35 +170,83 @@ var ScrollView = React.createClass({
* is up dismisses the keyboard. When true, the scroll view will not catch
* taps, and the keyboard will not dismiss automatically. The default value
* is false.
+ * @platform ios
*/
keyboardShouldPersistTaps: PropTypes.bool,
/**
* The maximum allowed zoom scale. The default value is 1.0.
+ * @platform ios
*/
maximumZoomScale: PropTypes.number,
/**
* The minimum allowed zoom scale. The default value is 1.0.
+ * @platform ios
*/
minimumZoomScale: PropTypes.number,
+ /**
+ * Fires at most once per frame during scrolling. The frequency of the
+ * events can be contolled using the `scrollEventThrottle` prop.
+ */
+ onScroll: PropTypes.func,
+ /**
+ * Called when a scrolling animation ends.
+ * @platform ios
+ */
+ onScrollAnimationEnd: PropTypes.func,
/**
* When true, the scroll view stops on multiples of the scroll view's size
* when scrolling. This can be used for horizontal pagination. The default
* value is false.
+ * @platform ios
*/
pagingEnabled: PropTypes.bool,
+ /**
+ * When false, the content does not scroll.
+ * The default value is true.
+ * @platform ios
+ */
+ scrollEnabled: PropTypes.bool,
+ /**
+ * This controls how often the scroll event will be fired while scrolling
+ * (in events per seconds). A higher number yields better accuracy for code
+ * that is tracking the scroll position, but can lead to scroll performance
+ * problems due to the volume of information being send over the bridge.
+ * The default value is zero, which means the scroll event will be sent
+ * only once each time the view is scrolled.
+ * @platform ios
+ */
+ scrollEventThrottle: PropTypes.number,
+ /**
+ * The amount by which the scroll view indicators are inset from the edges
+ * of the scroll view. This should normally be set to the same value as
+ * the `contentInset`. Defaults to `{0, 0, 0, 0}`.
+ * @platform ios
+ */
+ scrollIndicatorInsets: EdgeInsetsPropType,
/**
* When true, the scroll view scrolls to top when the status bar is tapped.
* The default value is true.
+ * @platform ios
*/
scrollsToTop: PropTypes.bool,
+ /**
+ * When true, shows a horizontal scroll indicator.
+ */
+ showsHorizontalScrollIndicator: PropTypes.bool,
+ /**
+ * When true, shows a vertical scroll indicator.
+ */
+ showsVerticalScrollIndicator: PropTypes.bool,
/**
* An array of child indices determining which children get docked to the
* top of the screen when scrolling. For example, passing
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
* top of the scroll view. This property is not supported in conjunction
* with `horizontal={true}`.
+ * @platform ios
*/
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
+ style: StyleSheetPropType(ViewStylePropTypes),
/**
* Experimental: When true, offscreen child views (whose `overflow` value is
* `hidden`) are removed from their native backing superview when offscreen.
@@ -194,6 +256,7 @@ var ScrollView = React.createClass({
removeClippedSubviews: PropTypes.bool,
/**
* The current scale of the scroll view content. The default value is 1.0.
+ * @platform ios
*/
zoomScale: PropTypes.number,
},
diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js
index 9aad783b0..bada73041 100644
--- a/Libraries/Components/Touchable/TouchableBounce.js
+++ b/Libraries/Components/Touchable/TouchableBounce.js
@@ -11,20 +11,12 @@
*/
'use strict';
-var AnimationExperimental = require('AnimationExperimental');
+var Animated = require('Animated');
var NativeMethodsMixin = require('NativeMethodsMixin');
-var POPAnimation = require('POPAnimation');
var React = require('React');
var Touchable = require('Touchable');
var merge = require('merge');
-var onlyChild = require('onlyChild');
-
-var invariant = require('invariant');
-invariant(
- AnimationExperimental || POPAnimation,
- 'Please add the RCTAnimationExperimental framework to your project, or add //Libraries/FBReactKit:RCTPOPAnimation to your BUCK file if running internally within Facebook.'
-);
type State = {
animationID: ?number;
@@ -58,40 +50,23 @@ var TouchableBounce = React.createClass({
},
getInitialState: function(): State {
- return merge(this.touchableGetInitialState(), {animationID: null});
+ return {
+ ...this.touchableGetInitialState(),
+ scale: new Animated.Value(1),
+ };
},
bounceTo: function(
value: number,
velocity: number,
bounciness: number,
- fromValue?: ?number,
callback?: ?Function
) {
- if (POPAnimation) {
- this.state.animationID && this.removeAnimation(this.state.animationID);
- var anim = {
- property: POPAnimation.Properties.scaleXY,
- dynamicsTension: 0,
- toValue: [value, value],
- velocity: [velocity, velocity],
- springBounciness: bounciness,
- fromValue: fromValue ? [fromValue, fromValue] : undefined,
- };
- this.state.animationID = POPAnimation.createSpringAnimation(anim);
- this.addAnimation(this.state.animationID, callback);
- } else {
- AnimationExperimental.startAnimation(
- {
- node: this,
- duration: 300,
- easing: 'easeOutBack',
- property: 'scaleXY',
- toValue: { x: value, y: value},
- },
- callback
- );
- }
+ Animated.spring(this.state.scale, {
+ toValue: value,
+ velocity,
+ bounciness,
+ }).start(callback);
},
/**
@@ -109,13 +84,14 @@ var TouchableBounce = React.createClass({
touchableHandlePress: function() {
var onPressWithCompletion = this.props.onPressWithCompletion;
if (onPressWithCompletion) {
- onPressWithCompletion(
- this.bounceTo.bind(this, 1, 10, 10, 0.93, this.props.onPressAnimationComplete)
- );
+ onPressWithCompletion(() => {
+ this.state.scale.setValue(0.93);
+ this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
+ });
return;
}
- this.bounceTo(1, 10, 10, undefined, this.props.onPressAnimationComplete);
+ this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
this.props.onPress && this.props.onPress();
},
@@ -127,18 +103,21 @@ var TouchableBounce = React.createClass({
return 0;
},
- render: function() {
- var child = onlyChild(this.props.children);
- return React.cloneElement(child, {
- accessible: true,
- testID: this.props.testID,
- onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
- onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
- onResponderGrant: this.touchableHandleResponderGrant,
- onResponderMove: this.touchableHandleResponderMove,
- onResponderRelease: this.touchableHandleResponderRelease,
- onResponderTerminate: this.touchableHandleResponderTerminate
- });
+ render: function(): ReactElement {
+ return (
+
+ {this.props.children}
+
+ );
}
});
diff --git a/Libraries/Inspector/ElementProperties.js b/Libraries/Inspector/ElementProperties.js
index 855423845..222da109f 100644
--- a/Libraries/Inspector/ElementProperties.js
+++ b/Libraries/Inspector/ElementProperties.js
@@ -27,7 +27,11 @@ var mapWithSeparator = require('mapWithSeparator');
var ElementProperties = React.createClass({
propTypes: {
hierarchy: PropTypes.array.isRequired,
- style: PropTypes.array.isRequired,
+ style: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.array,
+ PropTypes.number,
+ ]),
},
render: function() {
diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js
index 6015bcf3f..c014c29c4 100644
--- a/Libraries/Inspector/Inspector.js
+++ b/Libraries/Inspector/Inspector.js
@@ -20,7 +20,7 @@ var StyleSheet = require('StyleSheet');
var UIManager = require('NativeModules').UIManager;
var View = require('View');
-var REACT_DEVTOOLS_HOOK: ?Object = typeof window !== 'undefined' ? window.__REACT_DEVTOOLS_BACKEND__ : null;
+var REACT_DEVTOOLS_HOOK: ?Object = typeof window !== 'undefined' ? window.__REACT_DEVTOOLS_GLOBAL_HOOK__ : null;
if (REACT_DEVTOOLS_HOOK) {
// required for devtools to be able to edit react native styles
@@ -34,7 +34,7 @@ class Inspector extends React.Component {
super(props);
this.state = {
- devtoolsBackend: null,
+ devtoolsAgent: null,
panelPos: 'bottom',
inspecting: true,
perfing: false,
@@ -45,14 +45,10 @@ class Inspector extends React.Component {
componentDidMount() {
if (REACT_DEVTOOLS_HOOK) {
this.attachToDevtools = this.attachToDevtools.bind(this);
- REACT_DEVTOOLS_HOOK.addStartupListener(this.attachToDevtools);
+ REACT_DEVTOOLS_HOOK.on('react-devtools', this.attachToDevtools);
// if devtools is already started
- // TODO(jared): should addStartupListener just go ahead and call the
- // listener if the devtools is already started? might be unexpected...
- // is there some name other than `addStartupListener` that would be
- // better?
- if (REACT_DEVTOOLS_HOOK.backend) {
- this.attachToDevtools(REACT_DEVTOOLS_HOOK.backend);
+ if (REACT_DEVTOOLS_HOOK.reactDevtoolsAgent) {
+ this.attachToDevtools(REACT_DEVTOOLS_HOOK.reactDevtoolsAgent);
}
}
}
@@ -62,13 +58,13 @@ class Inspector extends React.Component {
this._subs.map(fn => fn());
}
if (REACT_DEVTOOLS_HOOK) {
- REACT_DEVTOOLS_HOOK.removeStartupListener(this.attachToDevtools);
+ REACT_DEVTOOLS_HOOK.off('react-devtools', this.attachToDevtools);
}
}
- attachToDevtools(backend: Object) {
+ attachToDevtools(agent: Object) {
var _hideWait = null;
- var hlSub = backend.sub('highlight', ({node, name, props}) => {
+ var hlSub = agent.sub('highlight', ({node, name, props}) => {
clearTimeout(_hideWait);
UIManager.measure(node, (x, y, width, height, left, top) => {
this.setState({
@@ -80,7 +76,10 @@ class Inspector extends React.Component {
});
});
});
- var hideSub = backend.sub('hideHighlight', () => {
+ var hideSub = agent.sub('hideHighlight', () => {
+ if (this.state.inspected === null) {
+ return;
+ }
// we wait to actually hide in order to avoid flicker
_hideWait = setTimeout(() => {
this.setState({
@@ -90,12 +89,12 @@ class Inspector extends React.Component {
});
this._subs = [hlSub, hideSub];
- backend.on('shutdown', () => {
- this.setState({devtoolsBackend: null});
+ agent.on('shutdown', () => {
+ this.setState({devtoolsAgent: null});
this._subs = null;
});
this.setState({
- devtoolsBackend: backend,
+ devtoolsAgent: agent,
});
}
@@ -114,8 +113,8 @@ class Inspector extends React.Component {
}
onTouchInstance(instance: Object, frame: Object, pointerY: number) {
- if (this.state.devtoolsBackend) {
- this.state.devtoolsBackend.selectFromReactInstance(instance, true);
+ if (this.state.devtoolsAgent) {
+ this.state.devtoolsAgent.selectFromReactInstance(instance, true);
}
var hierarchy = InspectorUtils.getOwnerHierarchy(instance);
var publicInstance = instance.getPublicInstance();
@@ -159,7 +158,7 @@ class Inspector extends React.Component {
/>}
+
+#import "RCTURLRequestDelegate.h"
+#import "RCTURLRequestHandler.h"
+
+typedef void (^RCTURLRequestCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error);
+typedef void (^RCTURLRequestCancellationBlock)(void);
+typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data);
+typedef void (^RCTURLRequestProgressBlock)(double progress, double total);
+typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
+
+@interface RCTDownloadTask : NSObject
+
+@property (nonatomic, readonly) NSURLRequest *request;
+@property (nonatomic, readonly) NSNumber *requestID;
+@property (nonatomic, readonly) id requestToken;
+@property (nonatomic, readonly) NSURLResponse *response;
+@property (nonatomic, readonly) RCTURLRequestCompletionBlock completionBlock;
+
+@property (nonatomic, copy) RCTURLRequestProgressBlock downloadProgressBlock;
+@property (nonatomic, copy) RCTURLRequestIncrementalDataBlock incrementalDataBlock;
+@property (nonatomic, copy) RCTURLRequestResponseBlock responseBlock;
+@property (nonatomic, copy) RCTURLRequestProgressBlock uploadProgressBlock;
+
+- (instancetype)initWithRequest:(NSURLRequest *)request
+ handler:(id)handler
+ completionBlock:(RCTURLRequestCompletionBlock)completionBlock NS_DESIGNATED_INITIALIZER;
+
+- (void)cancel;
+
+@end
diff --git a/Libraries/Network/RCTDownloadTask.m b/Libraries/Network/RCTDownloadTask.m
new file mode 100644
index 000000000..e0b86dacb
--- /dev/null
+++ b/Libraries/Network/RCTDownloadTask.m
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "RCTDownloadTask.h"
+
+#import "RCTAssert.h"
+
+@implementation RCTDownloadTask
+{
+ NSMutableData *_data;
+ id _handler;
+ RCTDownloadTask *_selfReference;
+}
+
+- (instancetype)initWithRequest:(NSURLRequest *)request
+ handler:(id)handler
+ completionBlock:(RCTURLRequestCompletionBlock)completionBlock
+{
+ RCTAssertParam(request);
+ RCTAssertParam(handler);
+ RCTAssertParam(completionBlock);
+
+ static NSUInteger requestID = 0;
+
+ if ((self = [super init])) {
+ if (!(_requestToken = [handler sendRequest:request withDelegate:self])) {
+ return nil;
+ }
+ _requestID = @(requestID++);
+ _request = request;
+ _handler = handler;
+ _completionBlock = completionBlock;
+ _selfReference = self;
+ }
+ return self;
+}
+
+- (void)invalidate
+{
+ _selfReference = nil;
+ _completionBlock = nil;
+ _downloadProgressBlock = nil;
+ _incrementalDataBlock = nil;
+ _responseBlock = nil;
+ _uploadProgressBlock = nil;
+}
+
+RCT_NOT_IMPLEMENTED(-init)
+
+- (void)cancel
+{
+ if ([_handler respondsToSelector:@selector(cancelRequest:)]) {
+ [_handler cancelRequest:_requestToken];
+ }
+ [self invalidate];
+}
+
+- (void)URLRequest:(id)requestToken didSendDataWithProgress:(int64_t)bytesSent
+{
+ RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
+ if (_uploadProgressBlock) {
+ _uploadProgressBlock(bytesSent, _request.HTTPBody.length);
+ }
+}
+
+- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
+{
+ RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
+ _response = response;
+ if (_responseBlock) {
+ _responseBlock(response);
+ }
+}
+
+- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data
+{
+ RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
+ if (!_data) {
+ _data = [[NSMutableData alloc] init];
+ }
+ [_data appendData:data];
+ if (_incrementalDataBlock) {
+ _incrementalDataBlock(data);
+ }
+ if (_downloadProgressBlock && _response.expectedContentLength > 0) {
+ _downloadProgressBlock(_data.length, _response.expectedContentLength);
+ }
+}
+
+- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
+{
+ _completionBlock(_response, _data, error);
+ [self invalidate];
+}
+
+@end
diff --git a/Libraries/Network/RCTHTTPRequestHandler.h b/Libraries/Network/RCTHTTPRequestHandler.h
index b8a7a3e26..155491e63 100644
--- a/Libraries/Network/RCTHTTPRequestHandler.h
+++ b/Libraries/Network/RCTHTTPRequestHandler.h
@@ -10,6 +10,9 @@
#import "RCTURLRequestHandler.h"
#import "RCTInvalidating.h"
+/**
+ * This is the default RCTURLRequestHandler implementation for HTTP requests.
+ */
@interface RCTHTTPRequestHandler : NSObject
@end
diff --git a/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m
index c89a4fbc9..d5ee89a45 100644
--- a/Libraries/Network/RCTHTTPRequestHandler.m
+++ b/Libraries/Network/RCTHTTPRequestHandler.m
@@ -50,8 +50,8 @@ RCT_EXPORT_MODULE()
return [@[@"http", @"https", @"file"] containsObject:[request.URL.scheme lowercaseString]];
}
-- (id)sendRequest:(NSURLRequest *)request
- withDelegate:(id)delegate
+- (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request
+ withDelegate:(id)delegate
{
// Lazy setup
if (!_session && [self isValid]) {
@@ -68,9 +68,10 @@ RCT_EXPORT_MODULE()
return task;
}
-- (void)cancelRequest:(NSURLSessionDataTask *)requestToken
+- (void)cancelRequest:(NSURLSessionDataTask *)task
{
- [requestToken cancel];
+ [task cancel];
+ [_delegates removeObjectForKey:task];
}
#pragma mark - NSURLSession delegate
@@ -81,10 +82,9 @@ RCT_EXPORT_MODULE()
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
- [[_delegates objectForKey:task] URLRequest:task didUploadProgress:(double)totalBytesSent total:(double)totalBytesExpectedToSend];
+ [[_delegates objectForKey:task] URLRequest:task didSendDataWithProgress:totalBytesSent];
}
-
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)task
didReceiveResponse:(NSURLResponse *)response
diff --git a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj
index 2a3262871..88835214b 100644
--- a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj
+++ b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; };
+ 13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */; };
352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */; };
58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTNetworking.m */; };
/* End PBXBuildFile section */
@@ -27,6 +28,8 @@
/* Begin PBXFileReference section */
1372B7351AB03E7B00659ED6 /* RCTReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTReachability.h; sourceTree = ""; };
1372B7361AB03E7B00659ED6 /* RCTReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTReachability.m; sourceTree = ""; };
+ 13D6D6681B5FCF8200883BE9 /* RCTDownloadTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTask.h; sourceTree = ""; };
+ 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTask.m; sourceTree = ""; };
352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHTTPRequestHandler.h; sourceTree = ""; };
352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTHTTPRequestHandler.m; sourceTree = ""; };
58B511DB1A9E6C8500147676 /* libRCTNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -48,6 +51,8 @@
58B511D21A9E6C8500147676 = {
isa = PBXGroup;
children = (
+ 13D6D6681B5FCF8200883BE9 /* RCTDownloadTask.h */,
+ 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */,
352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */,
352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */,
58B512061A9E6CE300147676 /* RCTNetworking.h */,
@@ -124,6 +129,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */,
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */,
58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */,
352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */,
diff --git a/Libraries/Network/RCTNetworking.h b/Libraries/Network/RCTNetworking.h
index d8bc39524..3e54354b5 100644
--- a/Libraries/Network/RCTNetworking.h
+++ b/Libraries/Network/RCTNetworking.h
@@ -14,4 +14,3 @@
@interface RCTNetworking : NSObject
@end
-
diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m
index d73f1fb4d..0af33fe78 100644
--- a/Libraries/Network/RCTNetworking.m
+++ b/Libraries/Network/RCTNetworking.m
@@ -11,18 +11,19 @@
#import "RCTAssert.h"
#import "RCTConvert.h"
+#import "RCTDownloadTask.h"
#import "RCTURLRequestHandler.h"
#import "RCTEventDispatcher.h"
#import "RCTHTTPRequestHandler.h"
#import "RCTLog.h"
#import "RCTUtils.h"
-typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
+typedef RCTURLRequestCancellationBlock (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
-@interface RCTNetworking ()
-
-- (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback;
+@interface RCTNetworking ()
+- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary *)data
+ callback:(RCTHTTPQueryResult)callback;
@end
/**
@@ -30,7 +31,7 @@ typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
*/
@interface RCTHTTPFormDataHelper : NSObject
-@property (nonatomic, weak) RCTNetworking *dataManager;
+@property (nonatomic, weak) RCTNetworking *networker;
@end
@@ -42,51 +43,49 @@ typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
NSString *boundary;
}
-- (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))callback
+static NSString *RCTGenerateFormBoundary()
{
- if (![formData count]) {
- callback(nil, nil);
- return;
+ const size_t boundaryLength = 70;
+ const char *boundaryChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./";
+
+ char *bytes = malloc(boundaryLength);
+ size_t charCount = strlen(boundaryChars);
+ for (int i = 0; i < boundaryLength; i++) {
+ bytes[i] = boundaryChars[arc4random_uniform((u_int32_t)charCount)];
}
+ return [[NSString alloc] initWithBytesNoCopy:bytes length:boundaryLength encoding:NSUTF8StringEncoding freeWhenDone:YES];
+}
+
+- (RCTURLRequestCancellationBlock)process:(NSArray *)formData
+ callback:(RCTHTTPQueryResult)callback
+{
+ if (formData.count == 0) {
+ return callback(nil, nil);
+ }
+
parts = [formData mutableCopy];
_callback = callback;
multipartBody = [[NSMutableData alloc] init];
- boundary = [self generateBoundary];
+ boundary = RCTGenerateFormBoundary();
- NSDictionary *currentPart = [parts objectAtIndex: 0];
- [_dataManager processDataForHTTPQuery:currentPart callback:^(NSError *e, NSDictionary *r) {
- [self handleResult:r error:e];
+ return [_networker processDataForHTTPQuery:parts[0] callback:^(NSError *error, NSDictionary *result) {
+ return [self handleResult:result error:error];
}];
}
-- (NSString *)generateBoundary
-{
- NSString *const boundaryChars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./";
- const NSUInteger boundaryLength = 70;
-
- NSMutableString *output = [NSMutableString stringWithCapacity:boundaryLength];
- NSUInteger numchars = [boundaryChars length];
- for (NSUInteger i = 0; i < boundaryLength; i++) {
- [output appendFormat:@"%C", [boundaryChars characterAtIndex:arc4random_uniform((u_int32_t)numchars)]];
- }
- return output;
-}
-
-- (void)handleResult:(NSDictionary *)result error:(NSError *)error
+- (RCTURLRequestCancellationBlock)handleResult:(NSDictionary *)result
+ error:(NSError *)error
{
if (error) {
- _callback(error, nil);
- return;
+ return _callback(error, nil);
}
- NSDictionary *currentPart = parts[0];
- [parts removeObjectAtIndex:0];
// Start with boundary.
[multipartBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary]
dataUsingEncoding:NSUTF8StringEncoding]];
// Print headers.
- NSMutableDictionary *headers = [(NSDictionary*)currentPart[@"headers"] mutableCopy];
+ NSMutableDictionary *headers = [parts[0][@"headers"] mutableCopy];
NSString *partContentType = result[@"contentType"];
if (partContentType != nil) {
[headers setObject:partContentType forKey:@"content-type"];
@@ -101,110 +100,18 @@ typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
[multipartBody appendData:result[@"body"]];
[multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
- if ([parts count]) {
- NSDictionary *nextPart = [parts objectAtIndex: 0];
- [_dataManager processDataForHTTPQuery:nextPart callback:^(NSError *e, NSDictionary *r) {
- [self handleResult:r error:e];
+ [parts removeObjectAtIndex:0];
+ if (parts.count) {
+ return [_networker processDataForHTTPQuery:parts[0] callback:^(NSError *err, NSDictionary *res) {
+ return [self handleResult:res error:err];
}];
- return;
}
// We've processed the last item. Finish and return.
[multipartBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary]
dataUsingEncoding:NSUTF8StringEncoding]];
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=\"%@\"", boundary];
- _callback(nil, @{@"body": multipartBody, @"contentType": contentType});
-}
-
-@end
-
-/**
- * Helper to package in-flight requests together with their response data.
- */
-@interface RCTActiveURLRequest : NSObject
-
-@property (nonatomic, strong) NSNumber *requestID;
-@property (nonatomic, strong) NSURLRequest *request;
-@property (nonatomic, strong) id handler;
-@property (nonatomic, assign) BOOL incrementalUpdates;
-@property (nonatomic, strong) NSURLResponse *response;
-@property (nonatomic, strong) NSMutableData *data;
-
-@end
-
-@implementation RCTActiveURLRequest
-
-- (instancetype)init
-{
- if ((self = [super init])) {
- _data = [[NSMutableData alloc] init];
- }
- return self;
-}
-
-@end
-
-/**
- * Helper to load request body data using a handler.
- */
-@interface RCTDataLoader : NSObject
-
-@end
-
-typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError *error);
-
-@implementation RCTDataLoader
-{
- RCTDataLoaderCallback _callback;
- RCTActiveURLRequest *_request;
- id _requestToken;
-}
-
-- (instancetype)initWithRequest:(NSURLRequest *)request
- handler:(id)handler
- callback:(RCTDataLoaderCallback)callback
-{
- RCTAssertParam(request);
- RCTAssertParam(handler);
- RCTAssertParam(callback);
-
- if ((self = [super init])) {
- _callback = callback;
- _request = [[RCTActiveURLRequest alloc] init];
- _request.request = request;
- _request.handler = handler;
- _request.incrementalUpdates = NO;
- _requestToken = [handler sendRequest:request withDelegate:self];
- }
- return self;
-}
-
-- (instancetype)init
-{
- return [self initWithRequest:nil handler:nil callback:nil];
-}
-
-- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
-{
- RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
-}
-
-- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
-{
- RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
- _request.response = response;
-}
-
-- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data
-{
- RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
- [_request.data appendData:data];
-}
-
-- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
-{
- RCTAssert(_callback != nil, @"The callback property must be set");
- _callback(_request.data, _request.response.MIMEType, error);
+ return _callback(nil, @{@"body": multipartBody, @"contentType": contentType});
}
@end
@@ -214,8 +121,7 @@ typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError
*/
@implementation RCTNetworking
{
- NSInteger _currentRequestID;
- NSMapTable *_activeRequests;
+ NSMutableDictionary *_tasksByRequestID;
}
@synthesize bridge = _bridge;
@@ -226,16 +132,13 @@ RCT_EXPORT_MODULE()
- (instancetype)init
{
if ((self = [super init])) {
- _currentRequestID = 0;
- _activeRequests = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory
- valueOptions:NSPointerFunctionsStrongMemory
- capacity:0];
+ _tasksByRequestID = [[NSMutableDictionary alloc] init];
}
return self;
}
-- (void)buildRequest:(NSDictionary *)query
- completionBlock:(void (^)(NSURLRequest *request))block
+- (RCTURLRequestCancellationBlock)buildRequest:(NSDictionary *)query
+ completionBlock:(void (^)(NSURLRequest *request))block
{
NSURL *URL = [RCTConvert NSURL:query[@"url"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
@@ -243,11 +146,11 @@ RCT_EXPORT_MODULE()
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]];
- [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) {
+ return [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) {
if (error) {
RCTLogError(@"Error processing request body: %@", error);
// Ideally we'd circle back to JS here and notify an error/abort on the request.
- return;
+ return (RCTURLRequestCancellationBlock)nil;
}
request.HTTPBody = result[@"body"];
NSString *contentType = result[@"contentType"];
@@ -262,6 +165,7 @@ RCT_EXPORT_MODULE()
}
block(request);
+ return (RCTURLRequestCancellationBlock)nil;
}];
}
@@ -316,71 +220,56 @@ RCT_EXPORT_MODULE()
* - @"contentType" (NSString): the content type header of the request
*
*/
-- (void)processDataForHTTPQuery:(NSDictionary *)query callback:(void (^)(NSError *error, NSDictionary *result))callback
+- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary *)query callback:
+(RCTURLRequestCancellationBlock (^)(NSError *error, NSDictionary *result))callback
{
if (!query) {
- callback(nil, nil);
- return;
+ return callback(nil, nil);
}
NSData *body = [RCTConvert NSData:query[@"string"]];
if (body) {
- callback(nil, @{@"body": body});
- return;
+ return callback(nil, @{@"body": body});
}
NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]];
if (request) {
+
id handler = [self handlerForRequest:request];
if (!handler) {
- return;
+ return callback(nil, nil);
}
- (void)[[RCTDataLoader alloc] initWithRequest:request handler:handler callback:^(NSData *data, NSString *MIMEType, NSError *error) {
- if (data) {
- callback(nil, @{@"body": data, @"contentType": MIMEType});
- } else {
- callback(error, nil);
- }
+
+ __block RCTURLRequestCancellationBlock cancellationBlock = nil;
+ RCTDownloadTask *task = [[RCTDownloadTask alloc] initWithRequest:request handler:handler completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
+ cancellationBlock = callback(error, data ? @{@"body": data, @"contentType": RCTNullIfNil(response.MIMEType)} : nil);
}];
- return;
+
+ __weak RCTDownloadTask *weakTask = task;
+ return ^{
+ [weakTask cancel];
+ if (cancellationBlock) {
+ cancellationBlock();
+ }
+ };
}
NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:query[@"formData"]];
- if (formData != nil) {
+ if (formData) {
RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init];
- formDataHelper.dataManager = self;
- [formDataHelper process:formData callback:callback];
- return;
+ formDataHelper.networker = self;
+ return [formDataHelper process:formData callback:callback];
}
// Nothing in the data payload, at least nothing we could understand anyway.
// Ignore and treat it as if it were null.
- callback(nil, nil);
+ return callback(nil, nil);
}
-- (void)sendRequest:(NSURLRequest *)request
- incrementalUpdates:(BOOL)incrementalUpdates
- responseSender:(RCTResponseSenderBlock)responseSender
-{
- id handler = [self handlerForRequest:request];
- id token = [handler sendRequest:request withDelegate:self];
- if (token) {
- RCTActiveURLRequest *activeRequest = [[RCTActiveURLRequest alloc] init];
- activeRequest.requestID = @(++_currentRequestID);
- activeRequest.request = request;
- activeRequest.handler = handler;
- activeRequest.incrementalUpdates = incrementalUpdates;
- [_activeRequests setObject:activeRequest forKey:token];
- responseSender(@[activeRequest.requestID]);
- }
-}
-
-- (void)sendData:(NSData *)data forRequestToken:(id)requestToken
+- (void)sendData:(NSData *)data forTask:(RCTDownloadTask *)task
{
if (data.length == 0) {
return;
}
- RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
-
// Get text encoding
- NSURLResponse *response = request.response;
+ NSURLResponse *response = task.response;
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
@@ -393,82 +282,81 @@ RCT_EXPORT_MODULE()
return;
}
- NSArray *responseJSON = @[request.requestID, responseText ?: @""];
+ NSArray *responseJSON = @[task.requestID, responseText ?: @""];
[_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData"
body:responseJSON];
}
-#pragma mark - RCTURLRequestDelegate
-
-- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
+- (void)sendRequest:(NSURLRequest *)request
+ incrementalUpdates:(BOOL)incrementalUpdates
+ responseSender:(RCTResponseSenderBlock)responseSender
{
- dispatch_async(_methodQueue, ^{
- RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
- RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
+ id handler = [self handlerForRequest:request];
+ if (!handler) {
+ return;
+ }
- NSArray *responseJSON = @[request.requestID, @(progress), @(total)];
- [_bridge.eventDispatcher sendDeviceEventWithName:@"didUploadProgress" body:responseJSON];
- });
-}
+ __block RCTDownloadTask *task;
-- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
-{
- dispatch_async(_methodQueue, ^{
- RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
- RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
+ RCTURLRequestProgressBlock uploadProgressBlock = ^(double progress, double total) {
+ dispatch_async(_methodQueue, ^{
+ NSArray *responseJSON = @[task.requestID, @(progress), @(total)];
+ [_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON];
+ });
+ };
- request.response = response;
+ void (^responseBlock)(NSURLResponse *) = ^(NSURLResponse *response) {
+ dispatch_async(_methodQueue, ^{
+ NSHTTPURLResponse *httpResponse = nil;
+ if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+ // Might be a local file request
+ httpResponse = (NSHTTPURLResponse *)response;
+ }
+ NSArray *responseJSON = @[task.requestID,
+ @(httpResponse.statusCode ?: 200),
+ httpResponse.allHeaderFields ?: @{},
+ ];
- NSHTTPURLResponse *httpResponse = nil;
- if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
- // Might be a local file request
- httpResponse = (NSHTTPURLResponse *)response;
- }
+ [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse"
+ body:responseJSON];
+ });
+ };
- NSArray *responseJSON = @[request.requestID,
- @(httpResponse.statusCode ?: 200),
- httpResponse.allHeaderFields ?: @{},
- ];
+ void (^incrementalDataBlock)(NSData *) = incrementalUpdates ? ^(NSData *data) {
+ dispatch_async(_methodQueue, ^{
+ [self sendData:data forTask:task];
+ });
+ } : nil;
- [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse"
- body:responseJSON];
- });
-}
+ RCTURLRequestCompletionBlock completionBlock =
+ ^(NSURLResponse *response, NSData *data, NSError *error) {
+ dispatch_async(_methodQueue, ^{
+ if (!incrementalUpdates) {
+ [self sendData:data forTask:task];
+ }
+ NSArray *responseJSON = @[task.requestID,
+ RCTNullIfNil(error.localizedDescription),
+ ];
-- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data
-{
- dispatch_async(_methodQueue, ^{
- RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
- RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
+ [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
+ body:responseJSON];
- if (request.incrementalUpdates) {
- [self sendData:data forRequestToken:requestToken];
- } else {
- [request.data appendData:data];
- }
- });
-}
+ [_tasksByRequestID removeObjectForKey:task.requestID];
+ });
+ };
-- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
-{
- dispatch_async(_methodQueue, ^{
- RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
- RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
+ task = [[RCTDownloadTask alloc] initWithRequest:request
+ handler:handler
+ completionBlock:completionBlock];
- if (!request.incrementalUpdates) {
- [self sendData:request.data forRequestToken:requestToken];
- }
+ task.incrementalDataBlock = incrementalDataBlock;
+ task.responseBlock = responseBlock;
+ task.uploadProgressBlock = uploadProgressBlock;
- NSArray *responseJSON = @[
- request.requestID,
- RCTNullIfNil(error.localizedDescription),
- ];
-
- [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
- body:responseJSON];
-
- [_activeRequests removeObjectForKey:requestToken];
- });
+ if (task.requestID) {
+ _tasksByRequestID[task.requestID] = task;
+ responseSender(@[task.requestID]);
+ }
}
#pragma mark - JS API
@@ -476,31 +364,22 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query
responseSender:(RCTResponseSenderBlock)responseSender)
{
+ // TODO: buildRequest returns a cancellation block, but there's currently
+ // no way to invoke it, if, for example the request is cancelled while
+ // loading a large file to build the request body
[self buildRequest:query completionBlock:^(NSURLRequest *request) {
BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]];
- [self sendRequest:request incrementalUpdates:incrementalUpdates
+ [self sendRequest:request
+ incrementalUpdates:incrementalUpdates
responseSender:responseSender];
}];
}
RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID)
{
- id requestToken = nil;
- RCTActiveURLRequest *activeRequest = nil;
- for (id token in _activeRequests) {
- RCTActiveURLRequest *request = [_activeRequests objectForKey:token];
- if ([request.requestID isEqualToNumber:requestID]) {
- activeRequest = request;
- requestToken = token;
- break;
- }
- }
-
- id handler = activeRequest.handler;
- if ([handler respondsToSelector:@selector(cancelRequest:)]) {
- [activeRequest.handler cancelRequest:requestToken];
- }
+ [_tasksByRequestID[requestID] cancel];
+ [_tasksByRequestID removeObjectForKey:requestID];
}
@end
diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js
index da020c808..7b84e35d1 100644
--- a/Libraries/Network/XMLHttpRequest.ios.js
+++ b/Libraries/Network/XMLHttpRequest.ios.js
@@ -35,7 +35,7 @@ class XMLHttpRequest extends XMLHttpRequestBase {
_didCreateRequest(requestId: number): void {
this._requestId = requestId;
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
- 'didUploadProgress',
+ 'didSendNetworkData',
(args) => this._didUploadProgress.call(this, args[0], args[1], args[2])
));
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
diff --git a/Libraries/react-native/react-native-interface.js b/Libraries/react-native/react-native-interface.js
index b76518387..623ffd98c 100644
--- a/Libraries/react-native/react-native-interface.js
+++ b/Libraries/react-native/react-native-interface.js
@@ -21,3 +21,6 @@ declare var fetch: any;
declare var Headers: any;
declare var Request: any;
declare var Response: any;
+declare module requestAnimationFrame {
+ declare var exports: (callback: any) => any;
+}
diff --git a/React/Base/RCTURLRequestDelegate.h b/React/Base/RCTURLRequestDelegate.h
index 48473b84b..f038d1d6a 100644
--- a/React/Base/RCTURLRequestDelegate.h
+++ b/React/Base/RCTURLRequestDelegate.h
@@ -16,10 +16,10 @@
@protocol RCTURLRequestDelegate
/**
- * Call this when you first receives a response from the server. This should
- * include response headers, etc.
+ * Call this when you send request data to the server. This is used to track
+ * upload progress, so should be called multiple times for large request bodies.
*/
-- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total;
+- (void)URLRequest:(id)requestToken didSendDataWithProgress:(int64_t)bytesSent;
/**
* Call this when you first receives a response from the server. This should
diff --git a/packager/launchPackager.command b/packager/launchPackager.command
index eb777493f..0537f7c84 100755
--- a/packager/launchPackager.command
+++ b/packager/launchPackager.command
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# Copyright (c) 2015-present, Facebook, Inc.
# All rights reserved.
diff --git a/packager/packager.sh b/packager/packager.sh
index f763b9bae..95cd8ce1e 100755
--- a/packager/packager.sh
+++ b/packager/packager.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# Copyright (c) 2015-present, Facebook, Inc.
# All rights reserved.