-
+
+
);
}
@@ -99,15 +88,15 @@ var styles = StyleSheet.create({
}
});
-var UIActivityIndicatorView = requireNativeComponent(
- 'UIActivityIndicatorView',
+var RCTActivityIndicatorView = requireNativeComponent(
+ 'RCTActivityIndicatorView',
null
);
if (__DEV__) {
var nativeOnlyProps = {activityIndicatorViewStyle: true};
verifyPropTypes(
ActivityIndicatorIOS,
- UIActivityIndicatorView.viewConfig,
+ RCTActivityIndicatorView.viewConfig,
nativeOnlyProps
);
}
diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js
index 2baaee21b416c6..b27e22d4b1e92c 100644
--- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js
+++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js
@@ -66,6 +66,9 @@ var TabBarItemIOS = React.createClass({
* blank content, you probably forgot to add a selected one.
*/
selected: React.PropTypes.bool,
+ /**
+ * React style object.
+ */
style: View.propTypes.style,
/**
* Text that appears under the icon. It is ignored when a system icon
diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js
index dfd3ab1a128469..9de1e15e92176f 100644
--- a/Libraries/Components/TextInput/TextInput.js
+++ b/Libraries/Components/TextInput/TextInput.js
@@ -445,6 +445,7 @@ var TextInput = React.createClass({
onSubmitEditing={this.props.onSubmitEditing}
onSelectionChangeShouldSetResponder={() => true}
placeholder={this.props.placeholder}
+ placeholderTextColor={this.props.placeholderTextColor}
text={this.state.bufferedValue}
autoCapitalize={autoCapitalize}
autoCorrect={this.props.autoCorrect}
diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js
index 0da57f554257e5..c7ca2ee261ab91 100644
--- a/Libraries/Components/View/View.js
+++ b/Libraries/Components/View/View.js
@@ -29,7 +29,7 @@ var stylePropType = StyleSheetPropType(ViewStylePropTypes);
* container that supports layout with flexbox, style, some touch handling, and
* accessibility controls, and is designed to be nested inside other views and
* to have 0 to many children of any type. `View` maps directly to the native
- * view equivalent on whatever platform react is running on, whether that is a
+ * view equivalent on whatever platform React is running on, whether that is a
* `UIView`, ``, `android.view`, etc. This example creates a `View` that
* wraps two colored boxes and custom component in a row with padding.
*
diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js
index bfc823f9f85928..959422bbc9888a 100644
--- a/Libraries/Components/WebView/WebView.android.js
+++ b/Libraries/Components/WebView/WebView.android.js
@@ -42,6 +42,7 @@ var WebView = React.createClass({
onNavigationStateChange: PropTypes.func,
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
style: View.propTypes.style,
+ javaScriptEnabledAndroid: PropTypes.bool,
/**
* Used to locate this view in end-to-end tests.
*/
@@ -90,6 +91,7 @@ var WebView = React.createClass({
key="webViewKey"
style={webViewStyles}
url={this.props.url}
+ javaScriptEnabledAndroid={this.props.javaScriptEnabledAndroid}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
onLoadingStart={this.onLoadingStart}
@@ -157,6 +159,7 @@ var WebView = React.createClass({
var RCTWebView = createReactIOSNativeComponentClass({
validAttributes: merge(ReactIOSViewAttributes.UIView, {
url: true,
+ javaScriptEnabledAndroid: true,
}),
uiViewClassName: 'RCTWebView',
});
diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js
index c4e4fbcd3299c8..ed2c98fae02f5a 100644
--- a/Libraries/Components/WebView/WebView.ios.js
+++ b/Libraries/Components/WebView/WebView.ios.js
@@ -92,6 +92,10 @@ var WebView = React.createClass({
onNavigationStateChange: PropTypes.func,
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
style: View.propTypes.style,
+ /**
+ * Used for android only, JS is enabled by default for WebView on iOS
+ */
+ javaScriptEnabledAndroid: PropTypes.bool,
},
getInitialState: function() {
diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js
index 13fe40a2364447..fae309aef5fe68 100644
--- a/Libraries/Geolocation/Geolocation.js
+++ b/Libraries/Geolocation/Geolocation.js
@@ -22,6 +22,12 @@ var subscriptions = [];
var updatesEnabled = false;
+type GeoOptions = {
+ timeout: number;
+ maximumAge: number;
+ enableHighAccuracy: bool;
+}
+
/**
* You need to include the `NSLocationWhenInUseUsageDescription` key
* in Info.plist to enable geolocation. Geolocation is enabled by default
@@ -32,10 +38,14 @@ var updatesEnabled = false;
*/
var Geolocation = {
+ /*
+ * Invokes the success callback once with the latest location info. Supported
+ * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool)
+ */
getCurrentPosition: function(
geo_success: Function,
geo_error?: Function,
- geo_options?: Object
+ geo_options?: GeoOptions
) {
invariant(
typeof geo_success === 'function',
@@ -48,7 +58,11 @@ var Geolocation = {
);
},
- watchPosition: function(success: Function, error?: Function, options?: Object): number {
+ /*
+ * Invokes the success callback whenever the location changes. Supported
+ * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool)
+ */
+ watchPosition: function(success: Function, error?: Function, options?: GeoOptions): number {
if (!updatesEnabled) {
RCTLocationObserver.startObserving(options || {});
updatesEnabled = true;
diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m
index 5d56caccbe314b..3e864657b82c5d 100644
--- a/Libraries/Geolocation/RCTLocationObserver.m
+++ b/Libraries/Geolocation/RCTLocationObserver.m
@@ -163,12 +163,12 @@ - (void)timeout:(NSTimer *)timer
#pragma mark - Public API
-RCT_EXPORT_METHOD(startObserving:(NSDictionary *)optionsJSON)
+RCT_EXPORT_METHOD(startObserving:(RCTLocationOptions)options)
{
[self checkLocationConfig];
// Select best options
- _observerOptions = [RCTConvert RCTLocationOptions:optionsJSON];
+ _observerOptions = options;
for (RCTLocationRequest *request in _pendingRequests) {
_observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy);
}
@@ -189,7 +189,7 @@ - (void)timeout:(NSTimer *)timer
}
}
-RCT_EXPORT_METHOD(getCurrentPosition:(NSDictionary *)optionsJSON
+RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options
withSuccessCallback:(RCTResponseSenderBlock)successBlock
errorCallback:(RCTResponseSenderBlock)errorBlock)
{
@@ -219,7 +219,6 @@ - (void)timeout:(NSTimer *)timer
}
// Check if previous recorded location exists and is good enough
- RCTLocationOptions options = [RCTConvert RCTLocationOptions:optionsJSON];
if (_lastLocationEvent &&
CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge &&
[_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) {
diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js
index e917b6b637355b..a41352f44ce4b0 100644
--- a/Libraries/Image/Image.ios.js
+++ b/Libraries/Image/Image.ios.js
@@ -31,7 +31,7 @@ var verifyPropTypes = require('verifyPropTypes');
var warning = require('warning');
/**
- * A react component for displaying different types of images,
+ * A React component for displaying different types of images,
* including network images, static resources, temporary local images, and
* images from local disk, such as the camera roll.
*
diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js
index 26f3c9ea32e999..632ff96567ee1c 100644
--- a/Libraries/Image/__tests__/resolveAssetSource-test.js
+++ b/Libraries/Image/__tests__/resolveAssetSource-test.js
@@ -32,6 +32,12 @@ describe('resolveAssetSource', () => {
expect(resolveAssetSource(source2)).toBe(source2);
});
+ it('ignores any weird data', () => {
+ expect(resolveAssetSource(null)).toBe(null);
+ expect(resolveAssetSource(42)).toBe(null);
+ expect(resolveAssetSource('nonsense')).toBe(null);
+ });
+
describe('bundle was loaded from network', () => {
beforeEach(() => {
SourceCode.scriptURL = 'http://10.0.0.1:8081/main.bundle';
diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js
index da136e9a73d583..02189155877006 100644
--- a/Libraries/Image/resolveAssetSource.js
+++ b/Libraries/Image/resolveAssetSource.js
@@ -43,10 +43,11 @@ function pickScale(scales, deviceScale) {
return scales[scales.length - 1] || 1;
}
-// TODO(frantic):
-// * Pick best scale and append @Nx to file path
-// * We are currently using httpServerLocation for both http and in-app bundle
function resolveAssetSource(source) {
+ if (!source || typeof source !== 'object') {
+ return null;
+ }
+
if (!source.__packager_asset) {
return source;
}
diff --git a/Libraries/ReactIOS/ReactIOSComponentMixin.js b/Libraries/ReactIOS/ReactIOSComponentMixin.js
index 94c708a433e863..03d7f5ad04fc3b 100644
--- a/Libraries/ReactIOS/ReactIOSComponentMixin.js
+++ b/Libraries/ReactIOS/ReactIOSComponentMixin.js
@@ -46,7 +46,7 @@ var ReactInstanceMap = require('ReactInstanceMap');
*
* `mountImage`: A way to represent the potential to create lower level
* resources whos `nodeHandle` can be discovered immediately by knowing the
- * `rootNodeID`. Today's web react represents this with `innerHTML` annotated
+ * `rootNodeID`. Today's web React represents this with `innerHTML` annotated
* with DOM ids that match the `rootNodeID`.
*
* Opaque name TodaysWebReact FutureWebWorkerReact ReactNative
diff --git a/Libraries/ReactIOS/ReactIOSMount.js b/Libraries/ReactIOS/ReactIOSMount.js
index ab46d6fe9d3144..9b1428fdd6d27a 100644
--- a/Libraries/ReactIOS/ReactIOSMount.js
+++ b/Libraries/ReactIOS/ReactIOSMount.js
@@ -191,7 +191,7 @@ var ReactIOSMount = {
* that has been rendered and unmounting it. There should just be one child
* component at this time.
*/
- unmountComponentAtNode: function(containerTag: number): bool {
+ unmountComponentAtNode: function(containerTag: number): boolean {
if (!ReactIOSTagHandles.reactTagIsNativeTopRootID(containerTag)) {
console.error('You cannot render into anything but a top root');
return false;
diff --git a/Libraries/ReactIOS/WarningBox.js b/Libraries/ReactIOS/WarningBox.js
new file mode 100644
index 00000000000000..37076ef5c299fe
--- /dev/null
+++ b/Libraries/ReactIOS/WarningBox.js
@@ -0,0 +1,398 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule WarningBox
+ */
+'use strict';
+
+var AsyncStorage = require('AsyncStorage');
+var EventEmitter = require('EventEmitter');
+var Map = require('Map');
+var PanResponder = require('PanResponder');
+var React = require('React');
+var StyleSheet = require('StyleSheet');
+var Text = require('Text');
+var TouchableOpacity = require('TouchableOpacity');
+var View = require('View');
+
+var invariant = require('invariant');
+var rebound = require('rebound');
+var stringifySafe = require('stringifySafe');
+
+var SCREEN_WIDTH = require('Dimensions').get('window').width;
+var IGNORED_WARNINGS_KEY = '__DEV_WARNINGS_IGNORED';
+
+var consoleWarn = console.warn.bind(console);
+
+var warningCounts = new Map();
+var ignoredWarnings: Array = [];
+var totalWarningCount = 0;
+var warningCountEvents = new EventEmitter();
+
+/**
+ * WarningBox renders warnings on top of the app being developed. Warnings help
+ * guard against subtle yet significant issues that can impact the quality of
+ * your application, such as accessibility and memory leaks. This "in your
+ * face" style of warning allows developers to notice and correct these issues
+ * as quickly as possible.
+ *
+ * The warning box is currently opt-in. Set the following flag to enable it:
+ *
+ * `console.yellowBoxEnabled = true;`
+ *
+ * If "ignore" is tapped on a warning, the WarningBox will record that warning
+ * and will not display it again. This is useful for hiding errors that already
+ * exist or have been introduced elsewhere. To re-enable all of the errors, set
+ * the following:
+ *
+ * `console.yellowBoxResetIgnored = true;`
+ *
+ * This can also be set permanently, and ignore will only silence the warnings
+ * until the next refresh.
+ */
+
+if (__DEV__) {
+ console.warn = function() {
+ consoleWarn.apply(null, arguments);
+ if (!console.yellowBoxEnabled) {
+ return;
+ }
+ var warning = Array.prototype.map.call(arguments, stringifySafe).join(' ');
+ if (!console.yellowBoxResetIgnored &&
+ ignoredWarnings.indexOf(warning) !== -1) {
+ return;
+ }
+ var count = warningCounts.has(warning) ? warningCounts.get(warning) + 1 : 1;
+ warningCounts.set(warning, count);
+ totalWarningCount += 1;
+ warningCountEvents.emit('count', totalWarningCount);
+ };
+}
+
+function saveIgnoredWarnings() {
+ AsyncStorage.setItem(
+ IGNORED_WARNINGS_KEY,
+ JSON.stringify(ignoredWarnings),
+ function(err) {
+ if (err) {
+ console.warn('Could not save ignored warnings.', err);
+ }
+ }
+ );
+}
+
+AsyncStorage.getItem(IGNORED_WARNINGS_KEY, function(err, data) {
+ if (!err && data && !console.yellowBoxResetIgnored) {
+ ignoredWarnings = JSON.parse(data);
+ }
+});
+
+var WarningRow = React.createClass({
+ componentWillMount: function() {
+ this.springSystem = new rebound.SpringSystem();
+ this.dismissalSpring = this.springSystem.createSpring();
+ this.dismissalSpring.setRestSpeedThreshold(0.05);
+ this.dismissalSpring.setCurrentValue(0);
+ this.dismissalSpring.addListener({
+ onSpringUpdate: () => {
+ var val = this.dismissalSpring.getCurrentValue();
+ this.text && this.text.setNativeProps({
+ left: SCREEN_WIDTH * val,
+ });
+ this.container && this.container.setNativeProps({
+ opacity: 1 - val,
+ });
+ this.closeButton && this.closeButton.setNativeProps({
+ opacity: 1 - (val * 5),
+ });
+ },
+ onSpringAtRest: () => {
+ if (this.dismissalSpring.getCurrentValue()) {
+ this.collapseSpring.setEndValue(1);
+ }
+ },
+ });
+ this.collapseSpring = this.springSystem.createSpring();
+ this.collapseSpring.setRestSpeedThreshold(0.05);
+ this.collapseSpring.setCurrentValue(0);
+ this.collapseSpring.getSpringConfig().friction = 20;
+ this.collapseSpring.getSpringConfig().tension = 200;
+ this.collapseSpring.addListener({
+ onSpringUpdate: () => {
+ var val = this.collapseSpring.getCurrentValue();
+ this.container && this.container.setNativeProps({
+ height: Math.abs(46 - (val * 46)),
+ });
+ },
+ onSpringAtRest: () => {
+ this.props.onDismissed();
+ },
+ });
+ this.panGesture = PanResponder.create({
+ onStartShouldSetPanResponder: () => {
+ return !! this.dismissalSpring.getCurrentValue();
+ },
+ onMoveShouldSetPanResponder: () => true,
+ onPanResponderGrant: () => {
+ this.isResponderOnlyToBlockTouches =
+ !! this.dismissalSpring.getCurrentValue();
+ },
+ onPanResponderMove: (e, gestureState) => {
+ if (this.isResponderOnlyToBlockTouches) {
+ return;
+ }
+ this.dismissalSpring.setCurrentValue(gestureState.dx / SCREEN_WIDTH);
+ },
+ onPanResponderRelease: (e, gestureState) => {
+ if (this.isResponderOnlyToBlockTouches) {
+ return;
+ }
+ var gestureCompletion = gestureState.dx / SCREEN_WIDTH;
+ var doesGestureRelease = (gestureState.vx + gestureCompletion) > 0.5;
+ this.dismissalSpring.setEndValue(doesGestureRelease ? 1 : 0);
+ }
+ });
+ },
+ render: function() {
+ var countText;
+ if (warningCounts.get(this.props.warning) > 1) {
+ countText = (
+
+ ({warningCounts.get(this.props.warning)}){" "}
+
+ );
+ }
+ return (
+ { this.container = container; }}
+ {...this.panGesture.panHandlers}>
+
+
+ { this.text = text; }}>
+ {countText}
+ {this.props.warning}
+
+
+
+ { this.closeButton = closeButton; }}
+ style={styles.closeButton}>
+ {
+ this.dismissalSpring.setEndValue(1);
+ }}>
+ ✕
+
+
+
+ );
+ }
+});
+
+var WarningBoxOpened = React.createClass({
+ render: function() {
+ var countText;
+ if (warningCounts.get(this.props.warning) > 1) {
+ countText = (
+
+ ({warningCounts.get(this.props.warning)}){" "}
+
+ );
+ }
+ return (
+
+
+
+ {countText}
+ {this.props.warning}
+
+
+
+
+
+ Dismiss
+
+
+
+
+
+
+ Ignore
+
+
+
+
+
+
+ );
+ },
+});
+
+var canMountWarningBox = true;
+
+var WarningBox = React.createClass({
+ getInitialState: function() {
+ return {
+ totalWarningCount,
+ openWarning: null,
+ };
+ },
+ componentWillMount: function() {
+ if (console.yellowBoxResetIgnored) {
+ AsyncStorage.setItem(IGNORED_WARNINGS_KEY, '[]', function(err) {
+ if (err) {
+ console.warn('Could not reset ignored warnings.', err);
+ }
+ });
+ ignoredWarnings = [];
+ }
+ },
+ componentDidMount: function() {
+ invariant(
+ canMountWarningBox,
+ 'There can only be one WarningBox'
+ );
+ canMountWarningBox = false;
+ warningCountEvents.addListener(
+ 'count',
+ this._onWarningCount
+ );
+ },
+ componentWillUnmount: function() {
+ warningCountEvents.removeAllListeners();
+ canMountWarningBox = true;
+ },
+ _onWarningCount: function(totalWarningCount) {
+ // Must use setImmediate because warnings often happen during render and
+ // state cannot be set while rendering
+ setImmediate(() => {
+ this.setState({ totalWarningCount, });
+ });
+ },
+ _onDismiss: function(warning) {
+ warningCounts.delete(warning);
+ this.setState({
+ openWarning: null,
+ });
+ },
+ render: function() {
+ if (warningCounts.size === 0) {
+ return ;
+ }
+ if (this.state.openWarning) {
+ return (
+ {
+ this.setState({ openWarning: null });
+ }}
+ onDismissed={this._onDismiss.bind(this, this.state.openWarning)}
+ onIgnored={() => {
+ ignoredWarnings.push(this.state.openWarning);
+ saveIgnoredWarnings();
+ this._onDismiss(this.state.openWarning);
+ }}
+ />
+ );
+ }
+ var warningRows = [];
+ warningCounts.forEach((count, warning) => {
+ warningRows.push(
+ {
+ this.setState({ openWarning: warning });
+ }}
+ onDismissed={this._onDismiss.bind(this, warning)}
+ warning={warning}
+ />
+ );
+ });
+ return (
+
+ {warningRows}
+
+ );
+ },
+});
+
+var styles = StyleSheet.create({
+ bold: {
+ fontWeight: 'bold',
+ },
+ closeButton: {
+ position: 'absolute',
+ right: 0,
+ height: 46,
+ width: 46,
+ },
+ closeButtonText: {
+ color: 'white',
+ fontSize: 32,
+ position: 'relative',
+ left: 8,
+ },
+ warningContainer: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0
+ },
+ warningBox: {
+ position: 'relative',
+ backgroundColor: 'rgba(171, 124, 36, 0.9)',
+ flex: 1,
+ height: 46,
+ },
+ warningText: {
+ color: 'white',
+ position: 'absolute',
+ left: 0,
+ marginLeft: 15,
+ marginRight: 46,
+ top: 7,
+ },
+ yellowBox: {
+ backgroundColor: 'rgba(171, 124, 36, 0.9)',
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ padding: 15,
+ paddingTop: 35,
+ },
+ yellowBoxText: {
+ color: 'white',
+ fontSize: 20,
+ },
+ yellowBoxButtons: {
+ flexDirection: 'row',
+ position: 'absolute',
+ bottom: 0,
+ },
+ yellowBoxButton: {
+ flex: 1,
+ padding: 25,
+ },
+ yellowBoxButtonText: {
+ color: 'white',
+ fontSize: 16,
+ }
+});
+
+module.exports = WarningBox;
diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js
index 084390ac50057d..16052c6fa69605 100644
--- a/Libraries/ReactIOS/renderApplication.ios.js
+++ b/Libraries/ReactIOS/renderApplication.ios.js
@@ -12,6 +12,9 @@
'use strict';
var React = require('React');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+var WarningBox = require('WarningBox');
var invariant = require('invariant');
@@ -24,12 +27,27 @@ function renderApplication(
rootTag,
'Expect to have a valid rootTag, instead got ', rootTag
);
+ var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled;
+ var warningBox = shouldRenderWarningBox ? : null;
React.render(
- ,
+
+
+ {warningBox}
+ ,
rootTag
);
}
+var styles = StyleSheet.create({
+ appContainer: {
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ },
+});
+
module.exports = renderApplication;
diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js
index ce7b4078e9acd4..a67abacb087ec5 100644
--- a/Libraries/Text/Text.js
+++ b/Libraries/Text/Text.js
@@ -33,7 +33,7 @@ var viewConfig = {
};
/**
- * A react component for displaying text which supports nesting,
+ * A React component for displaying text which supports nesting,
* styling, and touch handling. In the following example, the nested title and
* body text will inherit the `fontFamily` from `styles.baseText`, but the title
* provides its own additional styles. The title and body will stack on top of
diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js
index fe28a7684c9d46..b93000a33a8f50 100644
--- a/Libraries/Utilities/Dimensions.js
+++ b/Libraries/Utilities/Dimensions.js
@@ -20,7 +20,7 @@ var dimensions = NativeModules.UIManager.Dimensions;
// We calculate the window dimensions in JS so that we don't encounter loss of
// precision in transferring the dimensions (which could be non-integers) over
// the bridge.
-if (dimensions.windowPhysicalPixels) {
+if (dimensions && dimensions.windowPhysicalPixels) {
// parse/stringify => Clone hack
dimensions = JSON.parse(JSON.stringify(dimensions));
diff --git a/Libraries/Utilities/stringifySafe.js b/Libraries/Utilities/stringifySafe.js
new file mode 100644
index 00000000000000..053ea69849e821
--- /dev/null
+++ b/Libraries/Utilities/stringifySafe.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule stringifySafe
+ * @flow
+ */
+'use strict';
+
+/**
+ * Tries to stringify with JSON.stringify and toString, but catches exceptions
+ * (e.g. from circular objects) and always returns a string and never throws.
+ */
+function stringifySafe(arg: any): string {
+ var ret;
+ if (arg === undefined) {
+ ret = 'undefined';
+ } else if (arg === null) {
+ ret = 'null';
+ } else if (typeof arg === 'string') {
+ ret = '"' + arg + '"';
+ } else {
+ // Perform a try catch, just in case the object has a circular
+ // reference or stringify throws for some other reason.
+ try {
+ ret = JSON.stringify(arg);
+ } catch (e) {
+ if (typeof arg.toString === 'function') {
+ try {
+ ret = arg.toString();
+ } catch (E) {}
+ }
+ }
+ }
+ return ret || '["' + typeof arg + '" failed to stringify]';
+}
+
+module.exports = stringifySafe;
diff --git a/Libraries/vendor/react/platform/NodeHandle.js b/Libraries/vendor/react/platform/NodeHandle.js
index 19f74c6ece0b73..c7c93545bf0317 100644
--- a/Libraries/vendor/react/platform/NodeHandle.js
+++ b/Libraries/vendor/react/platform/NodeHandle.js
@@ -19,7 +19,7 @@
* worker thread.
*
* The only other requirement of a platform/environment is that it always be
- * possible to extract the react rootNodeID in a blocking manner (see
+ * possible to extract the React rootNodeID in a blocking manner (see
* `getRootNodeID`).
*
* +------------------+ +------------------+ +------------------+
diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m
index 48fd672a3926d4..7dc2322f0c0813 100644
--- a/React/Base/RCTBridge.m
+++ b/React/Base/RCTBridge.m
@@ -31,6 +31,8 @@
NSString *const RCTReloadNotification = @"RCTReloadNotification";
NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification";
+dispatch_queue_t const RCTJSThread = nil;
+
/**
* Must be kept in sync with `MessageQueue.js`.
*/
@@ -353,7 +355,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
#define RCT_CONVERT_CASE(_value, _type) \
case _value: { \
- _type (*convert)(id, SEL, id) = (typeof(convert))[RCTConvert methodForSelector:selector]; \
+ _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
@@ -375,12 +377,27 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
RCT_CONVERT_CASE('B', BOOL)
RCT_CONVERT_CASE('@', id)
RCT_CONVERT_CASE('^', void *)
- case '{':
- RCTAssert(NO, @"Argument %zd of %C[%@ %@] is defined as %@, however RCT_EXPORT_METHOD() "
- "does not currently support struct-type arguments.", i - 2,
- [reactMethodName characterAtIndex:0], _moduleClassName,
- objCMethodName, argumentName);
- break;
+
+ case '{': {
+ [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) {
+ NSUInteger size;
+ NSGetSizeAndAlignment(argumentType, &size, NULL);
+ void *returnValue = malloc(size);
+ NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector];
+ 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);
}
@@ -436,6 +453,10 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
RCT_SIMPLE_CASE('d', double, doubleValue)
RCT_SIMPLE_CASE('B', BOOL, boolValue)
+ case '{':
+ RCTLogMustFix(@"Cannot convert JSON to struct %s", argumentType);
+ break;
+
default:
defaultCase(argumentType);
}
@@ -795,6 +816,7 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL
_bundleURL = bundleURL;
_moduleProvider = block;
_launchOptions = [launchOptions copy];
+
[self setUp];
[self bindKeys];
}
@@ -872,6 +894,8 @@ - (void)setUp
dispatch_queue_t queue = [module methodQueue];
if (queue) {
_queuesByID[moduleID] = queue;
+ } else {
+ _queuesByID[moduleID] = [NSNull null];
}
}
}];
@@ -1128,6 +1152,16 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:
#pragma mark - Payload Generation
+- (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID
+{
+ id queue = _queuesByID[moduleID];
+ if (queue == [NSNull null]) {
+ [_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
+ } else {
+ dispatch_async(queue ?: _methodQueue, block);
+ }
+}
+
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context
{
#if BATCHED_BRIDGE
@@ -1235,10 +1269,9 @@ - (void)_handleBuffer:(id)buffer context:(NSNumber *)context
// TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case?
[_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, BOOL *stop) {
if ([module respondsToSelector:@selector(batchDidComplete)]) {
- dispatch_queue_t queue = _queuesByID[moduleID];
- dispatch_async(queue ?: _methodQueue, ^{
+ [self dispatchBlock:^{
[module batchDidComplete];
- });
+ } forModule:moduleID];
}
}];
}
@@ -1273,8 +1306,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
}
__weak RCTBridge *weakSelf = self;
- dispatch_queue_t queue = _queuesByID[moduleID];
- dispatch_async(queue ?: _methodQueue, ^{
+ [self dispatchBlock:^{
RCTProfileBeginEvent();
__strong RCTBridge *strongSelf = weakSelf;
@@ -1303,7 +1335,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
@"method": method.JSMethodName,
@"selector": NSStringFromSelector(method.selector),
});
- });
+ } forModule:@(moduleID)];
return YES;
}
diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h
index dd6f61e235e279..34b861ff3f8f10 100644
--- a/React/Base/RCTBridgeModule.h
+++ b/React/Base/RCTBridgeModule.h
@@ -17,6 +17,16 @@
*/
typedef void (^RCTResponseSenderBlock)(NSArray *response);
+/**
+ * This constant can be returned from +methodQueue to force module
+ * methods to be called on the JavaScript thread. This can have serious
+ * implications for performance, so only use this if you're sure it's what
+ * you need.
+ *
+ * NOTE: RCTJSThread is not a real libdispatch queue
+ */
+extern const dispatch_queue_t RCTJSThread;
+
/**
* Provides the interface needed to register a bridge module.
*/
diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m
index f1ed77298dafdb..eacb03b857a91d 100644
--- a/React/Base/RCTConvert.m
+++ b/React/Base/RCTConvert.m
@@ -49,11 +49,11 @@ + (NSNumber *)NSNumber:(id)json
});
NSNumber *number = [formatter numberFromString:json];
if (!number) {
- RCTLogError(@"JSON String '%@' could not be interpreted as a number", json);
+ RCTLogConvertError(json, "a number");
}
return number;
} else if (json && json != [NSNull null]) {
- RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]);
+ RCTLogConvertError(json, "a number");
}
return nil;
}
@@ -66,30 +66,38 @@ + (NSData *)NSData:(id)json
+ (NSURL *)NSURL:(id)json
{
- if (!json || json == (id)kCFNull) {
+ NSString *path = [self NSString:json];
+ if (!path.length) {
return nil;
}
- if (![json isKindOfClass:[NSString class]]) {
- RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json);
- return nil;
- }
+ @try { // NSURL has a history of crashing with bad input, so let's be safe
- NSString *path = json;
- if ([path isAbsolutePath])
- {
+ NSURL *URL = [NSURL URLWithString:path];
+ if (URL.scheme) { // Was a well-formed absolute URL
+ return URL;
+ }
+
+ // Check if it has a scheme
+ if ([path rangeOfString:@"[a-zA-Z][a-zA-Z._-]+:" options:NSRegularExpressionSearch].location == 0) {
+ path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ URL = [NSURL URLWithString:path];
+ if (URL) {
+ return URL;
+ }
+ }
+
+ // Assume that it's a local path
+ path = [path stringByRemovingPercentEncoding];
+ if (![path isAbsolutePath]) {
+ path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path];
+ }
return [NSURL fileURLWithPath:path];
}
- else if ([path length])
- {
- NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]];
- if ([URL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) {
- RCTLogWarn(@"The file '%@' does not exist", URL);
- return nil;
- }
- return URL;
+ @catch (__unused NSException *e) {
+ RCTLogConvertError(json, "a valid URL");
+ return nil;
}
- return nil;
}
+ (NSURLRequest *)NSURLRequest:(id)json
@@ -112,11 +120,12 @@ + (NSDate *)NSDate:(id)json
});
NSDate *date = [formatter dateFromString:json];
if (!date) {
- RCTLogError(@"JSON String '%@' could not be interpreted as a date. Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json);
+ RCTLogError(@"JSON String '%@' could not be interpreted as a date. "
+ "Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json);
}
return date;
} else if (json && json != [NSNull null]) {
- RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]);
+ RCTLogConvertError(json, "a date");
}
return nil;
}
diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h
index dd6bd8ed6f8aa2..15cb180210f6e6 100644
--- a/React/Base/RCTEventDispatcher.h
+++ b/React/Base/RCTEventDispatcher.h
@@ -50,7 +50,7 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
/**
* Send a user input event. The body dictionary must contain a "target"
- * parameter, representing the react tag of the view sending the event
+ * parameter, representing the React tag of the view sending the event
*/
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body;
diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m
index fb4e02eae30f75..8487556e576a53 100644
--- a/React/Base/RCTEventDispatcher.m
+++ b/React/Base/RCTEventDispatcher.m
@@ -44,7 +44,7 @@ - (void)sendDeviceEventWithName:(NSString *)name body:(id)body
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body
{
RCTAssert([body[@"target"] isKindOfClass:[NSNumber class]],
- @"Event body dictionary must include a 'target' property containing a react tag");
+ @"Event body dictionary must include a 'target' property containing a React tag");
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent"
args:body ? @[body[@"target"], name, body] : @[body[@"target"], name]];
diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m
index dd8fab461868fd..2e7d21b9442fe7 100755
--- a/React/Base/RCTJavaScriptLoader.m
+++ b/React/Base/RCTJavaScriptLoader.m
@@ -10,47 +10,15 @@
#import "RCTJavaScriptLoader.h"
#import "RCTBridge.h"
-#import "RCTInvalidating.h"
-#import "RCTLog.h"
-#import "RCTRedBox.h"
+#import "RCTConvert.h"
#import "RCTSourceCode.h"
#import "RCTUtils.h"
-#define NO_REMOTE_MODULE @"Could not fetch module bundle %@. Ensure node server is running.\n\nIf it timed out, try reloading."
-#define NO_LOCAL_BUNDLE @"Could not load local bundle %@. Ensure file exists."
-
-#define CACHE_DIR @"RCTJSBundleCache"
-
-#pragma mark - Application Engine
-
-/**
- * TODO:
- * - Add window resize rotation events matching the DOM API.
- * - Device pixel ration hooks.
- * - Source maps.
- */
@implementation RCTJavaScriptLoader
{
__weak RCTBridge *_bridge;
}
-/**
- * `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore
- * engine in its own dedicated thread.
- *
- * TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one
- * additional GCD dispatch per frame and likely makes it so that other UIThread
- * operations don't delay the dispatch (so we can begin working in JS much
- * faster.) Event handling must still be sent via a GCD dispatch, of course.
- *
- * We must add the display link to two runloops in order to get setTimeouts to
- * fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`)
- * TODO: We can invent a `requestAnimationFrame` and
- * `requestAvailableAnimationFrame` to control if callbacks can be fired during
- * an animation.
- * http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink
- *
- */
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super init])) {
@@ -61,92 +29,86 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete
{
- if (scriptURL == nil) {
+ // Sanitize the script URL
+ scriptURL = [RCTConvert NSURL:scriptURL.absoluteString];
+
+ if (!scriptURL ||
+ ([scriptURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) {
NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{
- NSLocalizedDescriptionKey: @"No script URL provided"
+ NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided"
}];
onComplete(error);
return;
}
- if ([scriptURL isFileURL]) {
- NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath];
- NSString *localPath = [scriptURL.absoluteString substringFromIndex:@"file://".length];
-
- if (![localPath hasPrefix:bundlePath]) {
- NSString *absolutePath = [NSString stringWithFormat:@"%@/%@", bundlePath, localPath];
- scriptURL = [NSURL fileURLWithPath:absolutePath];
- }
- }
-
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
- // Handle general request errors
- if (error) {
- if ([[error domain] isEqualToString:NSURLErrorDomain]) {
- NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]];
- NSDictionary *userInfo = @{
- NSLocalizedDescriptionKey: desc,
- NSLocalizedFailureReasonErrorKey: [error localizedDescription],
- NSUnderlyingErrorKey: error,
- };
- error = [NSError errorWithDomain:@"JSServer"
- code:error.code
- userInfo:userInfo];
- }
- onComplete(error);
- return;
- }
-
- // Parse response as text
- NSStringEncoding encoding = NSUTF8StringEncoding;
- if (response.textEncodingName != nil) {
- CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
- if (cfEncoding != kCFStringEncodingInvalidId) {
- encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
- }
- }
- NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
-
- // Handle HTTP errors
- if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
- NSDictionary *userInfo;
- NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
- if ([errorDetails isKindOfClass:[NSDictionary class]] &&
- [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
- NSMutableArray *fakeStack = [[NSMutableArray alloc] init];
- for (NSDictionary *err in errorDetails[@"errors"]) {
- [fakeStack addObject: @{
- @"methodName": err[@"description"] ?: @"",
- @"file": err[@"filename"] ?: @"",
- @"lineNumber": err[@"lineNumber"] ?: @0
- }];
- }
- userInfo = @{
- NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
- @"stack": fakeStack,
- };
- } else {
- userInfo = @{NSLocalizedDescriptionKey: rawText};
- }
- error = [NSError errorWithDomain:@"JSServer"
- code:[(NSHTTPURLResponse *)response statusCode]
- userInfo:userInfo];
-
- onComplete(error);
- return;
- }
- RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
- sourceCodeModule.scriptURL = scriptURL;
- sourceCodeModule.scriptText = rawText;
+ // Handle general request errors
+ if (error) {
+ if ([[error domain] isEqualToString:NSURLErrorDomain]) {
+ NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]];
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: desc,
+ NSLocalizedFailureReasonErrorKey: [error localizedDescription],
+ NSUnderlyingErrorKey: error,
+ };
+ error = [NSError errorWithDomain:@"JSServer"
+ code:error.code
+ userInfo:userInfo];
+ }
+ onComplete(error);
+ return;
+ }
- [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
- dispatch_async(dispatch_get_main_queue(), ^{
- onComplete(scriptError);
- });
- }];
- }];
+ // Parse response as text
+ NSStringEncoding encoding = NSUTF8StringEncoding;
+ if (response.textEncodingName != nil) {
+ CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
+ if (cfEncoding != kCFStringEncodingInvalidId) {
+ encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
+ }
+ }
+ NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
+
+ // Handle HTTP errors
+ if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
+ NSDictionary *userInfo;
+ NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
+ if ([errorDetails isKindOfClass:[NSDictionary class]] &&
+ [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
+ NSMutableArray *fakeStack = [[NSMutableArray alloc] init];
+ for (NSDictionary *err in errorDetails[@"errors"]) {
+ [fakeStack addObject: @{
+ @"methodName": err[@"description"] ?: @"",
+ @"file": err[@"filename"] ?: @"",
+ @"lineNumber": err[@"lineNumber"] ?: @0
+ }];
+ }
+ userInfo = @{
+ NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
+ @"stack": fakeStack,
+ };
+ } else {
+ userInfo = @{NSLocalizedDescriptionKey: rawText};
+ }
+ error = [NSError errorWithDomain:@"JSServer"
+ code:[(NSHTTPURLResponse *)response statusCode]
+ userInfo:userInfo];
+
+ onComplete(error);
+ return;
+ }
+ RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
+ sourceCodeModule.scriptURL = scriptURL;
+ sourceCodeModule.scriptText = rawText;
+
+ [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ onComplete(scriptError);
+ });
+ }];
+ }];
[task resume];
}
diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m
index 45624efdcc1e8a..54556d41810ba4 100644
--- a/React/Base/RCTRootView.m
+++ b/React/Base/RCTRootView.m
@@ -23,16 +23,6 @@
#import "RCTWebViewExecutor.h"
#import "UIView+React.h"
-/**
- * HACK(t6568049) This should be removed soon, hiding to prevent people from
- * relying on it
- */
-@interface RCTBridge (RCTRootView)
-
-- (void)setJavaScriptExecutor:(id)executor;
-
-@end
-
@interface RCTUIManager (RCTRootView)
- (NSNumber *)allocateRootTag;
@@ -120,11 +110,11 @@ - (void)bundleFinishedLoading
dispatch_async(dispatch_get_main_queue(), ^{
/**
- * Every root view that is created must have a unique react tag.
+ * Every root view that is created must have a unique React tag.
* Numbering of these tags goes from 1, 11, 21, 31, etc
*
* NOTE: Since the bridge persists, the RootViews might be reused, so now
- * the react tag is assigned every time we load new content.
+ * the React tag is assigned every time we load new content.
*/
[_contentView removeFromSuperview];
_contentView = [[UIView alloc] initWithFrame:self.bounds];
diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m
index bd731a99844715..7af26da74eab70 100644
--- a/React/Base/RCTTouchHandler.m
+++ b/React/Base/RCTTouchHandler.m
@@ -26,7 +26,7 @@ @implementation RCTTouchHandler
/**
* Arrays managed in parallel tracking native touch object along with the
- * native view that was touched, and the react touch data dictionary.
+ * native view that was touched, and the React touch data dictionary.
* This must be kept track of because `UIKit` destroys the touch targets
* if touches are canceled and we have no other way to recover this information.
*/
diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h
index 812a651222a85c..1c04125684fd11 100644
--- a/React/Base/RCTUtils.h
+++ b/React/Base/RCTUtils.h
@@ -46,3 +46,6 @@ RCT_EXTERN BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector);
// TODO(#6472857): create NSErrors and automatically convert them over the bridge.
RCT_EXTERN NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData);
RCT_EXTERN NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData);
+
+// Returns YES if React is running in a test environment
+RCT_EXTERN BOOL RCTRunningInTestEnvironment(void);
diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m
index cea45c324c63e7..0b4a3873c8c0cc 100644
--- a/React/Base/RCTUtils.m
+++ b/React/Base/RCTUtils.m
@@ -201,3 +201,13 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
RCTLogError(@"\nError: %@", error);
return error;
}
+
+BOOL RCTRunningInTestEnvironment(void)
+{
+ static BOOL _isTestEnvironment = NO;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _isTestEnvironment = (NSClassFromString(@"SenTestCase") != nil || NSClassFromString(@"XCTest") != nil);
+ });
+ return _isTestEnvironment;
+}
diff --git a/React/Executors/RCTContextExecutor.h b/React/Executors/RCTContextExecutor.h
index 159965a2fbed01..a41fcf31419a43 100644
--- a/React/Executors/RCTContextExecutor.h
+++ b/React/Executors/RCTContextExecutor.h
@@ -23,6 +23,6 @@
* You probably don't want to use this; use -init instead.
*/
- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread
- globalContextRef:(JSGlobalContextRef)context;
+ globalContextRef:(JSGlobalContextRef)context NS_DESIGNATED_INITIALIZER;
@end
diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m
index 86444dd2a7f903..f5089a9d6a76ca 100644
--- a/React/Executors/RCTContextExecutor.m
+++ b/React/Executors/RCTContextExecutor.m
@@ -55,6 +55,11 @@ - (void)invalidate
}
}
+- (void)dealloc
+{
+ CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]);
+}
+
@end
@implementation RCTContextExecutor
@@ -74,12 +79,12 @@ @implementation RCTContextExecutor
static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception)
{
if (argumentCount > 0) {
- JSStringRef string = JSValueToStringCopy(context, arguments[0], exception);
- if (!string) {
+ JSStringRef messageRef = JSValueToStringCopy(context, arguments[0], exception);
+ if (!messageRef) {
return JSValueMakeUndefined(context);
}
- NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string);
- JSStringRelease(string);
+ NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, messageRef);
+ JSStringRelease(messageRef);
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:
@"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)"
options:NSRegularExpressionCaseInsensitive
@@ -89,14 +94,11 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object,
range:(NSRange){0, message.length}
withTemplate:@"[$4$5] \t$2"];
- // TODO: it would be good if log level was sent as a param, instead of this hack
RCTLogLevel level = RCTLogLevelInfo;
- if ([message rangeOfString:@"error" options:NSCaseInsensitiveSearch].length) {
- level = RCTLogLevelError;
- } else if ([message rangeOfString:@"warning" options:NSCaseInsensitiveSearch].length) {
- level = RCTLogLevelWarning;
+ if (argumentCount > 1) {
+ level = MAX(level, JSValueToNumber(context, arguments[1], exception) - 1);
}
- _RCTLogFormat(level, NULL, -1, @"%@", message);
+ RCTGetLogFunction()(level, nil, nil, message);
}
return JSValueMakeUndefined(context);
@@ -156,15 +158,12 @@ + (void)runRunLoopThread
- (instancetype)init
{
- static NSThread *javaScriptThread;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- // All JS is single threaded, so a serial queue is our only option.
- javaScriptThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoopThread) object:nil];
- [javaScriptThread setName:@"com.facebook.React.JavaScript"];
- [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]];
- [javaScriptThread start];
- });
+ NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class]
+ selector:@selector(runRunLoopThread)
+ object:nil];
+ [javaScriptThread setName:@"com.facebook.React.JavaScript"];
+ [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]];
+ [javaScriptThread start];
return [self initWithJavaScriptThread:javaScriptThread globalContextRef:NULL];
}
@@ -172,6 +171,9 @@ - (instancetype)init
- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread
globalContextRef:(JSGlobalContextRef)context
{
+ RCTAssert(javaScriptThread != nil,
+ @"Can't initialize RCTContextExecutor without a javaScriptThread");
+
if ((self = [super init])) {
_javaScriptThread = javaScriptThread;
__weak RCTContextExecutor *weakSelf = self;
@@ -305,17 +307,22 @@ - (void)executeApplicationScript:(NSString *)script
}
onComplete(error);
}
- }), @"js_call", (@{ @"url": sourceURL }))];
+ }), @"js_call", (@{ @"url": sourceURL.absoluteString }))];
}
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
- if ([NSThread currentThread] != _javaScriptThread) {
- [self performSelector:@selector(executeBlockOnJavaScriptQueue:)
- onThread:_javaScriptThread withObject:block waitUntilDone:NO];
- } else {
- block();
- }
+ /**
+ * Always dispatch async, ensure there are no sync calls on the JS thread
+ * otherwise timers can cause a deadlock
+ */
+ [self performSelector:@selector(_runBlock:)
+ onThread:_javaScriptThread withObject:block waitUntilDone:NO];
+}
+
+- (void)_runBlock:(dispatch_block_t)block
+{
+ block();
}
- (void)injectJSONText:(NSString *)script
diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m
index 1d99c1a2d42663..e2df5befca18eb 100644
--- a/React/Modules/RCTTiming.m
+++ b/React/Modules/RCTTiming.m
@@ -110,7 +110,7 @@ - (void)dealloc
- (dispatch_queue_t)methodQueue
{
- return dispatch_get_main_queue();
+ return RCTJSThread;
}
- (BOOL)isValid
@@ -131,8 +131,6 @@ - (void)stopTimers
- (void)startTimers
{
- RCTAssertMainThread();
-
if (![self isValid] || _timers.count == 0) {
return;
}
diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m
index 451a343d04960d..496b9c3513a677 100644
--- a/React/Modules/RCTUIManager.m
+++ b/React/Modules/RCTUIManager.m
@@ -661,7 +661,7 @@ - (void)_manageChildren:(NSNumber *)containerReactTag
{
id container = registry[containerReactTag];
RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count);
- RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one react child to add");
+ RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add");
// Removes (both permanent and temporary moves) are using "before" indices
NSArray *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
@@ -918,7 +918,7 @@ - (void)flushUIBlocks
}
// TODO: this doesn't work because sometimes view is inside a modal window
- // RCTAssert([rootView isReactRootView], @"React view is not inside a react root view");
+ // RCTAssert([rootView isReactRootView], @"React view is not inside a React root view");
// By convention, all coordinates, whether they be touch coordinates, or
// measurement coordinates are with respect to the root view.
@@ -995,18 +995,17 @@ static void RCTMeasureLayout(RCTShadowView *view,
}
/**
- * Returns an array of computed offset layouts in a dictionary form. The layouts are of any react subviews
+ * Returns an array of computed offset layouts in a dictionary form. The layouts are of any React subviews
* that are immediate descendants to the parent view found within a specified rect. The dictionary result
* contains left, top, width, height and an index. The index specifies the position among the other subviews.
* Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the
* passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts.
*/
-RCT_EXPORT_METHOD(measureViewsInRect:(id)rectJSON
+RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect
parentView:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback)
{
- CGRect rect = [RCTConvert CGRect:rectJSON];
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
if (!shadowView) {
RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag);
@@ -1102,9 +1101,8 @@ static void RCTMeasureLayout(RCTShadowView *view,
}
RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag
- withRect:(id)rectJSON)
+ withRect:(CGRect)rect)
{
- CGRect rect = [RCTConvert CGRect:rectJSON];
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
UIView *view = viewRegistry[reactTag];
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj
index 3364cce76fbfe8..fce2aae428aa55 100644
--- a/React/React.xcodeproj/project.pbxproj
+++ b/React/React.xcodeproj/project.pbxproj
@@ -33,7 +33,7 @@
13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080131A69489C00A75B9A /* RCTNavItemManager.m */; };
13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080151A69489C00A75B9A /* RCTTextField.m */; };
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; };
- 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */; };
+ 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */; };
13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; };
13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; };
13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; };
@@ -136,8 +136,8 @@
13B080151A69489C00A75B9A /* RCTTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextField.m; sourceTree = ""; };
13B080161A69489C00A75B9A /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = ""; };
13B080171A69489C00A75B9A /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = ""; };
- 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIActivityIndicatorViewManager.h; sourceTree = ""; };
- 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIActivityIndicatorViewManager.m; sourceTree = ""; };
+ 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorViewManager.h; sourceTree = ""; };
+ 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorViewManager.m; sourceTree = ""; };
13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; };
13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; };
13C156011AB1A2840079392D /* RCTWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebView.h; sourceTree = ""; };
@@ -317,8 +317,8 @@
13B080151A69489C00A75B9A /* RCTTextField.m */,
13B080161A69489C00A75B9A /* RCTTextFieldManager.h */,
13B080171A69489C00A75B9A /* RCTTextFieldManager.m */,
- 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */,
- 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */,
+ 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */,
+ 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */,
13E0674F1A70F44B002CDEE1 /* RCTView.h */,
13E067501A70F44B002CDEE1 /* RCTView.m */,
13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */,
@@ -499,7 +499,7 @@
14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */,
14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */,
14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */,
- 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */,
+ 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */,
13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */,
58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */,
13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */,
diff --git a/React/Views/RCTUIActivityIndicatorViewManager.h b/React/Views/RCTActivityIndicatorViewManager.h
similarity index 64%
rename from React/Views/RCTUIActivityIndicatorViewManager.h
rename to React/Views/RCTActivityIndicatorViewManager.h
index e5a10fdd75c746..cbd6816ae4cddf 100644
--- a/React/Views/RCTUIActivityIndicatorViewManager.h
+++ b/React/Views/RCTActivityIndicatorViewManager.h
@@ -9,6 +9,12 @@
#import "RCTViewManager.h"
-@interface RCTUIActivityIndicatorViewManager : RCTViewManager
+@interface RCTConvert (UIActivityIndicatorView)
+
++ (UIActivityIndicatorViewStyle)UIActivityIndicatorViewStyle:(id)json;
+
+@end
+
+@interface RCTActivityIndicatorViewManager : RCTViewManager
@end
diff --git a/React/Views/RCTUIActivityIndicatorViewManager.m b/React/Views/RCTActivityIndicatorViewManager.m
similarity index 52%
rename from React/Views/RCTUIActivityIndicatorViewManager.m
rename to React/Views/RCTActivityIndicatorViewManager.m
index e2c9b3d353bf06..3876400dff3714 100644
--- a/React/Views/RCTUIActivityIndicatorViewManager.m
+++ b/React/Views/RCTActivityIndicatorViewManager.m
@@ -7,35 +7,37 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
-#import "RCTUIActivityIndicatorViewManager.h"
+#import "RCTActivityIndicatorViewManager.h"
#import "RCTConvert.h"
@implementation RCTConvert (UIActivityIndicatorView)
+// NOTE: It's pointless to support UIActivityIndicatorViewStyleGray
+// as we can set the color to any arbitrary value that we want to
+
RCT_ENUM_CONVERTER(UIActivityIndicatorViewStyle, (@{
- @"white-large": @(UIActivityIndicatorViewStyleWhiteLarge),
- @"large-white": @(UIActivityIndicatorViewStyleWhiteLarge),
- @"white": @(UIActivityIndicatorViewStyleWhite),
- @"gray": @(UIActivityIndicatorViewStyleGray),
+ @"large": @(UIActivityIndicatorViewStyleWhiteLarge),
+ @"small": @(UIActivityIndicatorViewStyleWhite),
}), UIActivityIndicatorViewStyleWhiteLarge, integerValue)
@end
-@implementation RCTUIActivityIndicatorViewManager
+@implementation RCTActivityIndicatorViewManager
-RCT_EXPORT_MODULE(UIActivityIndicatorViewManager)
+RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[UIActivityIndicatorView alloc] init];
}
-RCT_EXPORT_VIEW_PROPERTY(activityIndicatorViewStyle, UIActivityIndicatorViewStyle)
RCT_EXPORT_VIEW_PROPERTY(color, UIColor)
+RCT_EXPORT_VIEW_PROPERTY(hidesWhenStopped, BOOL)
+RCT_REMAP_VIEW_PROPERTY(size, activityIndicatorViewStyle, UIActivityIndicatorViewStyle)
RCT_CUSTOM_VIEW_PROPERTY(animating, BOOL, UIActivityIndicatorView)
{
- BOOL animating = json ? [json boolValue] : [defaultView isAnimating];
+ BOOL animating = json ? [RCTConvert BOOL:json] : [defaultView isAnimating];
if (animating != [view isAnimating]) {
if (animating) {
[view startAnimating];
@@ -45,14 +47,4 @@ - (UIView *)view
}
}
-- (NSDictionary *)constantsToExport
-{
- return
- @{
- @"StyleWhite": @(UIActivityIndicatorViewStyleWhite),
- @"StyleWhiteLarge": @(UIActivityIndicatorViewStyleWhiteLarge),
- @"StyleGray": @(UIActivityIndicatorViewStyleGray),
- };
-}
-
@end
diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m
index f3ebb6554a2cb8..a4cb338fb3c88b 100644
--- a/React/Views/RCTNavigator.m
+++ b/React/Views/RCTNavigator.m
@@ -60,7 +60,7 @@ @interface RCTNavigationController : UINavigationController 0 && self.placeholderTextColor) {
+ self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder
+ attributes:@{
+ NSForegroundColorAttributeName : self.placeholderTextColor
+ }];
+ } else if (self.placeholder.length) {
+ self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder];
+ }
+}
+
+- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor {
+ _placeholderTextColor = placeholderTextColor;
+ RCTUpdatePlaceholder(self);
+}
+
+- (void)setPlaceholder:(NSString *)placeholder {
+ super.placeholder = placeholder;
+ RCTUpdatePlaceholder(self);
+}
+
- (NSArray *)reactSubviews
{
// TODO: do we support subviews of textfield in React?
diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m
index 6e78d86a3b1c39..ff401a719c7c7a 100644
--- a/React/Views/RCTTextFieldManager.m
+++ b/React/Views/RCTTextFieldManager.m
@@ -10,7 +10,6 @@
#import "RCTTextFieldManager.h"
#import "RCTBridge.h"
-#import "RCTConvert.h"
#import "RCTShadowView.h"
#import "RCTSparseArray.h"
#import "RCTTextField.h"
@@ -28,6 +27,7 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL)
RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
+RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode)
RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL)
diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h
index 73fe2c7cbb0338..1a4bcb40007e75 100644
--- a/React/Views/RCTView.h
+++ b/React/Views/RCTView.h
@@ -13,13 +13,6 @@
#import "RCTPointerEvents.h"
-typedef NS_ENUM(NSInteger, RCTBorderSide) {
- RCTBorderSideTop,
- RCTBorderSideRight,
- RCTBorderSideBottom,
- RCTBorderSideLeft
-};
-
@protocol RCTAutoInsetsProtocol;
@interface RCTView : UIView
diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m
index d40798302b2a94..c0786b5abfa863 100644
--- a/React/Views/RCTView.m
+++ b/React/Views/RCTView.m
@@ -12,9 +12,10 @@
#import "RCTAutoInsetsProtocol.h"
#import "RCTConvert.h"
#import "RCTLog.h"
+#import "RCTUtils.h"
#import "UIView+React.h"
-static const RCTBorderSide RCTBorderSideCount = 4;
+static void *RCTViewCornerRadiusKVOContext = &RCTViewCornerRadiusKVOContext;
static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event)
{
@@ -30,6 +31,10 @@
return nil;
}
+static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]);
+static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform);
+static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise);
+
@implementation UIView (RCTViewUnmounting)
- (void)react_remountAllSubviews
@@ -107,8 +112,39 @@ - (UIView *)react_findClipView
@implementation RCTView
{
NSMutableArray *_reactSubviews;
- CAShapeLayer *_borderLayers[RCTBorderSideCount];
- CGFloat _borderWidths[RCTBorderSideCount];
+ UIColor *_backgroundColor;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ if ((self = [super initWithFrame:frame])) {
+ _borderWidth = -1;
+ _borderTopWidth = -1;
+ _borderRightWidth = -1;
+ _borderBottomWidth = -1;
+ _borderLeftWidth = -1;
+
+ _backgroundColor = [super backgroundColor];
+ [super setBackgroundColor:[UIColor clearColor]];
+
+ [self.layer addObserver:self forKeyPath:@"cornerRadius" options:0 context:RCTViewCornerRadiusKVOContext];
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [self.layer removeObserver:self forKeyPath:@"cornerRadius" context:RCTViewCornerRadiusKVOContext];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
+{
+ if (context == RCTViewCornerRadiusKVOContext) {
+ [self.layer setNeedsDisplay];
+ } else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
}
- (NSString *)accessibilityLabel
@@ -381,189 +417,353 @@ - (void)layoutSubviews
if (_reactSubviews) {
[self updateClippedSubviews];
}
-
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- if (_borderLayers[side]) [self updatePathForShapeLayerForSide:side];
- }
}
-- (void)layoutSublayersOfLayer:(CALayer *)layer
+#pragma mark - Borders
+
+- (UIColor *)backgroundColor
{
- [super layoutSublayersOfLayer:layer];
+ return _backgroundColor;
+}
- const CGRect bounds = layer.bounds;
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- _borderLayers[side].frame = bounds;
+- (void)setBackgroundColor:(UIColor *)backgroundColor
+{
+ if ([_backgroundColor isEqual:backgroundColor]) {
+ return;
}
+ _backgroundColor = backgroundColor;
+ [self.layer setNeedsDisplay];
}
-- (BOOL)getTrapezoidPoints:(CGPoint[4])outPoints forSide:(RCTBorderSide)side
+- (UIImage *)generateBorderImage:(out CGRect *)contentsCenter
{
- const CGRect bounds = self.layer.bounds;
- const CGFloat minX = CGRectGetMinX(bounds);
- const CGFloat maxX = CGRectGetMaxX(bounds);
- const CGFloat minY = CGRectGetMinY(bounds);
- const CGFloat maxY = CGRectGetMaxY(bounds);
+ const CGFloat maxRadius = MIN(self.bounds.size.height, self.bounds.size.width) / 2.0;
+ const CGFloat radius = MAX(0, MIN(self.layer.cornerRadius, maxRadius));
-#define BW(SIDE) [self borderWidthForSide:RCTBorderSide##SIDE]
+ const CGFloat borderWidth = MAX(0, _borderWidth);
+ const CGFloat topWidth = _borderTopWidth >= 0 ? _borderTopWidth : borderWidth;
+ const CGFloat rightWidth = _borderRightWidth >= 0 ? _borderRightWidth : borderWidth;
+ const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth;
+ const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth;
- switch (side) {
- case RCTBorderSideRight:
- outPoints[0] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom));
- outPoints[1] = CGPointMake(maxX - BW(Right), minY + BW(Top));
- outPoints[2] = CGPointMake(maxX, minY);
- outPoints[3] = CGPointMake(maxX, maxY);
- break;
- case RCTBorderSideBottom:
- outPoints[0] = CGPointMake(minX + BW(Left), maxY - BW(Bottom));
- outPoints[1] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom));
- outPoints[2] = CGPointMake(maxX, maxY);
- outPoints[3] = CGPointMake(minX, maxY);
- break;
- case RCTBorderSideLeft:
- outPoints[0] = CGPointMake(minX + BW(Left), minY + BW(Top));
- outPoints[1] = CGPointMake(minX + BW(Left), maxY - BW(Bottom));
- outPoints[2] = CGPointMake(minX, maxY);
- outPoints[3] = CGPointMake(minX, minY);
- break;
- case RCTBorderSideTop:
- outPoints[0] = CGPointMake(maxX - BW(Right), minY + BW(Top));
- outPoints[1] = CGPointMake(minX + BW(Left), minY + BW(Top));
- outPoints[2] = CGPointMake(minX, minY);
- outPoints[3] = CGPointMake(maxX, minY);
- break;
- }
+ const CGFloat topRadius = MAX(0, radius - topWidth);
+ const CGFloat rightRadius = MAX(0, radius - rightWidth);
+ const CGFloat bottomRadius = MAX(0, radius - bottomWidth);
+ const CGFloat leftRadius = MAX(0, radius - leftWidth);
- return YES;
-}
+ const UIEdgeInsets edgeInsets = UIEdgeInsetsMake(topWidth + topRadius, leftWidth + leftRadius, bottomWidth + bottomRadius, rightWidth + rightRadius);
+ const CGSize size = CGSizeMake(edgeInsets.left + 1 + edgeInsets.right, edgeInsets.top + 1 + edgeInsets.bottom);
-- (CAShapeLayer *)createShapeLayerIfNotExistsForSide:(RCTBorderSide)side
-{
- CAShapeLayer *borderLayer = _borderLayers[side];
- if (!borderLayer) {
- borderLayer = [CAShapeLayer layer];
- borderLayer.fillColor = self.layer.borderColor;
- [self.layer addSublayer:borderLayer];
- _borderLayers[side] = borderLayer;
- }
- return borderLayer;
-}
+ UIScreen *screen = self.window.screen ?: [UIScreen mainScreen];
+ UIGraphicsBeginImageContextWithOptions(size, NO, screen.scale * 2);
-- (void)updatePathForShapeLayerForSide:(RCTBorderSide)side
-{
- CAShapeLayer *borderLayer = [self createShapeLayerIfNotExistsForSide:side];
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
+ const CGRect rect = {CGPointZero, size};
+ CGPathRef path = CGPathCreateWithRoundedRect(rect, radius, radius, NULL);
- CGPoint trapezoidPoints[4];
- [self getTrapezoidPoints:trapezoidPoints forSide:side];
+ if (_backgroundColor) {
+ CGContextSaveGState(ctx);
- CGMutablePathRef path = CGPathCreateMutable();
- CGPathAddLines(path, NULL, trapezoidPoints, 4);
- CGPathCloseSubpath(path);
- borderLayer.path = path;
+ CGContextAddPath(ctx, path);
+ CGContextSetFillColorWithColor(ctx, _backgroundColor.CGColor);
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
+
+ CGContextAddPath(ctx, path);
CGPathRelease(path);
-}
-- (void)updateBorderLayers
-{
- BOOL widthsAndColorsSame = YES;
- CGFloat width = _borderWidths[0];
- CGColorRef color = _borderLayers[0].fillColor;
- for (RCTBorderSide side = 1; side < RCTBorderSideCount; side++) {
- CAShapeLayer *layer = _borderLayers[side];
- if (_borderWidths[side] != width || (layer && !CGColorEqualToColor(layer.fillColor, color))) {
- widthsAndColorsSame = NO;
- break;
+ if (radius > 0 && topWidth > 0 && rightWidth > 0 && bottomWidth > 0 && leftWidth > 0) {
+ const UIEdgeInsets insetEdgeInsets = UIEdgeInsetsMake(topWidth, leftWidth, bottomWidth, rightWidth);
+ const CGRect insetRect = UIEdgeInsetsInsetRect(rect, insetEdgeInsets);
+ CGPathRef insetPath = RCTPathCreateWithRoundedRect(insetRect, leftRadius, topRadius, rightRadius, topRadius, leftRadius, bottomRadius, rightRadius, bottomRadius, NULL);
+ CGContextAddPath(ctx, insetPath);
+ CGPathRelease(insetPath);
+ }
+
+ CGContextEOClip(ctx);
+
+ BOOL hasEqualColor = !_borderTopColor && !_borderRightColor && !_borderBottomColor && !_borderLeftColor;
+ BOOL hasEqualBorder = _borderWidth >= 0 && _borderTopWidth < 0 && _borderRightWidth < 0 && _borderBottomWidth < 0 && _borderLeftWidth < 0;
+ if (radius <= 0 && hasEqualBorder && hasEqualColor) {
+ CGContextSetStrokeColorWithColor(ctx, _borderColor);
+ CGContextSetLineWidth(ctx, 2 * _borderWidth);
+ CGContextClipToRect(ctx, rect);
+ CGContextStrokeRect(ctx, rect);
+ } else if (radius <= 0 && hasEqualColor) {
+ CGContextSetFillColorWithColor(ctx, _borderColor);
+ CGContextAddRect(ctx, rect);
+ const CGRect insetRect = UIEdgeInsetsInsetRect(rect, edgeInsets);
+ CGContextAddRect(ctx, insetRect);
+ CGContextEOFillPath(ctx);
+ } else {
+ BOOL didSet = NO;
+ CGPoint topLeft;
+ if (topRadius > 0 && leftRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, topWidth, 2 * leftRadius, 2 * topRadius), CGPointMake(0, 0), CGPointMake(leftWidth, topWidth), points);
+ if (!isnan(points[1].x) && !isnan(points[1].y)) {
+ topLeft = points[1];
+ didSet = YES;
+ }
}
- }
- if (widthsAndColorsSame) {
- // Set main layer border
- if (width) {
- _borderWidth = self.layer.borderWidth = width;
+ if (!didSet) {
+ topLeft = CGPointMake(leftWidth, topWidth);
}
- if (color) {
- self.layer.borderColor = color;
+
+ didSet = NO;
+ CGPoint bottomLeft;
+ if (bottomRadius > 0 && leftRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, (size.height - bottomWidth) - 2 * bottomRadius, 2 * leftRadius, 2 * bottomRadius), CGPointMake(0, size.height), CGPointMake(leftWidth, size.height - bottomWidth), points);
+ if (!isnan(points[1].x) && !isnan(points[1].y)) {
+ bottomLeft = points[1];
+ didSet = YES;
+ }
}
- // Remove border layers
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- [_borderLayers[side] removeFromSuperlayer];
- _borderLayers[side] = nil;
+ if (!didSet) {
+ bottomLeft = CGPointMake(leftWidth, size.height - bottomWidth);
}
- } else {
+ didSet = NO;
+ CGPoint topRight;
+ if (topRadius > 0 && rightRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, topWidth, 2 * rightRadius, 2 * topRadius), CGPointMake(size.width, 0), CGPointMake(size.width - rightWidth, topWidth), points);
+ if (!isnan(points[0].x) && !isnan(points[0].y)) {
+ topRight = points[0];
+ didSet = YES;
+ }
+ }
+
+ if (!didSet) {
+ topRight = CGPointMake(size.width - rightWidth, topWidth);
+ }
+
+ didSet = NO;
+ CGPoint bottomRight;
+ if (bottomRadius > 0 && rightRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, (size.height - bottomWidth) - 2 * bottomRadius, 2 * rightRadius, 2 * bottomRadius), CGPointMake(size.width, size.height), CGPointMake(size.width - rightWidth, size.height - bottomWidth), points);
+ if (!isnan(points[0].x) && !isnan(points[0].y)) {
+ bottomRight = points[0];
+ didSet = YES;
+ }
+ }
- // Clear main layer border
- self.layer.borderWidth = 0;
+ if (!didSet) {
+ bottomRight = CGPointMake(size.width - rightWidth, size.height - bottomWidth);
+ }
+
+ // RIGHT
+ if (rightWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(size.width, 0),
+ topRight,
+ bottomRight,
+ CGPointMake(size.width, size.height),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderRightColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
- // Set up border layers
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- [self updatePathForShapeLayerForSide:side];
+ // BOTTOM
+ if (bottomWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(0, size.height),
+ bottomLeft,
+ bottomRight,
+ CGPointMake(size.width, size.height),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderBottomColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
+
+ // LEFT
+ if (leftWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(0, 0),
+ topLeft,
+ bottomLeft,
+ CGPointMake(0, size.height),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderLeftColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
+
+ // TOP
+ if (topWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(0, 0),
+ topLeft,
+ topRight,
+ CGPointMake(size.width, 0),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderTopColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
}
}
-}
-- (CGFloat)borderWidthForSide:(RCTBorderSide)side
-{
- return _borderWidths[side] ?: _borderWidth;
+ UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ *contentsCenter = CGRectMake(edgeInsets.left / size.width, edgeInsets.top / size.height, 1.0 / size.width, 1.0 / size.height);
+ return [image resizableImageWithCapInsets:edgeInsets];
}
-- (void)setBorderWidth:(CGFloat)width forSide:(RCTBorderSide)side
+- (void)displayLayer:(CALayer *)layer
{
- _borderWidths[side] = width;
- [self updateBorderLayers];
-}
+ CGRect contentsCenter;
+ UIImage *image = [self generateBorderImage:&contentsCenter];
-#define BORDER_WIDTH(SIDE) \
-- (CGFloat)border##SIDE##Width { return [self borderWidthForSide:RCTBorderSide##SIDE]; } \
-- (void)setBorder##SIDE##Width:(CGFloat)width { [self setBorderWidth:width forSide:RCTBorderSide##SIDE]; }
+ if (RCTRunningInTestEnvironment()) {
+ const CGSize size = self.bounds.size;
+ UIGraphicsBeginImageContextWithOptions(size, NO, image.scale);
+ [image drawInRect:(CGRect){CGPointZero, size}];
+ image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
-BORDER_WIDTH(Top)
-BORDER_WIDTH(Right)
-BORDER_WIDTH(Bottom)
-BORDER_WIDTH(Left)
+ contentsCenter = CGRectMake(0, 0, 1, 1);
+ }
-- (CGColorRef)borderColorForSide:(RCTBorderSide)side
-{
- return _borderLayers[side].fillColor ?: self.layer.borderColor;
+ layer.contents = (id)image.CGImage;
+ layer.contentsCenter = contentsCenter;
+ layer.contentsScale = image.scale;
+ layer.magnificationFilter = kCAFilterNearest;
}
-- (void)setBorderColor:(CGColorRef)color forSide:(RCTBorderSide)side
-{
- [self createShapeLayerIfNotExistsForSide:side].fillColor = color;
- [self updateBorderLayers];
-}
+#pragma mark Border Color
-#define BORDER_COLOR(SIDE) \
-- (CGColorRef)border##SIDE##Color { return [self borderColorForSide:RCTBorderSide##SIDE]; } \
-- (void)setBorder##SIDE##Color:(CGColorRef)color { [self setBorderColor:color forSide:RCTBorderSide##SIDE]; }
+#define setBorderColor(side) \
+ - (void)setBorder##side##Color:(CGColorRef)border##side##Color \
+ { \
+ if (CGColorEqualToColor(_border##side##Color, border##side##Color)) { \
+ return; \
+ } \
+ _border##side##Color = border##side##Color; \
+ [self.layer setNeedsDisplay]; \
+ }
-BORDER_COLOR(Top)
-BORDER_COLOR(Right)
-BORDER_COLOR(Bottom)
-BORDER_COLOR(Left)
+setBorderColor()
+setBorderColor(Top)
+setBorderColor(Right)
+setBorderColor(Bottom)
+setBorderColor(Left)
-- (void)setBorderWidth:(CGFloat)borderWidth
-{
- _borderWidth = borderWidth;
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- _borderWidths[side] = borderWidth;
+#pragma mark - Border Width
+
+#define setBorderWidth(side) \
+ - (void)setBorder##side##Width:(CGFloat)border##side##Width \
+ { \
+ if (_border##side##Width == border##side##Width) { \
+ return; \
+ } \
+ _border##side##Width = border##side##Width; \
+ [self.layer setNeedsDisplay]; \
}
- [self updateBorderLayers];
-}
-- (void)setBorderColor:(CGColorRef)borderColor
+setBorderWidth()
+setBorderWidth(Top)
+setBorderWidth(Right)
+setBorderWidth(Bottom)
+setBorderWidth(Left)
+
+@end
+
+static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise)
{
- self.layer.borderColor = borderColor;
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- _borderLayers[side].fillColor = borderColor;
+ CGFloat xScale = 1, yScale = 1, radius = 0;
+ if (xRadius != 0) {
+ xScale = 1;
+ yScale = yRadius / xRadius;
+ radius = xRadius;
+ } else if (yRadius != 0) {
+ xScale = xRadius / yRadius;
+ yScale = 1;
+ radius = yRadius;
}
- [self updateBorderLayers];
+
+ CGAffineTransform t = CGAffineTransformMakeTranslation(x, y);
+ t = CGAffineTransformScale(t, xScale, yScale);
+ if (m != NULL) {
+ t = CGAffineTransformConcat(t, *m);
+ }
+
+ CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise);
}
-- (CGColorRef)borderColor
+static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform)
{
- return self.layer.borderColor;
+ const CGFloat minX = CGRectGetMinX(rect);
+ const CGFloat minY = CGRectGetMinY(rect);
+ const CGFloat maxX = CGRectGetMaxX(rect);
+ const CGFloat maxY = CGRectGetMaxY(rect);
+
+ CGMutablePathRef path = CGPathCreateMutable();
+ RCTPathAddEllipticArc(path, transform, minX + topLeftRadiusX, minY + topLeftRadiusY, topLeftRadiusX, topLeftRadiusY, M_PI, 3 * M_PI_2, false);
+ RCTPathAddEllipticArc(path, transform, maxX - topRightRadiusX, minY + topRightRadiusY, topRightRadiusX, topRightRadiusY, 3 * M_PI_2, 0, false);
+ RCTPathAddEllipticArc(path, transform, maxX - bottomRightRadiusX, maxY - bottomRightRadiusY, bottomRightRadiusX, bottomRightRadiusY, 0, M_PI_2, false);
+ RCTPathAddEllipticArc(path, transform, minX + bottomLeftRadiusX, maxY - bottomLeftRadiusY, bottomLeftRadiusX, bottomLeftRadiusY, M_PI_2, M_PI, false);
+ CGPathCloseSubpath(path);
+ return path;
}
-@end
+static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2])
+{
+ const CGFloat ellipseCenterX = CGRectGetMidX(ellipseBoundingRect);
+ const CGFloat ellipseCenterY = CGRectGetMidY(ellipseBoundingRect);
+
+ // ellipseBoundingRect.origin.x -= ellipseCenterX;
+ // ellipseBoundingRect.origin.y -= ellipseCenterY;
+
+ p1.x -= ellipseCenterX;
+ p1.y -= ellipseCenterY;
+
+ p2.x -= ellipseCenterX;
+ p2.y -= ellipseCenterY;
+
+ const CGFloat m = (p2.y - p1.y) / (p2.x - p1.x);
+ const CGFloat a = ellipseBoundingRect.size.width / 2;
+ const CGFloat b = ellipseBoundingRect.size.height / 2;
+ const CGFloat c = p1.y - m * p1.x;
+ const CGFloat A = (b * b + a * a * m * m);
+ const CGFloat B = 2 * a * a * c * m;
+ const CGFloat D = sqrt((a * a * (b * b - c * c)) / A + pow(B / (2 * A), 2));
+
+ const CGFloat x_ = -B / (2 * A);
+ const CGFloat x1 = x_ + D;
+ const CGFloat x2 = x_ - D;
+ const CGFloat y1 = m * x1 + c;
+ const CGFloat y2 = m * x2 + c;
+
+ intersections[0] = CGPointMake(x1 + ellipseCenterX, y1 + ellipseCenterY);
+ intersections[1] = CGPointMake(x2 + ellipseCenterX, y2 + ellipseCenterY);
+ return YES;
+}
diff --git a/React/Views/RCTViewNodeProtocol.h b/React/Views/RCTViewNodeProtocol.h
index 691aaaba15af3a..e78cc2ce7b26fc 100644
--- a/React/Views/RCTViewNodeProtocol.h
+++ b/React/Views/RCTViewNodeProtocol.h
@@ -35,7 +35,7 @@
@end
// TODO: this is kinda dumb - let's come up with a
-// better way of identifying root react views please!
+// better way of identifying root React views please!
static inline BOOL RCTIsReactRootView(NSNumber *reactTag) {
return reactTag.integerValue % 10 == 1;
}
diff --git a/package.json b/package.json
index a29d697447e32d..276f0a31046340 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"optimist": "0.6.1",
"promise": "^7.0.0",
"react-timer-mixin": "^0.13.1",
- "react-tools": "0.13.1",
+ "react-tools": "0.13.2",
"rebound": "^0.0.12",
"sane": "1.0.3",
"source-map": "0.1.31",
diff --git a/packager/webSocketProxy.js b/packager/webSocketProxy.js
index 8223bbf24b0e78..f863621362e421 100644
--- a/packager/webSocketProxy.js
+++ b/packager/webSocketProxy.js
@@ -34,7 +34,12 @@ function attachToServer(server, path) {
ws.on('message', function(message) {
allClientsExcept(ws).forEach(function(cn) {
- cn.send(message);
+ try {
+ // Sometimes this call throws 'not opened'
+ cn.send(message);
+ } catch(e) {
+ console.warn('WARN: ' + e.message);
+ }
});
});
});