diff --git a/Examples/UIExplorer/AsyncStorageExample.js b/Examples/UIExplorer/AsyncStorageExample.js
index 323cedd15..8bc1631cd 100644
--- a/Examples/UIExplorer/AsyncStorageExample.js
+++ b/Examples/UIExplorer/AsyncStorageExample.js
@@ -29,16 +29,17 @@ var COLORS = ['red', 'orange', 'yellow', 'green', 'blue'];
var BasicStorageExample = React.createClass({
componentDidMount() {
- AsyncStorage.getItem(STORAGE_KEY, (error, value) => {
- if (error) {
- this._appendMessage('AsyncStorage error: ' + error.message);
- } else if (value !== null) {
- this.setState({selectedValue: value});
- this._appendMessage('Recovered selection from disk: ' + value);
- } else {
- this._appendMessage('Initialized with no selection on disk.');
- }
- });
+ AsyncStorage.getItem(STORAGE_KEY)
+ .then((value) => {
+ if (value !== null){
+ this.setState({selectedValue: value});
+ this._appendMessage('Recovered selection from disk: ' + value);
+ } else {
+ this._appendMessage('Initialized with no selection on disk.');
+ }
+ })
+ .catch((error) => this._appendMessage('AsyncStorage error: ' + error.message))
+ .done();
},
getInitialState() {
return {
@@ -81,23 +82,17 @@ var BasicStorageExample = React.createClass({
_onValueChange(selectedValue) {
this.setState({selectedValue});
- AsyncStorage.setItem(STORAGE_KEY, selectedValue, (error) => {
- if (error) {
- this._appendMessage('AsyncStorage error: ' + error.message);
- } else {
- this._appendMessage('Saved selection to disk: ' + selectedValue);
- }
- });
+ AsyncStorage.setItem(STORAGE_KEY, selectedValue)
+ .then(() => this._appendMessage('Saved selection to disk: ' + selectedValue))
+ .catch((error) => this._appendMessage('AsyncStorage error: ' + error.message))
+ .done();
},
_removeStorage() {
- AsyncStorage.removeItem(STORAGE_KEY, (error) => {
- if (error) {
- this._appendMessage('AsyncStorage error: ' + error.message);
- } else {
- this._appendMessage('Selection removed from disk.');
- }
- });
+ AsyncStorage.removeItem(STORAGE_KEY)
+ .then(() => this._appendMessage('Selection removed from disk.'))
+ .catch((error) => { this._appendMessage('AsyncStorage error: ' + error.message) })
+ .done();
},
_appendMessage(message) {
diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json
index 9c8120dff..e1e9cd56b 100644
--- a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json
+++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json
@@ -1,5 +1,9 @@
{
"images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
{
"idiom" : "universal",
"scale" : "2x",
diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m
index 88bb5fe28..b7c76c9f5 100644
--- a/Libraries/Animation/RCTAnimationExperimentalManager.m
+++ b/Libraries/Animation/RCTAnimationExperimentalManager.m
@@ -71,7 +71,7 @@
UIView *view = viewRegistry[reactTag];
if (!view) {
- RCTLogWarn(@"React tag %@ is not registered with the view registry", reactTag);
+ RCTLogWarn(@"React tag #%@ is not registered with the view registry", reactTag);
return;
}
diff --git a/Libraries/Components/Subscribable.js b/Libraries/Components/Subscribable.js
index 06b7e0ce4..cf9bc773a 100644
--- a/Libraries/Components/Subscribable.js
+++ b/Libraries/Components/Subscribable.js
@@ -11,308 +11,25 @@
*/
'use strict';
-/**
- * Subscribable wraps EventEmitter in a clean interface, and provides a mixin
- * so components can easily subscribe to events and not worry about cleanup on
- * unmount.
- *
- * Also acts as a basic store because it records the last data that it emitted,
- * and provides a way to populate the initial data. The most recent data can be
- * fetched from the Subscribable by calling `get()`
- *
- * Advantages over EventEmitter + Subscibable.Mixin.addListenerOn:
- * - Cleaner usage: no strings to identify the event
- * - Lifespan pattern enforces cleanup
- * - More logical: Subscribable.Mixin now uses a Subscribable class
- * - Subscribable saves the last data and makes it available with `.get()`
- *
- * Legacy Subscribable.Mixin.addListenerOn allowed automatic subscription to
- * EventEmitters. Now we should avoid EventEmitters and wrap with Subscribable
- * instead:
- *
- * ```
- * AppState.networkReachability = new Subscribable(
- * RCTDeviceEventEmitter,
- * 'reachabilityDidChange',
- * (resp) => resp.network_reachability,
- * RCTReachability.getCurrentReachability
- * );
- *
- * var myComponent = React.createClass({
- * mixins: [Subscribable.Mixin],
- * getInitialState: function() {
- * return {
- * isConnected: AppState.networkReachability.get() !== 'none'
- * };
- * },
- * componentDidMount: function() {
- * this._reachSubscription = this.subscribeTo(
- * AppState.networkReachability,
- * (reachability) => {
- * this.setState({ isConnected: reachability !== 'none' })
- * }
- * );
- * },
- * render: function() {
- * return (
- *
- * {this.state.isConnected ? 'Network Connected' : 'No network'}
- *
- * this._reachSubscription.remove()}>
- * End reachability subscription
- *
- * );
- * }
- * });
- * ```
- */
-
var EventEmitter = require('EventEmitter');
-var invariant = require('invariant');
-var logError = require('logError');
-
-var SUBSCRIBABLE_INTERNAL_EVENT = 'subscriptionEvent';
-
-type Data = Object;
-type EventMapping = (_: Data) => Data;
-
-class Subscribable {
- _eventMapping: EventMapping;
- _lastData: Data;
-
- /**
- * Creates a new Subscribable object
- *
- * @param {EventEmitter} eventEmitter Emitter to trigger subscription events.
- * @param {string} eventName Name of emitted event that triggers subscription
- * events.
- * @param {function} eventMapping (optional) Function to convert the output
- * of the eventEmitter to the subscription output.
- * @param {function} getInitData (optional) Async function to grab the initial
- * data to publish. Signature `function(successCallback, errorCallback)`.
- * The resolved data will be transformed with the eventMapping before it
- * gets emitted.
- */
- constructor(eventEmitter: EventEmitter, eventName: string, eventMapping?: EventMapping, getInitData?: Function) {
-
- this._internalEmitter = new EventEmitter();
- this._eventMapping = eventMapping || (data => data);
-
- this._upstreamSubscription = eventEmitter.addListener(
- eventName,
- this._handleEmit,
- this
- );
-
- // Asyncronously get the initial data, if provided
- getInitData && getInitData(this._handleInitData.bind(this), logError);
- }
-
- /**
- * Returns the last data emitted from the Subscribable, or undefined
- */
- get(): Data {
- return this._lastData;
- }
-
- /**
- * Unsubscribe from the upstream EventEmitter
- */
- cleanup() {
- this._upstreamSubscription && this._upstreamSubscription.remove();
- }
-
- /**
- * Add a new listener to the subscribable. This should almost never be used
- * directly, and instead through Subscribable.Mixin.subscribeTo
- *
- * @param {object} lifespan Object with `addUnmountCallback` that accepts
- * a handler to be called when the component unmounts. This is required and
- * desirable because it enforces cleanup. There is no easy way to leave the
- * subsciption hanging
- * {
- * addUnmountCallback: function(newUnmountHanlder) {...},
- * }
- * @param {function} callback Handler to call when Subscribable has data
- * updates
- * @param {object} context Object to bind the handler on, as "this"
- *
- * @return {object} the subscription object:
- * {
- * remove: function() {...},
- * }
- * Call `remove` to terminate the subscription before unmounting
- */
- subscribe(lifespan: { addUnmountCallback: Function }, callback: Function, context: Object) {
- invariant(
- typeof lifespan.addUnmountCallback === 'function',
- 'Must provide a valid lifespan, which provides a way to add a ' +
- 'callback for when subscription can be cleaned up. This is used ' +
- 'automatically by Subscribable.Mixin'
- );
- invariant(
- typeof callback === 'function',
- 'Must provide a valid subscription handler.'
- );
-
- // Add a listener to the internal EventEmitter
- var subscription = this._internalEmitter.addListener(
- SUBSCRIBABLE_INTERNAL_EVENT,
- callback,
- context
- );
-
- // Clean up subscription upon the lifespan unmount callback
- lifespan.addUnmountCallback(() => {
- subscription.remove();
- });
-
- return subscription;
- }
-
- /**
- * Callback for the initial data resolution. Currently behaves the same as
- * `_handleEmit`, but we may eventually want to keep track of the difference
- */
- _handleInitData(dataInput: Data) {
- var emitData = this._eventMapping(dataInput);
- this._lastData = emitData;
- this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
- }
-
- /**
- * Handle new data emissions. Pass the data through our eventMapping
- * transformation, store it for later `get()`ing, and emit it for subscribers
- */
- _handleEmit(dataInput: Data) {
- var emitData = this._eventMapping(dataInput);
- this._lastData = emitData;
- this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
- }
-}
+/**
+ * Subscribable provides a mixin for safely subscribing a component to an
+ * eventEmitter
+ *
+ * This will be replaced with the observe interface that will be coming soon to
+ * React Core
+ */
+var Subscribable = {};
Subscribable.Mixin = {
- /**
- * @return {object} lifespan Object with `addUnmountCallback` that accepts
- * a handler to be called when the component unmounts
- * {
- * addUnmountCallback: function(newUnmountHanlder) {...},
- * }
- */
- _getSubscribableLifespan: function() {
- if (!this._subscribableLifespan) {
- this._subscribableLifespan = {
- addUnmountCallback: (cb) => {
- this._endSubscribableLifespanCallbacks.push(cb);
- },
- };
- }
- return this._subscribableLifespan;
- },
-
- _endSubscribableLifespan: function() {
- this._endSubscribableLifespanCallbacks.forEach(cb => cb());
- },
-
- /**
- * Components use `subscribeTo` for listening to Subscribable stores. Cleanup
- * is automatic on component unmount.
- *
- * To stop listening to the subscribable and end the subscription early,
- * components should store the returned subscription object and invoke the
- * `remove()` function on it
- *
- * @param {Subscribable} subscription to subscribe to.
- * @param {function} listener Function to invoke when event occurs.
- * @param {object} context Object to bind the handler on, as "this"
- *
- * @return {object} the subscription object:
- * {
- * remove: function() {...},
- * }
- * Call `remove` to terminate the subscription before unmounting
- */
- subscribeTo: function(subscribable, handler, context) {
- invariant(
- subscribable instanceof Subscribable,
- 'Must provide a Subscribable'
- );
- return subscribable.subscribe(
- this._getSubscribableLifespan(),
- handler,
- context
- );
- },
-
- /**
- * Gets a Subscribable store, scoped to the component, that can be passed to
- * children. The component will automatically clean up the subscribable's
- * subscription to the eventEmitter when unmounting.
- *
- * `provideSubscribable` will always return the same Subscribable for any
- * particular emitter/eventName combo, so it can be called directly from
- * render, and it will never create duplicate Subscribables.
- *
- * @param {EventEmitter} eventEmitter Emitter to trigger subscription events.
- * @param {string} eventName Name of emitted event that triggers subscription
- * events.
- * @param {function} eventMapping (optional) Function to convert the output
- * of the eventEmitter to the subscription output.
- * @param {function} getInitData (optional) Async function to grab the initial
- * data to publish. Signature `function(successCallback, errorCallback)`.
- * The resolved data will be transformed with the eventMapping before it
- * gets emitted.
- */
- provideSubscribable: function(eventEmitter, eventName, eventMapping, getInitData) {
- this._localSubscribables = this._localSubscribables || {};
- this._localSubscribables[eventEmitter] =
- this._localSubscribables[eventEmitter] || {};
- if (!this._localSubscribables[eventEmitter][eventName]) {
- this._localSubscribables[eventEmitter][eventName] =
- new Subscribable(eventEmitter, eventName, eventMapping, getInitData);
- }
- return this._localSubscribables[eventEmitter][eventName];
- },
-
- /**
- * Removes any local Subscribables created with `provideSubscribable`, so the
- * component can unmount without leaving any dangling listeners on
- * eventEmitters
- */
- _cleanupLocalSubscribables: function() {
- if (!this._localSubscribables) {
- return;
- }
- Object.keys(this._localSubscribables).forEach((eventEmitter) => {
- var emitterSubscribables = this._localSubscribables[eventEmitter];
- if (emitterSubscribables) {
- Object.keys(emitterSubscribables).forEach((eventName) => {
- emitterSubscribables[eventName].cleanup();
- });
- }
- });
- this._localSubscribables = null;
- },
-
componentWillMount: function() {
- this._endSubscribableLifespanCallbacks = [];
-
- // DEPRECATED addListenerOn* usage:
this._subscribableSubscriptions = [];
},
componentWillUnmount: function() {
- // Resolve the lifespan, which will cause Subscribable to clean any
- // remaining subscriptions
- this._endSubscribableLifespan && this._endSubscribableLifespan();
-
- this._cleanupLocalSubscribables();
-
- // DEPRECATED addListenerOn* usage uses _subscribableSubscriptions array
- // instead of lifespan
this._subscribableSubscriptions.forEach(
(subscription) => subscription.remove()
);
@@ -320,9 +37,6 @@ Subscribable.Mixin = {
},
/**
- * DEPRECATED - Use `Subscribable` and `Mixin.subscribeTo` instead.
- * `addListenerOn` subscribes the component to an `EventEmitter`.
- *
* Special form of calling `addListener` that *guarantees* that a
* subscription *must* be tied to a component instance, and therefore will
* be cleaned up when the component is unmounted. It is impossible to create
@@ -335,7 +49,12 @@ Subscribable.Mixin = {
* @param {function} listener Function to invoke when event occurs.
* @param {object} context Object to use as listener context.
*/
- addListenerOn: function(eventEmitter, eventType, listener, context) {
+ addListenerOn: function(
+ eventEmitter: EventEmitter,
+ eventType: string,
+ listener: Function,
+ context: Object
+ ) {
this._subscribableSubscriptions.push(
eventEmitter.addListener(eventType, listener, context)
);
diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js
index 89f3b49c2..0ad3c4269 100644
--- a/Libraries/CustomComponents/Navigator/Navigator.js
+++ b/Libraries/CustomComponents/Navigator/Navigator.js
@@ -27,12 +27,14 @@
'use strict';
var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule;
-var Backstack = require('Backstack');
+var BackAndroid = require('BackAndroid');
var Dimensions = require('Dimensions');
var InteractionMixin = require('InteractionMixin');
-var NavigatorSceneConfigs = require('NavigatorSceneConfigs');
-var NavigatorNavigationBar = require('NavigatorNavigationBar');
var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar');
+var NavigatorInterceptor = require('NavigatorInterceptor');
+var NavigatorNavigationBar = require('NavigatorNavigationBar');
+var NavigatorSceneConfigs = require('NavigatorSceneConfigs');
+var NavigatorStaticContextContainer = require('NavigatorStaticContextContainer');
var PanResponder = require('PanResponder');
var React = require('React');
var StaticContainer = require('StaticContainer.react');
@@ -41,6 +43,7 @@ var Subscribable = require('Subscribable');
var TimerMixin = require('react-timer-mixin');
var View = require('View');
+var getNavigatorContext = require('getNavigatorContext');
var clamp = require('clamp');
var invariant = require('invariant');
var keyMirror = require('keyMirror');
@@ -59,8 +62,6 @@ function getuid() {
return __uid++;
}
-var nextComponentUid = 0;
-
// styles moved to the top of the file so getDefaultProps can refer to it
var styles = StyleSheet.create({
container: {
@@ -185,12 +186,11 @@ var Navigator = React.createClass({
/**
* Required function which renders the scene for a given route. Will be
- * invoked with the route, the navigator object, and a ref handler that
- * will allow a ref to your scene to be provided by props.onItemRef
+ * invoked with the route and the navigator object
*
* ```
- * (route, navigator, onRef) =>
- *
+ * (route, navigator) =>
+ *
* ```
*/
renderScene: PropTypes.func.isRequired,
@@ -242,19 +242,18 @@ var Navigator = React.createClass({
* Styles to apply to the container of each scene
*/
sceneStyle: View.propTypes.style,
+ },
- /**
- * Should the backstack back button "jump" back instead of pop? Set to true
- * if a jump forward might happen after the android back button is pressed,
- * so the scenes will remain mounted
- */
- shouldJumpOnBackstackPop: PropTypes.bool,
+ contextTypes: {
+ navigator: PropTypes.object,
},
statics: {
BreadcrumbNavigationBar: NavigatorBreadcrumbNavigationBar,
NavigationBar: NavigatorNavigationBar,
SceneConfigs: NavigatorSceneConfigs,
+ Interceptor: NavigatorInterceptor,
+ getContext: getNavigatorContext,
},
mixins: [TimerMixin, InteractionMixin, Subscribable.Mixin],
@@ -303,7 +302,20 @@ var Navigator = React.createClass({
},
componentWillMount: function() {
- this.navigatorActions = {
+ this.parentNavigator = getNavigatorContext(this) || this.props.navigator;
+ this.navigatorContext = {
+ setHandlerForRoute: this.setHandlerForRoute,
+ request: this.request,
+
+ parentNavigator: this.parentNavigator,
+ getCurrentRoutes: this.getCurrentRoutes,
+ // We want to bubble focused routes to the top navigation stack. If we
+ // are a child navigator, this allows us to call props.navigator.on*Focus
+ // of the topmost Navigator
+ onWillFocus: this.props.onWillFocus,
+ onDidFocus: this.props.onDidFocus,
+
+ // Legacy, imperitive nav actions. Use request when possible.
jumpBack: this.jumpBack,
jumpForward: this.jumpForward,
jumpTo: this.jumpTo,
@@ -317,14 +329,8 @@ var Navigator = React.createClass({
resetTo: this.resetTo,
popToRoute: this.popToRoute,
popToTop: this.popToTop,
- parentNavigator: this.props.navigator,
- getCurrentRoutes: this.getCurrentRoutes,
- // We want to bubble focused routes to the top navigation stack. If we
- // are a child navigator, this allows us to call props.navigator.on*Focus
- // of the topmost Navigator
- onWillFocus: this.props.onWillFocus,
- onDidFocus: this.props.onDidFocus,
};
+ this._handlers = {};
this.panGesture = PanResponder.create({
onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture,
@@ -336,17 +342,50 @@ var Navigator = React.createClass({
});
this._itemRefs = {};
this._interactionHandle = null;
- this._backstackComponentKey = 'jsnavstack' + nextComponentUid;
- nextComponentUid++;
-
- Backstack.eventEmitter && this.addListenerOn(
- Backstack.eventEmitter,
- 'popNavigation',
- this._onBackstackPopState);
this._emitWillFocus(this.state.presentedIndex);
},
+ request: function(action, arg1, arg2) {
+ if (this.parentNavigator) {
+ return this.parentNavigator.request.apply(null, arguments);
+ }
+ return this._handleRequest.apply(null, arguments);
+ },
+
+ _handleRequest: function(action, arg1, arg2) {
+ var childHandler = this._handlers[this.state.presentedIndex];
+ if (childHandler && childHandler(action, arg1, arg2)) {
+ return true;
+ }
+ switch (action) {
+ case 'pop':
+ return this._handlePop();
+ case 'push':
+ return this._handlePush(arg1);
+ default:
+ invariant(false, 'Unsupported request type ' + action);
+ return false;
+ }
+ },
+
+ _handlePop: function() {
+ if (this.state.presentedIndex === 0) {
+ return false;
+ }
+ this.pop();
+ return true;
+ },
+
+ _handlePush: function(route) {
+ this.push(route);
+ return true;
+ },
+
+ setHandlerForRoute: function(route, handler) {
+ this._handlers[this.state.routeStack.indexOf(route)] = handler;
+ },
+
_configureSpring: function(animationConfig) {
var config = this.spring.getSpringConfig();
config.friction = animationConfig.springFriction;
@@ -361,30 +400,28 @@ var Navigator = React.createClass({
animationConfig && this._configureSpring(animationConfig);
this.spring.addListener(this);
this.onSpringUpdate();
-
- // Fill up the Backstack with routes that have been provided in
- // initialRouteStack
- this._fillBackstackRange(0, this.state.routeStack.indexOf(this.props.initialRoute));
this._emitDidFocus(this.state.presentedIndex);
+ if (this.parentNavigator) {
+ this.parentNavigator.setHandler(this._handleRequest);
+ } else {
+ // There is no navigator in our props or context, so this is the
+ // top-level navigator. We will handle back button presses here
+ BackAndroid.addEventListener('hardwareBackPress', this._handleBackPress);
+ }
},
componentWillUnmount: function() {
- Backstack.removeComponentHistory(this._backstackComponentKey);
+ if (this.parentNavigator) {
+ this.parentNavigator.setHandler(null);
+ } else {
+ BackAndroid.removeEventListener('hardwareBackPress', this._handleBackPress);
+ }
},
- _onBackstackPopState: function(componentKey, stateKey, state) {
- if (componentKey !== this._backstackComponentKey) {
- return;
- }
- if (!this._canNavigate()) {
- // A bit hacky: if we can't actually handle the pop, just push it back on the stack
- Backstack.pushNavigation(componentKey, stateKey, state);
- } else {
- if (this.props.shouldJumpOnBackstackPop) {
- this._jumpToWithoutBackstack(state.fromRoute);
- } else {
- this._popToRouteWithoutBackstack(state.fromRoute);
- }
+ _handleBackPress: function() {
+ var didPop = this.request('pop');
+ if (!didPop) {
+ BackAndroid.exitApp();
}
},
@@ -737,41 +774,6 @@ var Navigator = React.createClass({
return !this.state.isAnimating;
},
- _jumpNWithoutBackstack: function(n) {
- var destIndex = this._getDestIndexWithinBounds(n);
- if (!this._canNavigate()) {
- return; // It's busy animating or transitioning.
- }
- var requestTransitionAndResetUpdatingRange = () => {
- this._requestTransitionTo(destIndex);
- this._resetUpdatingRange();
- };
- this.setState({
- updatingRangeStart: destIndex,
- updatingRangeLength: 1,
- toIndex: destIndex,
- }, requestTransitionAndResetUpdatingRange);
- },
-
- _fillBackstackRange: function(start, end) {
- invariant(
- start <= end,
- 'Can only fill the backstack forward. Provide end index greater than start'
- );
- for (var i = 0; i < (end - start); i++) {
- var fromIndex = start + i;
- var toIndex = start + i + 1;
- Backstack.pushNavigation(
- this._backstackComponentKey,
- toIndex,
- {
- fromRoute: this.state.routeStack[fromIndex],
- toRoute: this.state.routeStack[toIndex],
- }
- );
- }
- },
-
_getDestIndexWithinBounds: function(n) {
var currentIndex = this.state.presentedIndex;
var destIndex = currentIndex + n;
@@ -788,20 +790,19 @@ var Navigator = React.createClass({
},
_jumpN: function(n) {
- var currentIndex = this.state.presentedIndex;
+ var destIndex = this._getDestIndexWithinBounds(n);
if (!this._canNavigate()) {
return; // It's busy animating or transitioning.
}
- if (n > 0) {
- this._fillBackstackRange(currentIndex, currentIndex + n);
- } else {
- var landingBeforeIndex = currentIndex + n + 1;
- Backstack.resetToBefore(
- this._backstackComponentKey,
- landingBeforeIndex
- );
- }
- this._jumpNWithoutBackstack(n);
+ var requestTransitionAndResetUpdatingRange = () => {
+ this._requestTransitionTo(destIndex);
+ this._resetUpdatingRange();
+ };
+ this.setState({
+ updatingRangeStart: destIndex,
+ updatingRangeLength: 1,
+ toIndex: destIndex,
+ }, requestTransitionAndResetUpdatingRange);
},
jumpTo: function(route) {
@@ -813,15 +814,6 @@ var Navigator = React.createClass({
this._jumpN(destIndex - this.state.presentedIndex);
},
- _jumpToWithoutBackstack: function(route) {
- var destIndex = this.state.routeStack.indexOf(route);
- invariant(
- destIndex !== -1,
- 'Cannot jump to route that is not in the route stack'
- );
- this._jumpNWithoutBackstack(destIndex - this.state.presentedIndex);
- },
-
jumpForward: function() {
this._jumpN(1);
},
@@ -852,11 +844,6 @@ var Navigator = React.createClass({
toRoute: route,
fromRoute: this.state.routeStack[this.state.routeStack.length - 1],
};
- Backstack.pushNavigation(
- this._backstackComponentKey,
- this.state.routeStack.length,
- navigationState);
-
this.setState({
idStack: nextIDStack,
routeStack: nextStack,
@@ -867,14 +854,7 @@ var Navigator = React.createClass({
}, requestTransitionAndResetUpdatingRange);
},
- _manuallyPopBackstack: function(n) {
- Backstack.resetToBefore(this._backstackComponentKey, this.state.routeStack.length - n);
- },
-
- /**
- * Like popN, but doesn't also update the Backstack.
- */
- _popNWithoutBackstack: function(n) {
+ popN: function(n) {
if (n === 0 || !this._canNavigate()) {
return;
}
@@ -888,17 +868,10 @@ var Navigator = React.createClass({
);
},
- popN: function(n) {
- if (n === 0 || !this._canNavigate()) {
- return;
- }
- this._popNWithoutBackstack(n);
- this._manuallyPopBackstack(n);
- },
-
pop: function() {
- if (this.props.navigator && this.state.routeStack.length === 1) {
- return this.props.navigator.pop();
+ // TODO (t6707686): remove this parentNavigator call after transitioning call sites to `.request('pop')`
+ if (this.parentNavigator && this.state.routeStack.length === 1) {
+ return this.parentNavigator.pop();
}
this.popN(1);
},
@@ -970,14 +943,6 @@ var Navigator = React.createClass({
return this.state.routeStack.length - indexOfRoute - 1;
},
- /**
- * Like popToRoute, but doesn't update the Backstack, presumably because it's already up to date.
- */
- _popToRouteWithoutBackstack: function(route) {
- var numToPop = this._getNumToPopForRoute(route);
- this._popNWithoutBackstack(numToPop);
- },
-
popToRoute: function(route) {
var numToPop = this._getNumToPopForRoute(route);
this.popN(numToPop);
@@ -1003,7 +968,7 @@ var Navigator = React.createClass({
return this.state.routeStack;
},
- _onItemRef: function(itemId, ref) {
+ _handleItemRef: function(itemId, ref) {
this._itemRefs[itemId] = ref;
var itemIndex = this.state.idStack.indexOf(itemId);
if (itemIndex === -1) {
@@ -1036,23 +1001,33 @@ var Navigator = React.createClass({
this.state.updatingRangeLength !== 0 &&
i >= this.state.updatingRangeStart &&
i <= this.state.updatingRangeStart + this.state.updatingRangeLength;
+ var sceneNavigatorContext = {
+ ...this.navigatorContext,
+ route,
+ setHandler: (handler) => {
+ this.navigatorContext.setHandlerForRoute(route, handler);
+ },
+ };
var child = this.props.renderScene(
route,
- this.navigatorActions,
- this._onItemRef.bind(null, this.state.idStack[i])
+ sceneNavigatorContext
);
-
var initialSceneStyle =
i === this.state.presentedIndex ? styles.presentNavItem : styles.futureNavItem;
return (
-
+
- {child}
+ {React.cloneElement(child, {
+ ref: this._handleItemRef.bind(null, this.state.idStack[i]),
+ })}
-
+
);
},
@@ -1081,7 +1056,7 @@ var Navigator = React.createClass({
}
return React.cloneElement(this.props.navigationBar, {
ref: (navBar) => { this._navBar = navBar; },
- navigator: this.navigatorActions,
+ navigator: this.navigatorContext,
navState: this.state,
});
},
diff --git a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js
new file mode 100644
index 000000000..dcc5d43ef
--- /dev/null
+++ b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2015, Facebook, Inc. All rights reserved.
+ *
+ * Facebook, Inc. (“Facebook”) owns all right, title and interest, including
+ * all intellectual property and other proprietary rights, in and to the React
+ * Native CustomComponents software (the “Software”). Subject to your
+ * compliance with these terms, you are hereby granted a non-exclusive,
+ * worldwide, royalty-free copyright license to (1) use and copy the Software;
+ * and (2) reproduce and distribute the Software as part of your own software
+ * (“Your Software”). Facebook reserves all rights not expressly granted to
+ * you in this license agreement.
+ *
+ * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
+ * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
+ * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @providesModule NavigatorInterceptor
+ */
+'use strict';
+
+var React = require('React');
+
+var getNavigatorContext = require('getNavigatorContext');
+
+var NavigatorInterceptor = React.createClass({
+
+ contextTypes: {
+ navigator: React.PropTypes.object,
+ },
+
+ componentWillMount: function() {
+ this.navigator = getNavigatorContext(this);
+ },
+
+ componentDidMount: function() {
+ this.navigator.setHandler(this._navigatorHandleRequest);
+ },
+
+ childContextTypes: {
+ navigator: React.PropTypes.object,
+ },
+
+ getChildContext: function() {
+ return {
+ navigator: {
+ ...this.navigator,
+ setHandler: (handler) => {
+ this._childNavigationHandler = handler;
+ },
+ }
+ };
+ },
+
+ componentWillUnmount: function() {
+ this.navigator.setHandler(null);
+ },
+
+ _navigatorHandleRequest: function(action, arg1, arg2) {
+ if (this._interceptorHandle(action, arg1, arg2)) {
+ return true;
+ }
+ if (this._childNavigationHandler && this._childNavigationHandler(action, arg1, arg2)) {
+ return true;
+ }
+ },
+
+ _interceptorHandle: function(action, arg1, arg2) {
+ if (this.props.onRequest && this.props.onRequest(action, arg1, arg2)) {
+ return true;
+ }
+ switch (action) {
+ case 'pop':
+ return this.props.onPopRequest && this.props.onPopRequest(action, arg1, arg2);
+ case 'push':
+ return this.props.onPushRequest && this.props.onPushRequest(action, arg1, arg2);
+ default:
+ return false;
+ }
+ },
+
+ render: function() {
+ return this.props.children;
+ },
+
+});
+
+module.exports = NavigatorInterceptor;
diff --git a/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js b/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js
new file mode 100644
index 000000000..ded8048e4
--- /dev/null
+++ b/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2015, Facebook, Inc. All rights reserved.
+ *
+ * Facebook, Inc. (“Facebook”) owns all right, title and interest, including
+ * all intellectual property and other proprietary rights, in and to the React
+ * Native CustomComponents software (the “Software”). Subject to your
+ * compliance with these terms, you are hereby granted a non-exclusive,
+ * worldwide, royalty-free copyright license to (1) use and copy the Software;
+ * and (2) reproduce and distribute the Software as part of your own software
+ * (“Your Software”). Facebook reserves all rights not expressly granted to
+ * you in this license agreement.
+ *
+ * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
+ * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
+ * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @providesModule NavigatorStaticContextContainer
+ */
+'use strict';
+
+var React = require('React');
+var StaticContainer = require('StaticContainer.react');
+
+var PropTypes = React.PropTypes;
+
+var NavigatorStaticContextContainer = React.createClass({
+
+ childContextTypes: {
+ navigator: PropTypes.object,
+ },
+
+ getChildContext: function() {
+ return {
+ navigator: this.props.navigatorContext,
+ };
+ },
+
+ render: function() {
+ return (
+
+ );
+ },
+});
+
+module.exports = NavigatorStaticContextContainer;
diff --git a/Libraries/CustomComponents/Navigator/getNavigatorContext.js b/Libraries/CustomComponents/Navigator/getNavigatorContext.js
new file mode 100644
index 000000000..0c169fe0f
--- /dev/null
+++ b/Libraries/CustomComponents/Navigator/getNavigatorContext.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2015, Facebook, Inc. All rights reserved.
+ *
+ * Facebook, Inc. (“Facebook”) owns all right, title and interest, including
+ * all intellectual property and other proprietary rights, in and to the React
+ * Native CustomComponents software (the “Software”). Subject to your
+ * compliance with these terms, you are hereby granted a non-exclusive,
+ * worldwide, royalty-free copyright license to (1) use and copy the Software;
+ * and (2) reproduce and distribute the Software as part of your own software
+ * (“Your Software”). Facebook reserves all rights not expressly granted to
+ * you in this license agreement.
+ *
+ * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
+ * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
+ * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @providesModule getNavigatorContext
+ */
+'use strict';
+
+
+var ReactInstanceMap = require('ReactInstanceMap');
+
+function getNavigatorContext(el) {
+ // TODO (t6707746): replace with `el.context.navigator` when parent context is supported
+ return ReactInstanceMap.get(el)._context.navigator;
+}
+
+module.exports = getNavigatorContext;
diff --git a/Libraries/Image/ImageSource.js b/Libraries/Image/ImageSource.js
new file mode 100644
index 000000000..c9b3dcac4
--- /dev/null
+++ b/Libraries/Image/ImageSource.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ImageSource
+ * @flow
+ */
+'use strict';
+
+type ImageSource = {
+ uri: string;
+ isStatic: boolean;
+};
diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m
index 3eb5c1b78..9577ce4e5 100644
--- a/Libraries/Image/RCTImageDownloader.m
+++ b/Libraries/Image/RCTImageDownloader.m
@@ -77,13 +77,14 @@ static NSString *RCTCacheKeyForURL(NSURL *url)
__weak RCTImageDownloader *weakSelf = self;
RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSData *data, NSError *error) {
-
- RCTImageDownloader *strongSelf = weakSelf;
- NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
- [strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
- for (RCTCachedDataDownloadBlock block in blocks) {
- block(cached, data, error);
- }
+ dispatch_async(_processingQueue, ^{
+ RCTImageDownloader *strongSelf = weakSelf;
+ NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
+ [strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
+ for (RCTCachedDataDownloadBlock block in blocks) {
+ block(cached, data, error);
+ }
+ });
};
if ([_cache hasDataForKey:cacheKey]) {
diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m
index ddf973b24..4d6aba5cc 100644
--- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m
+++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m
@@ -119,7 +119,9 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
[_jsQueue addOperationWithBlock:^{
if (!self.valid) {
- NSError *error = [NSError errorWithDomain:@"WS" code:1 userInfo:@{NSLocalizedDescriptionKey:@"socket closed"}];
+ NSError *error = [NSError errorWithDomain:@"WS" code:1 userInfo:@{
+ NSLocalizedDescriptionKey: @"socket closed"
+ }];
callback(error, nil);
return;
}
diff --git a/Libraries/Storage/AsyncStorage.ios.js b/Libraries/Storage/AsyncStorage.ios.js
index aa48e40de..5ee2dc5e3 100644
--- a/Libraries/Storage/AsyncStorage.ios.js
+++ b/Libraries/Storage/AsyncStorage.ios.js
@@ -27,49 +27,73 @@ var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncLocalStorage;
* operates globally.
*
* This JS code is a simple facad over the native iOS implementation to provide
- * a clear JS API, real Error objects, and simple non-multi functions.
+ * a clear JS API, real Error objects, and simple non-multi functions. Each
+ * method returns a `Promise` object.
*/
var AsyncStorage = {
/**
* Fetches `key` and passes the result to `callback`, along with an `Error` if
- * there is any.
+ * there is any. Returns a `Promise` object.
*/
getItem: function(
key: string,
callback: (error: ?Error, result: ?string) => void
- ): void {
- RCTAsyncStorage.multiGet([key], function(errors, result) {
- // Unpack result to get value from [[key,value]]
- var value = (result && result[0] && result[0][1]) ? result[0][1] : null;
- callback((errors && convertError(errors[0])) || null, value);
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiGet([key], function(errors, result) {
+ // Unpack result to get value from [[key,value]]
+ var value = (result && result[0] && result[0][1]) ? result[0][1] : null;
+ callback && callback((errors && convertError(errors[0])) || null, value);
+ if (errors) {
+ reject(convertError(errors[0]));
+ } else {
+ resolve(value);
+ }
+ });
});
},
/**
* Sets `value` for `key` and calls `callback` on completion, along with an
- * `Error` if there is any.
+ * `Error` if there is any. Returns a `Promise` object.
*/
setItem: function(
key: string,
value: string,
callback: ?(error: ?Error) => void
- ): void {
- RCTAsyncStorage.multiSet([[key,value]], function(errors) {
- callback && callback((errors && convertError(errors[0])) || null);
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiSet([[key,value]], function(errors) {
+ callback && callback((errors && convertError(errors[0])) || null);
+ if (errors) {
+ reject(convertError(errors[0]));
+ } else {
+ resolve(null);
+ }
+ });
});
},
-
+ /**
+ * Returns a `Promise` object.
+ */
removeItem: function(
key: string,
callback: ?(error: ?Error) => void
- ): void {
- RCTAsyncStorage.multiRemove([key], function(errors) {
- callback && callback((errors && convertError(errors[0])) || null);
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiRemove([key], function(errors) {
+ callback && callback((errors && convertError(errors[0])) || null);
+ if (errors) {
+ reject(convertError(errors[0]));
+ } else {
+ resolve(null);
+ }
+ });
});
},
/**
- * Merges existing value with input value, assuming they are stringified json.
+ * Merges existing value with input value, assuming they are stringified json. Returns a `Promise` object.
*
* Not supported by all native implementations.
*/
@@ -77,29 +101,50 @@ var AsyncStorage = {
key: string,
value: string,
callback: ?(error: ?Error) => void
- ): void {
- RCTAsyncStorage.multiMerge([[key,value]], function(errors) {
- callback && callback((errors && convertError(errors[0])) || null);
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiMerge([[key,value]], function(errors) {
+ callback && callback((errors && convertError(errors[0])) || null);
+ if (errors) {
+ reject(convertError(errors[0]));
+ } else {
+ resolve(null);
+ }
+ });
});
},
/**
* Erases *all* AsyncStorage for all clients, libraries, etc. You probably
* don't want to call this - use removeItem or multiRemove to clear only your
- * own keys instead.
+ * own keys instead. Returns a `Promise` object.
*/
- clear: function(callback: ?(error: ?Error) => void) {
- RCTAsyncStorage.clear(function(error) {
- callback && callback(convertError(error));
+ clear: function(callback: ?(error: ?Error) => void): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.clear(function(error) {
+ callback && callback(convertError(error));
+ if (error && convertError(error)){
+ reject(convertError(error));
+ } else {
+ resolve(null);
+ }
+ });
});
},
/**
- * Gets *all* keys known to the system, for all callers, libraries, etc.
+ * Gets *all* keys known to the system, for all callers, libraries, etc. Returns a `Promise` object.
*/
- getAllKeys: function(callback: (error: ?Error) => void) {
- RCTAsyncStorage.getAllKeys(function(error, keys) {
- callback(convertError(error), keys);
+ getAllKeys: function(callback: (error: ?Error) => void): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.getAllKeys(function(error, keys) {
+ callback && callback(convertError(error), keys);
+ if (error) {
+ reject(convertError(error));
+ } else {
+ resolve(keys);
+ }
+ });
});
},
@@ -115,67 +160,90 @@ var AsyncStorage = {
/**
* multiGet invokes callback with an array of key-value pair arrays that
- * matches the input format of multiSet.
+ * matches the input format of multiSet. Returns a `Promise` object.
*
* multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']])
*/
multiGet: function(
keys: Array,
callback: (errors: ?Array, result: ?Array>) => void
- ): void {
- RCTAsyncStorage.multiGet(keys, function(errors, result) {
- callback(
- (errors && errors.map((error) => convertError(error))) || null,
- result
- );
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiGet(keys, function(errors, result) {
+ var error = (errors && errors.map((error) => convertError(error))) || null;
+ callback && callback(error, result);
+ if (errors) {
+ reject(error);
+ } else {
+ resolve(result);
+ }
+ });
});
},
/**
* multiSet and multiMerge take arrays of key-value array pairs that match
- * the output of multiGet, e.g.
+ * the output of multiGet, e.g. Returns a `Promise` object.
*
* multiSet([['k1', 'val1'], ['k2', 'val2']], cb);
*/
multiSet: function(
keyValuePairs: Array>,
callback: ?(errors: ?Array) => void
- ): void {
- RCTAsyncStorage.multiSet(keyValuePairs, function(errors) {
- callback && callback(
- (errors && errors.map((error) => convertError(error))) || null
- );
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiSet(keyValuePairs, function(errors) {
+ var error = (errors && errors.map((error) => convertError(error))) || null;
+ callback && callback(error);
+ if (errors) {
+ reject(error);
+ } else {
+ resolve(null);
+ }
+ });
});
},
/**
- * Delete all the keys in the `keys` array.
+ * Delete all the keys in the `keys` array. Returns a `Promise` object.
*/
multiRemove: function(
keys: Array,
callback: ?(errors: ?Array) => void
- ): void {
- RCTAsyncStorage.multiRemove(keys, function(errors) {
- callback && callback(
- (errors && errors.map((error) => convertError(error))) || null
- );
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiRemove(keys, function(errors) {
+ var error = (errors && errors.map((error) => convertError(error))) || null;
+ callback && callback(error);
+ if (errors) {
+ reject(error);
+ } else {
+ resolve(null);
+ }
+ });
});
},
/**
* Merges existing values with input values, assuming they are stringified
- * json.
+ * json. Returns a `Promise` object.
*
* Not supported by all native implementations.
*/
multiMerge: function(
keyValuePairs: Array>,
callback: ?(errors: ?Array) => void
- ): void {
- RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) {
- callback && callback(
- (errors && errors.map((error) => convertError(error))) || null
- );
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) {
+ var error = (errors && errors.map((error) => convertError(error))) || null;
+ callback && callback(error);
+ if (errors) {
+ reject(error);
+ } else {
+ resolve(null);
+ }
+ });
});
},
};
diff --git a/Libraries/Text/RCTRawTextManager.m b/Libraries/Text/RCTRawTextManager.m
index 3215b36c1..ab856d049 100644
--- a/Libraries/Text/RCTRawTextManager.m
+++ b/Libraries/Text/RCTRawTextManager.m
@@ -15,7 +15,7 @@
- (UIView *)view
{
- return [[UIView alloc] init];
+ return nil;
}
- (RCTShadowView *)shadowView
@@ -26,4 +26,3 @@
RCT_EXPORT_SHADOW_PROPERTY(text, NSString)
@end
-
diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h
index 82ea2b632..286edb53a 100644
--- a/Libraries/Text/RCTShadowText.h
+++ b/Libraries/Text/RCTShadowText.h
@@ -23,7 +23,7 @@ extern NSString *const RCTReactTagAttributeName;
@property (nonatomic, copy) NSString *fontStyle;
@property (nonatomic, assign) BOOL isHighlighted;
@property (nonatomic, assign) CGFloat lineHeight;
-@property (nonatomic, assign) NSInteger maxNumberOfLines;
+@property (nonatomic, assign) NSUInteger maximumNumberOfLines;
@property (nonatomic, assign) CGSize shadowOffset;
@property (nonatomic, assign) NSTextAlignment textAlign;
@@ -31,6 +31,8 @@ extern NSString *const RCTReactTagAttributeName;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) NSLineBreakMode truncationMode;
-- (NSAttributedString *)attributedString;
+@property (nonatomic, copy, readonly) NSAttributedString *attributedString;
+@property (nonatomic, strong, readonly) NSLayoutManager *layoutManager;
+@property (nonatomic, strong, readonly) NSTextContainer *textContainer;
@end
diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m
index da4b90c06..4201b1b4e 100644
--- a/Libraries/Text/RCTShadowText.m
+++ b/Libraries/Text/RCTShadowText.m
@@ -20,9 +20,17 @@ NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName";
static css_dim_t RCTMeasure(void *context, float width)
{
RCTShadowText *shadowText = (__bridge RCTShadowText *)context;
- CGSize computedSize = [[shadowText attributedString] boundingRectWithSize:(CGSize){isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX}
- options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
- context:nil].size;
+
+ NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[shadowText attributedString]];
+ [textStorage addLayoutManager:shadowText.layoutManager];
+
+ shadowText.textContainer.size = CGSizeMake(isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX);
+ shadowText.layoutManager.textStorage = textStorage;
+ [shadowText.layoutManager ensureLayoutForTextContainer:shadowText.textContainer];
+
+ CGSize computedSize = [shadowText.layoutManager usedRectForTextContainer:shadowText.textContainer].size;
+
+ [textStorage removeLayoutManager:shadowText.layoutManager];
css_dim_t result;
result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width);
@@ -30,8 +38,9 @@ static css_dim_t RCTMeasure(void *context, float width)
return result;
}
-@implementation RCTShadowText
-{
+@implementation RCTShadowText {
+ NSLayoutManager *_layoutManager;
+ NSTextContainer *_textContainer;
NSAttributedString *_cachedAttributedString;
UIFont *_font;
}
@@ -41,7 +50,15 @@ static css_dim_t RCTMeasure(void *context, float width)
if ((self = [super init])) {
_fontSize = NAN;
_isHighlighted = NO;
+
+ _textContainer = [[NSTextContainer alloc] init];
+ _textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
+ _textContainer.lineFragmentPadding = 0.0;
+
+ _layoutManager = [[NSLayoutManager alloc] init];
+ [_layoutManager addTextContainer:_textContainer];
}
+
return self;
}
@@ -201,11 +218,31 @@ RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *);
RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat);
RCT_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *);
RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat);
-RCT_TEXT_PROPERTY(MaxNumberOfLines, _maxNumberOfLines, NSInteger);
RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize);
RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment);
-RCT_TEXT_PROPERTY(TruncationMode, _truncationMode, NSLineBreakMode);
RCT_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL);
RCT_TEXT_PROPERTY(Font, _font, UIFont *);
+- (NSLineBreakMode)truncationMode
+{
+ return _textContainer.lineBreakMode;
+}
+
+- (void)setTruncationMode:(NSLineBreakMode)truncationMode
+{
+ _textContainer.lineBreakMode = truncationMode;
+ [self dirtyText];
+}
+
+- (NSUInteger)maximumNumberOfLines
+{
+ return _textContainer.maximumNumberOfLines;
+}
+
+- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines
+{
+ _textContainer.maximumNumberOfLines = maximumNumberOfLines;
+ [self dirtyText];
+}
+
@end
diff --git a/Libraries/Text/RCTText.h b/Libraries/Text/RCTText.h
index 59b15668a..24b98e991 100644
--- a/Libraries/Text/RCTText.h
+++ b/Libraries/Text/RCTText.h
@@ -11,9 +11,10 @@
@interface RCTText : UIView
+@property (nonatomic, strong) NSLayoutManager *layoutManager;
+@property (nonatomic, strong) NSTextContainer *textContainer;
@property (nonatomic, copy) NSAttributedString *attributedText;
-@property (nonatomic, assign) NSLineBreakMode lineBreakMode;
-@property (nonatomic, assign) NSUInteger numberOfLines;
+
@property (nonatomic, assign) UIEdgeInsets contentInset;
@end
diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m
index a2f7f11cc..d6e00f1b6 100644
--- a/Libraries/Text/RCTText.m
+++ b/Libraries/Text/RCTText.m
@@ -23,15 +23,7 @@
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
- _textContainer = [[NSTextContainer alloc] init];
- _textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
- _textContainer.lineFragmentPadding = 0.0;
-
- _layoutManager = [[NSLayoutManager alloc] init];
- [_layoutManager addTextContainer:_textContainer];
-
_textStorage = [[NSTextStorage alloc] init];
- [_textStorage addLayoutManager:_layoutManager];
self.contentMode = UIViewContentModeRedraw;
}
@@ -50,25 +42,31 @@
[self setNeedsDisplay];
}
-- (NSUInteger)numberOfLines
+- (void)setTextContainer:(NSTextContainer *)textContainer
{
- return _textContainer.maximumNumberOfLines;
-}
+ if ([_textContainer isEqual:textContainer]) return;
+
+ _textContainer = textContainer;
+
+ for (NSInteger i = _layoutManager.textContainers.count - 1; i >= 0; i--) {
+ [_layoutManager removeTextContainerAtIndex:i];
+ }
+ [_layoutManager addTextContainer:_textContainer];
-- (void)setNumberOfLines:(NSUInteger)numberOfLines
-{
- _textContainer.maximumNumberOfLines = numberOfLines;
[self setNeedsDisplay];
}
-- (NSLineBreakMode)lineBreakMode
+- (void)setLayoutManager:(NSLayoutManager *)layoutManager
{
- return _textContainer.lineBreakMode;
-}
+ if ([_layoutManager isEqual:layoutManager]) return;
+
+ _layoutManager = layoutManager;
+
+ for (NSLayoutManager *existingLayoutManager in _textStorage.layoutManagers) {
+ [_textStorage removeLayoutManager:existingLayoutManager];
+ }
+ [_textStorage addLayoutManager:_layoutManager];
-- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode
-{
- _textContainer.lineBreakMode = lineBreakMode;
[self setNeedsDisplay];
}
diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m
index 8df67e59e..d19dfa71e 100644
--- a/Libraries/Text/RCTTextManager.m
+++ b/Libraries/Text/RCTTextManager.m
@@ -33,15 +33,6 @@
#pragma mark - View properties
RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor, UIColor)
-RCT_CUSTOM_VIEW_PROPERTY(numberOfLines, NSInteger, RCTText)
-{
- NSLineBreakMode truncationMode = NSLineBreakByClipping;
- view.numberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.numberOfLines;
- if (view.numberOfLines > 0) {
- truncationMode = NSLineBreakByTruncatingTail;
- }
- view.lineBreakMode = truncationMode;
-}
#pragma mark - Shadow properties
@@ -65,8 +56,8 @@ RCT_CUSTOM_SHADOW_PROPERTY(containerBackgroundColor, UIColor, RCTShadowText)
RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText)
{
NSLineBreakMode truncationMode = NSLineBreakByClipping;
- view.maxNumberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.maxNumberOfLines;
- if (view.maxNumberOfLines > 0) {
+ view.maximumNumberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.maximumNumberOfLines;
+ if (view.maximumNumberOfLines > 0) {
truncationMode = NSLineBreakByTruncatingTail;
}
view.truncationMode = truncationMode;
@@ -124,12 +115,16 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText)
};
}
-- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
+- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowView
{
NSNumber *reactTag = shadowView.reactTag;
UIEdgeInsets padding = shadowView.paddingAsInsets;
+
return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
- ((RCTText *)viewRegistry[reactTag]).contentInset = padding;
+ RCTText *text = (RCTText *)viewRegistry[reactTag];
+ text.contentInset = padding;
+ text.layoutManager = shadowView.layoutManager;
+ text.textContainer = shadowView.textContainer;
};
}
diff --git a/Libraries/Utilities/BackAndroid.ios.js b/Libraries/Utilities/BackAndroid.ios.js
new file mode 100644
index 000000000..c5a56f40e
--- /dev/null
+++ b/Libraries/Utilities/BackAndroid.ios.js
@@ -0,0 +1,21 @@
+/**
+ * iOS stub for BackAndroid.android.js
+ *
+ * @providesModule BackAndroid
+ */
+
+'use strict';
+
+var warning = require('warning');
+
+function platformWarn() {
+ warning(false, 'BackAndroid is not supported on this platform.');
+}
+
+var BackAndroid = {
+ exitApp: platformWarn,
+ addEventListener: platformWarn,
+ removeEventListener: platformWarn,
+};
+
+module.exports = BackAndroid;
diff --git a/Libraries/Utilities/Backstack.ios.js b/Libraries/Utilities/Backstack.ios.js
deleted file mode 100644
index 00a538668..000000000
--- a/Libraries/Utilities/Backstack.ios.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * To lower the risk of breaking things on iOS, we are stubbing out the
- * BackStack for now. See Backstack.android.js
- *
- * @providesModule Backstack
- */
-
-'use strict';
-
-var Backstack = {
- pushNavigation: () => {},
- resetToBefore: () => {},
- removeComponentHistory: () => {},
-};
-
-module.exports = Backstack;
diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h
index 05767533d..3f0ad735e 100644
--- a/React/Base/RCTBridge.h
+++ b/React/Base/RCTBridge.h
@@ -28,6 +28,11 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(void);
extern NSString *const RCTReloadBridge;
+/**
+ * This function returns the module name for a given class.
+ */
+extern NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass);
+
/**
* Async batched bridge used to communicate with the JavaScript application.
*/
@@ -81,14 +86,6 @@ extern NSString *const RCTReloadBridge;
*/
@property (nonatomic, readonly) dispatch_queue_t shadowQueue;
-/**
- * Global logging function that will print to both xcode and JS debugger consoles.
- *
- * NOTE: Use via RCTLog* macros defined in RCTLog.h
- * TODO (#5906496): should log function be exposed here, or could it be a module?
- */
-+ (void)log:(NSArray *)objects level:(NSString *)level;
-
@property (nonatomic, copy, readonly) NSDictionary *launchOptions;
diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m
index 1ad3d58b7..3bf23a5d3 100644
--- a/React/Base/RCTBridge.m
+++ b/React/Base/RCTBridge.m
@@ -22,6 +22,7 @@
#import "RCTJavaScriptLoader.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
+#import "RCTRedBox.h"
#import "RCTRootView.h"
#import "RCTSparseArray.h"
#import "RCTUtils.h"
@@ -41,10 +42,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
NSString *const RCTReloadBridge = @"RCTReloadBridge";
-/**
- * This function returns the module name for a given class.
- */
-static NSString *RCTModuleNameForClass(Class cls)
+NSString *RCTBridgeModuleNameForClass(Class cls)
{
return [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls);
}
@@ -92,7 +90,7 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void)
[(NSMutableArray *)modules addObject:cls];
// Add module name
- NSString *moduleName = RCTModuleNameForClass(cls);
+ NSString *moduleName = RCTBridgeModuleNameForClass(cls);
[(NSMutableArray *)RCTModuleNamesByID addObject:moduleName];
}
});
@@ -187,7 +185,7 @@ static Class _globalExecutorClass;
RCT_ARG_BLOCK( \
if (json && ![json isKindOfClass:[_class class]]) { \
RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \
- json, RCTModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \
+ json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \
return; \
} \
_logic \
@@ -203,7 +201,7 @@ static Class _globalExecutorClass;
RCT_ARG_BLOCK( \
if (json && ![json respondsToSelector:@selector(_selector)]) { \
RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \
- index, json, RCTModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \
+ index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \
return; \
} \
_type value = [json _selector]; \
@@ -231,7 +229,7 @@ static Class _globalExecutorClass;
RCT_ARG_BLOCK(
if (json && ![json isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index,
- json, RCTModuleNameForClass(_moduleClass), _JSMethodName);
+ json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
return;
}
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
@@ -268,7 +266,7 @@ static Class _globalExecutorClass;
// Safety check
if (arguments.count != _argumentBlocks.count) {
RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd",
- RCTModuleNameForClass(_moduleClass), _JSMethodName,
+ RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
arguments.count, _argumentBlocks.count);
return;
}
@@ -544,7 +542,7 @@ static id _latestJSExecutor;
// Register passed-in module instances
NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
for (id module in _moduleProvider ? _moduleProvider() : nil) {
- preregisteredModules[RCTModuleNameForClass([module class])] = module;
+ preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module;
}
// Instantiate modules
@@ -895,27 +893,18 @@ static id _latestJSExecutor;
return (_latestJSExecutor != nil && [_latestJSExecutor isValid]);
}
-+ (void)log:(NSArray *)objects level:(NSString *)level
++ (void)logMessage:(NSString *)message level:(NSString *)level
{
if (!_latestJSExecutor || ![_latestJSExecutor isValid]) {
- RCTLogError(@"ERROR: No valid JS executor to log %@.", objects);
+ RCTLogError(@"ERROR: No valid JS executor to log '%@'.", message);
return;
}
- NSMutableArray *args = [NSMutableArray arrayWithObject:level];
- // TODO (#5906496): Find out and document why we skip the first object
- for (id ob in [objects subarrayWithRange:(NSRange){1, [objects count] - 1}]) {
- if ([NSJSONSerialization isValidJSONObject:@[ob]]) {
- [args addObject:ob];
- } else {
- [args addObject:[ob description]];
- }
- }
-
- // Note: the js executor could get invalidated while we're trying to call this...need to watch out for that.
+ // Note: the js executor could get invalidated while we're trying to call
+ // this...need to watch out for that.
[_latestJSExecutor executeJSCall:@"RCTLog"
method:@"logIfNoNativeHook"
- arguments:args
+ arguments:@[level, message]
callback:^(id json, NSError *error) {}];
}
diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h
index 666ce013e..7e573370e 100644
--- a/React/Base/RCTConvert.h
+++ b/React/Base/RCTConvert.h
@@ -145,7 +145,7 @@ RCT_CUSTOM_CONVERTER(type, name, [json getter])
} \
@catch (__unused NSException *e) { \
RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \
- json, [json class], #type); \
+ json, [json classForCoder], #type); \
json = nil; \
return code; \
} \
@@ -181,7 +181,8 @@ RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter])
return default; \
} \
if (![json isKindOfClass:[NSString class]]) { \
- RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \
+ RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", \
+ #type, [json classForCoder], json); \
} \
id value = mapping[json]; \
if(!value && [json description].length > 0) { \
diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m
index 5fbc2d527..1ddc9884c 100644
--- a/React/Base/RCTConvert.m
+++ b/React/Base/RCTConvert.m
@@ -45,7 +45,7 @@ RCT_CONVERTER(NSString *, NSString, description)
}
return number;
} else if (json && json != [NSNull null]) {
- RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json class]);
+ RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]);
}
return nil;
}
@@ -53,7 +53,7 @@ RCT_CONVERTER(NSString *, NSString, description)
+ (NSURL *)NSURL:(id)json
{
if (![json isKindOfClass:[NSString class]]) {
- RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json class], json);
+ RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json);
return nil;
}
@@ -98,7 +98,7 @@ RCT_CONVERTER(NSString *, NSString, description)
}
return date;
} else if (json && json != [NSNull null]) {
- RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json class]);
+ RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]);
}
return nil;
}
@@ -223,7 +223,7 @@ RCT_ENUM_CONVERTER(UIBarStyle, (@{
json = [json mutableCopy]; \
for (NSString *alias in aliases) { \
NSString *key = aliases[alias]; \
- NSNumber *number = json[key]; \
+ NSNumber *number = json[alias]; \
if (number) { \
RCTLogWarn(@"Using deprecated '%@' property for '%s'. Use '%@' instead.", alias, #type, key); \
((NSMutableDictionary *)json)[key] = number; \
@@ -234,7 +234,8 @@ RCT_ENUM_CONVERTER(UIBarStyle, (@{
((CGFloat *)&result)[i] = [self CGFloat:json[fields[i]]]; \
} \
} else if (json && json != [NSNull null]) { \
- RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \
+ RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", \
+ #type, [json classForCoder], json); \
} \
return result; \
} \
@@ -511,8 +512,8 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
} else if (json && ![json isKindOfClass:[NSNull class]]) {
- RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, \
- received %@: %@", [json class], json);
+ RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, received %@: %@",
+ [json classForCoder], json);
}
// Default color
@@ -538,7 +539,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
// image itself) so as to reduce overhead on subsequent checks of the same input
if (![json isKindOfClass:[NSString class]]) {
- RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json class], json);
+ RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json classForCoder], json);
return nil;
}
diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h
index 9698e6476..c30da141b 100644
--- a/React/Base/RCTLog.h
+++ b/React/Base/RCTLog.h
@@ -7,58 +7,138 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
+#import
+
#import "RCTAssert.h"
-#import "RCTRedBox.h"
-
-#define RCTLOG_INFO 1
-#define RCTLOG_WARN 2
-#define RCTLOG_ERROR 3
-#define RCTLOG_MUSTFIX 4
-
-// If set to e.g. `RCTLOG_ERROR`, will assert after logging the first error.
-#if DEBUG
-#define RCTLOG_FATAL_LEVEL RCTLOG_MUSTFIX
-#define RCTLOG_REDBOX_LEVEL RCTLOG_ERROR
-#else
-#define RCTLOG_FATAL_LEVEL (RCTLOG_MUSTFIX + 1)
-#define RCTLOG_REDBOX_LEVEL (RCTLOG_MUSTFIX + 1)
-#endif
-
-// If defined, only log messages that match this regex will fatal
-#define RCTLOG_FATAL_REGEX nil
-
-extern __unsafe_unretained NSString *RCTLogLevels[];
-
-#define _RCTLog(_level, ...) do { \
- NSString *__RCTLog__levelStr = RCTLogLevels[_level - 1]; \
- NSString *__RCTLog__msg = RCTLogObjects(RCTLogFormat(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__), __RCTLog__levelStr); \
- if (_level >= RCTLOG_FATAL_LEVEL) { \
- BOOL __RCTLog__fail = YES; \
- if (RCTLOG_FATAL_REGEX) { \
- NSRegularExpression *__RCTLog__regex = [NSRegularExpression regularExpressionWithPattern:RCTLOG_FATAL_REGEX options:0 error:NULL]; \
- __RCTLog__fail = [__RCTLog__regex numberOfMatchesInString:__RCTLog__msg options:0 range:NSMakeRange(0, [__RCTLog__msg length])] > 0; \
- } \
- RCTCAssert(!__RCTLog__fail, @"RCTLOG_FATAL_LEVEL %@: %@", __RCTLog__levelStr, __RCTLog__msg); \
- } \
- if (_level >= RCTLOG_REDBOX_LEVEL) { \
- [[RCTRedBox sharedInstance] showErrorMessage:__RCTLog__msg]; \
- } \
-} while (0)
-
-#define RCTLog(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__)
-#define RCTLogInfo(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__)
-#define RCTLogWarn(...) _RCTLog(RCTLOG_WARN, __VA_ARGS__)
-#define RCTLogError(...) _RCTLog(RCTLOG_ERROR, __VA_ARGS__)
-#define RCTLogMustFix(...) _RCTLog(RCTLOG_MUSTFIX, __VA_ARGS__)
#ifdef __cplusplus
extern "C" {
#endif
-NSString *RCTLogObjects(NSArray *objects, NSString *level);
-NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5);
+/**
+ * Thresholds for logs to raise an assertion, or display redbox, respectively.
+ * You can override these values when debugging in order to tweak the default
+ * logging behavior.
+ */
+#define RCTLOG_FATAL_LEVEL RCTLogLevelMustFix
+#define RCTLOG_REDBOX_LEVEL RCTLogLevelError
-void RCTInjectLogFunction(void (^logFunction)(NSString *msg));
+/**
+ * A regular expression that can be used to selectively limit the throwing of
+ * a exception to specific log contents.
+ */
+#define RCTLOG_FATAL_REGEX nil
+
+/**
+ * An enum representing the severity of the log message.
+ */
+typedef NS_ENUM(NSInteger, RCTLogLevel) {
+ RCTLogLevelInfo = 1,
+ RCTLogLevelWarning = 2,
+ RCTLogLevelError = 3,
+ RCTLogLevelMustFix = 4
+};
+
+/**
+ * A block signature to be used for custom logging functions. In most cases you
+ * will want to pass these arguments to the RCTFormatLog function in order to
+ * generate a string, or use the RCTSimpleLogFunction() constructor to register
+ * a simple function that does not use all of the arguments.
+ */
+typedef void (^RCTLogFunction)(
+ RCTLogLevel level,
+ NSString *fileName,
+ NSNumber *lineNumber,
+ NSString *message
+);
+
+/**
+ * A method to generate a string from a collection of log data. To omit any
+ * particular data from the log, just pass nil or zero for the argument.
+ */
+NSString *RCTFormatLog(
+ NSDate *timestamp,
+ NSThread *thread,
+ RCTLogLevel level,
+ NSString *fileName,
+ NSNumber *lineNumber,
+ NSString *message
+);
+
+/**
+ * A method to generate a log function from a block with a much simpler
+ * template. The message passed to the simpler block is equivalent to the
+ * output of the RCTFormatLog() function.
+ */
+RCTLogFunction RCTSimpleLogFunction(void (^logFunction)(RCTLogLevel level, NSString *message));
+
+/**
+ * The default logging function used by RCTLogXX.
+ */
+extern RCTLogFunction RCTDefaultLogFunction;
+
+/**
+ * These methods get and set the current logging threshold. This is the level
+ * below which logs will be ignored. Default is RCTLogLevelInfo for debug and
+ * RCTLogLevelError for production.
+ */
+void RCTSetLogThreshold(RCTLogLevel threshold);
+RCTLogLevel RCTGetLogThreshold(void);
+
+/**
+ * These methods get and set the current logging function called by the RCTLogXX
+ * macros. You can use these to replace the standard behavior with custom log
+ * functionality.
+ */
+void RCTSetLogFunction(RCTLogFunction logFunction);
+RCTLogFunction RCTGetLogFunction(void);
+
+/**
+ * This appends additional code to the existing log function, without replacing
+ * the existing functionality. Useful if you just want to forward logs to an
+ * extra service without changing the default behavior.
+ */
+void RCTAddLogFunction(RCTLogFunction logFunction);
+
+/**
+ * This method adds a conditional prefix to any messages logged within the scope
+ * of the passed block. This is useful for adding additional context to log
+ * messages. The block will be performed synchronously on the current thread.
+ */
+void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix);
+
+/**
+ * Private logging functions - ignore these.
+ */
+void _RCTLogFormat(RCTLogLevel, const char *, int, NSString *, ...) NS_FORMAT_FUNCTION(4,5);
+#define _RCTLog(lvl, ...) do { \
+ NSString *msg = [NSString stringWithFormat:__VA_ARGS__]; \
+ if (lvl >= RCTLOG_FATAL_LEVEL) { \
+ BOOL fail = YES; \
+ if (RCTLOG_FATAL_REGEX) { \
+ if ([msg rangeOfString:RCTLOG_FATAL_REGEX options:NSRegularExpressionSearch].length) { \
+ fail = NO; \
+ } \
+ } \
+ RCTCAssert(!fail, @"FATAL ERROR: %@", msg); \
+ }\
+ _RCTLogFormat(lvl, __FILE__, __LINE__, __VA_ARGS__); \
+} while (0)
+
+/**
+ * Legacy injection function - don't use this.
+ */
+void RCTInjectLogFunction(void (^)(NSString *msg));
+
+/**
+ * Logging macros. Use these to log information, warnings and errors in your
+ * own code.
+ */
+#define RCTLog(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__)
+#define RCTLogInfo(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__)
+#define RCTLogWarn(...) _RCTLog(RCTLogLevelWarning, __VA_ARGS__)
+#define RCTLogError(...) _RCTLog(RCTLogLevelError, __VA_ARGS__)
+#define RCTLogMustFix(...) _RCTLog(RCTLogLevelMustFix, __VA_ARGS__)
#ifdef __cplusplus
}
diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m
index cd035eda2..d2de897f1 100644
--- a/React/Base/RCTLog.m
+++ b/React/Base/RCTLog.m
@@ -9,85 +9,218 @@
#import "RCTLog.h"
+#import "RCTAssert.h"
#import "RCTBridge.h"
+#import "RCTRedBox.h"
-__unsafe_unretained NSString *RCTLogLevels[] = {
- @"info",
- @"warn",
- @"error",
- @"mustfix"
+@interface RCTBridge (Logging)
+
++ (void)logMessage:(NSString *)message level:(NSString *)level;
+
+@end
+
+static NSString *const RCTLogPrefixStack = @"RCTLogPrefixStack";
+
+const char *RCTLogLevels[] = {
+ "info",
+ "warn",
+ "error",
+ "mustfix"
};
-static void (^RCTInjectedLogFunction)(NSString *msg);
+static RCTLogFunction RCTCurrentLogFunction;
+static RCTLogLevel RCTCurrentLogThreshold;
-void RCTInjectLogFunction(void (^logFunction)(NSString *msg)) {
- RCTInjectedLogFunction = logFunction;
-}
-
-static inline NSString *_RCTLogPreamble(const char *file, int lineNumber, const char *funcName)
+void RCTLogSetup(void) __attribute__((constructor));
+void RCTLogSetup()
{
- NSString *threadName = [[NSThread currentThread] name];
- NSString *fileName=[[NSString stringWithUTF8String:file] lastPathComponent];
- if (!threadName || threadName.length <= 0) {
- threadName = [NSString stringWithFormat:@"%p", [NSThread currentThread]];
- }
- return [NSString stringWithFormat:@"[RCTLog][tid:%@][%@:%d]>", threadName, fileName, lineNumber];
-}
+ RCTCurrentLogFunction = RCTDefaultLogFunction;
-// TODO (#5906496): Does this need to be tied to RCTBridge?
-NSString *RCTLogObjects(NSArray *objects, NSString *level)
-{
- NSString *str = objects[0];
-#if TARGET_IPHONE_SIMULATOR
- if ([RCTBridge hasValidJSExecutor]) {
- fprintf(stderr, "%s\n", [str UTF8String]); // don't print timestamps and other junk
- [RCTBridge log:objects level:level];
- } else
+#if DEBUG
+ RCTCurrentLogThreshold = RCTLogLevelInfo - 1;
+#else
+ RCTCurrentLogThreshold = RCTLogLevelError;
#endif
- {
- // Print normal errors with timestamps when not in simulator.
- // Non errors are already compiled out above, so log as error here.
- if (RCTInjectedLogFunction) {
- RCTInjectedLogFunction(str);
- } else {
- NSLog(@">\n %@", str);
- }
- }
- return str;
+
}
-// Returns array of objects. First arg is a simple string to print, remaining args
-// are objects to pass through to the debugger so they are inspectable in the console.
-NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...)
+RCTLogFunction RCTDefaultLogFunction = ^(
+ RCTLogLevel level,
+ NSString *fileName,
+ NSNumber *lineNumber,
+ NSString *message
+)
{
- va_list args;
- va_start(args, format);
- NSString *preamble = _RCTLogPreamble(file, lineNumber, funcName);
+ NSString *log = RCTFormatLog(
+ [NSDate date], [NSThread currentThread], level, fileName, lineNumber, message
+ );
+ fprintf(stderr, "%s\n", log.UTF8String);
+};
- // Pull out NSObjects so we can pass them through as inspectable objects to the js debugger
- NSArray *formatParts = [format componentsSeparatedByString:@"%"];
- NSMutableArray *objects = [NSMutableArray arrayWithObject:preamble];
- BOOL valid = YES;
- for (int i = 0; i < formatParts.count; i++) {
- if (i == 0) { // first part is always a string
- [objects addObject:formatParts[i]];
+void RCTSetLogFunction(RCTLogFunction logFunction)
+{
+ RCTCurrentLogFunction = logFunction;
+}
+
+RCTLogFunction RCTGetLogFunction()
+{
+ return RCTCurrentLogFunction;
+}
+
+void RCTAddLogFunction(RCTLogFunction logFunction)
+{
+ RCTLogFunction existing = RCTCurrentLogFunction;
+ if (existing) {
+ RCTCurrentLogFunction = ^(RCTLogLevel level,
+ NSString *fileName,
+ NSNumber *lineNumber,
+ NSString *message) {
+
+ existing(level, fileName, lineNumber, message);
+ logFunction(level, fileName, lineNumber, message);
+ };
+ } else {
+ RCTCurrentLogFunction = logFunction;
+ }
+}
+
+void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix)
+{
+ NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary;
+ NSMutableArray *prefixStack = threadDictionary[RCTLogPrefixStack];
+ if (!prefixStack) {
+ prefixStack = [[NSMutableArray alloc] init];
+ threadDictionary[RCTLogPrefixStack] = prefixStack;
+ }
+ [prefixStack addObject:prefix];
+ block();
+ [prefixStack removeLastObject];
+}
+
+NSString *RCTFormatLog(
+ NSDate *timestamp,
+ NSThread *thread,
+ RCTLogLevel level,
+ NSString *fileName,
+ NSNumber *lineNumber,
+ NSString *message
+)
+{
+ NSMutableString *log = [[NSMutableString alloc] init];
+ if (timestamp) {
+ static NSDateFormatter *formatter;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ formatter = [[NSDateFormatter alloc] init];
+ formatter.dateFormat = formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS ";
+ });
+ [log appendString:[formatter stringFromDate:timestamp]];
+ }
+ [log appendString:@"[react]"];
+ if (level) {
+ [log appendFormat:@"[%s]", RCTLogLevels[level - 1]];
+ }
+ if (thread) {
+ NSString *threadName = thread.name;
+ if (threadName.length == 0) {
+#if DEBUG
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ threadName = @(dispatch_queue_get_label(dispatch_get_current_queue()));
+#pragma clang diagnostic pop
+#else
+ threadName = [NSString stringWithFormat:@"%p", thread];
+#endif
+ }
+ [log appendFormat:@"[tid:%@]", threadName];
+ }
+ if (fileName) {
+ fileName = [fileName lastPathComponent];
+ if (lineNumber) {
+ [log appendFormat:@"[%@:%@]", fileName, lineNumber];
} else {
- if (valid && [formatParts[i] length] && [formatParts[i] characterAtIndex:0] == '@') {
- id obj = va_arg(args, id);
- [objects addObject:obj ?: @"null"];
- [objects addObject:[formatParts[i] substringFromIndex:1]]; // remove formatting char
- } else {
- // We could determine the type (double, int?) of the va_arg by parsing the formatPart, but for now we just bail.
- valid = NO;
- [objects addObject:[NSString stringWithFormat:@"unknown object for %%%@", formatParts[i]]];
- }
+ [log appendFormat:@"[%@]", fileName];
}
}
- va_end(args);
- va_start(args, format);
- NSString *strOut = [preamble stringByAppendingString:[[NSString alloc] initWithFormat:format arguments:args]];
- va_end(args);
- NSMutableArray *objectsOut = [NSMutableArray arrayWithObject:strOut];
- [objectsOut addObjectsFromArray:objects];
- return objectsOut;
+ if (message) {
+ [log appendString:@" "];
+ [log appendString:message];
+ }
+ return log;
+}
+
+RCTLogFunction RCTSimpleLogFunction(void (^logFunction)(RCTLogLevel level, NSString *message))
+{
+ return ^(RCTLogLevel level,
+ NSString *fileName,
+ NSNumber *lineNumber,
+ NSString *message) {
+
+ logFunction(level, RCTFormatLog(
+ [NSDate date], [NSThread currentThread], level, fileName, lineNumber, message
+ ));
+ };
+}
+
+void _RCTLogFormat(RCTLogLevel level, const char *fileName, int lineNumber, NSString *format, ...)
+{
+ if (RCTCurrentLogFunction && level >= RCTCurrentLogThreshold) {
+
+ // Get message
+ va_list args;
+ va_start(args, format);
+ __block NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
+ va_end(args);
+
+ // Add prefix
+ NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary;
+ NSArray *prefixStack = threadDictionary[RCTLogPrefixStack];
+ NSString *prefix = [prefixStack lastObject];
+ if (prefix) {
+ message = [prefix stringByAppendingString:message];
+ }
+
+ // Call log function
+ RCTCurrentLogFunction(
+ level, fileName ? @(fileName) : nil, (lineNumber >= 0) ? @(lineNumber) : nil, message
+ );
+
+#if DEBUG
+
+ // Log to red box
+ if (level >= RCTLOG_REDBOX_LEVEL) {
+ [[RCTRedBox sharedInstance] showErrorMessage:message];
+ }
+
+ // Log to JS executor
+ if ([RCTBridge hasValidJSExecutor]) {
+ [RCTBridge logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"];
+ }
+
+#endif
+
+ }
+}
+
+#pragma mark - Deprecated
+
+void RCTInjectLogFunction(void (^logFunction)(NSString *msg))
+{
+ RCTSetLogFunction(^(RCTLogLevel level,
+ NSString *fileName,
+ NSNumber *lineNumber,
+ NSString *message) {
+
+ if (level > RCTLogLevelError) {
+
+ // Use custom log function
+ NSString *loc = fileName ? [NSString stringWithFormat:@"[%@:%@] ", fileName, lineNumber] : @"";
+ logFunction([loc stringByAppendingString:message]);
+
+ } else if (RCTDefaultLogFunction && level >= RCTCurrentLogThreshold) {
+
+ // Use default logger
+ RCTDefaultLogFunction(level, fileName, lineNumber, message);
+ }
+ });
}
diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h
index b9e91b7a0..f85bb7ecb 100644
--- a/React/Base/RCTRootView.h
+++ b/React/Base/RCTRootView.h
@@ -17,6 +17,9 @@ extern NSString *const RCTReloadViewsNotification;
@interface RCTRootView : UIView
+/**
+ * - Designated initializer -
+ */
- (instancetype)initWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName NS_DESIGNATED_INITIALIZER;
@@ -39,6 +42,10 @@ extern NSString *const RCTReloadViewsNotification;
*/
@property (nonatomic, copy, readonly) NSString *moduleName;
+/**
+ * The bridge used by the root view. Bridges can be shared between multiple
+ * root views, so you can use this property to initialize another RCTRootView.
+ */
@property (nonatomic, strong, readonly) RCTBridge *bridge;
/**
diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m
index 9d913c048..6c15e509c 100644
--- a/React/Base/RCTRootView.m
+++ b/React/Base/RCTRootView.m
@@ -17,7 +17,6 @@
#import "RCTEventDispatcher.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
-#import "RCTRedBox.h"
#import "RCTSourceCode.h"
#import "RCTTouchHandler.h"
#import "RCTUIManager.h"
diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m
index e2b289f80..6c424ec7f 100644
--- a/React/Executors/RCTContextExecutor.m
+++ b/React/Executors/RCTContextExecutor.m
@@ -38,23 +38,18 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object,
if (!string) {
return JSValueMakeUndefined(context);
}
-
- NSString *str = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string);
- NSError *error = nil;
+ NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string);
+ JSStringRelease(string);
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
- error:&error];
- NSString *modifiedString = [regex stringByReplacingMatchesInString:str options:0 range:NSMakeRange(0, [str length]) withTemplate:@"[$4$5] \t$2"];
+ error:NULL];
+ message = [regex stringByReplacingMatchesInString:message
+ options:0
+ range:(NSRange){0, message.length}
+ withTemplate:@"[$4$5] \t$2"];
- modifiedString = [@"RCTJSLog> " stringByAppendingString:modifiedString];
-#if TARGET_IPHONE_SIMULATOR
- fprintf(stderr, "%s\n", [modifiedString UTF8String]); // don't print timestamps and other junk
-#else
- // Print normal errors with timestamps to files when not in simulator.
- RCTLogObjects(@[modifiedString], @"log");
-#endif
- JSStringRelease(string);
+ _RCTLogFormat(0, NULL, -1, @"%@", message);
}
return JSValueMakeUndefined(context);
diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m
index 11da8e7e7..bda7c357e 100644
--- a/React/Modules/RCTAlertManager.m
+++ b/React/Modules/RCTAlertManager.m
@@ -9,6 +9,7 @@
#import "RCTAlertManager.h"
+#import "RCTAssert.h"
#import "RCTLog.h"
@interface RCTAlertManager()
diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m
index 185982fdc..c21adc1d0 100644
--- a/React/Modules/RCTUIManager.m
+++ b/React/Modules/RCTUIManager.m
@@ -197,6 +197,11 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio
@synthesize bridge = _bridge;
+/**
+ * Declared in RCTBridge.
+ */
+extern NSString *RCTBridgeModuleNameForClass(Class cls);
+
/**
* This function derives the view name automatically
* from the module name.
@@ -334,7 +339,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName)
dispatch_async(_bridge.shadowQueue, ^{
RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag];
- RCTAssert(rootShadowView != nil, @"Could not locate root view with tag %@", reactTag);
+ RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag);
rootShadowView.frame = frame;
[rootShadowView updateLayout];
@@ -672,7 +677,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName)
}
}
-static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, RCTViewManager *manager)
+static BOOL RCTCallPropertySetter(NSString *key, SEL setter, id value, id view, id defaultView, RCTViewManager *manager)
{
// TODO: cache respondsToSelector tests
if ([manager respondsToSelector:setter]) {
@@ -681,7 +686,25 @@ static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView,
value = nil;
}
- ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView);
+ void (^block)() = ^{
+ ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView);
+ };
+
+#if DEBUG
+
+ NSString *viewName = RCTViewNameForModuleName(RCTBridgeModuleNameForClass([manager class]));
+ NSString *logPrefix = [NSString stringWithFormat:
+ @"Error setting property '%@' of %@ with tag #%@: ",
+ key, viewName, [view reactTag]];
+
+ RCTPerformBlockWithLogPrefix(block, logPrefix);
+
+#else
+
+ block();
+
+#endif
+
return YES;
}
return NO;
@@ -693,7 +716,7 @@ static void RCTSetViewProps(NSDictionary *props, UIView *view,
[props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forView:withDefaultView:", key]);
- RCTCallPropertySetter(setter, obj, view, defaultView, manager);
+ RCTCallPropertySetter(key, setter, obj, view, defaultView, manager);
}];
}
@@ -704,7 +727,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
[props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forShadowView:withDefaultView:", key]);
- RCTCallPropertySetter(setter, obj, shadowView, defaultView, manager);
+ RCTCallPropertySetter(key, setter, obj, shadowView, defaultView, manager);
}];
@@ -727,44 +750,47 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
// Register manager
_viewManagerRegistry[reactTag] = manager;
- // Generate default view, used for resetting default props
- if (!_defaultShadowViews[viewName]) {
- _defaultShadowViews[viewName] = [manager shadowView];
- }
-
RCTShadowView *shadowView = [manager shadowView];
- shadowView.viewName = viewName;
- shadowView.reactTag = reactTag;
- RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager);
- _shadowViewRegistry[shadowView.reactTag] = shadowView;
+ if (shadowView) {
+
+ // Generate default view, used for resetting default props
+ if (!_defaultShadowViews[viewName]) {
+ _defaultShadowViews[viewName] = [manager shadowView];
+ }
+
+ // Set properties
+ shadowView.viewName = viewName;
+ shadowView.reactTag = reactTag;
+ RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager);
+ }
+ _shadowViewRegistry[reactTag] = shadowView;
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
RCTCAssertMainThread();
- // Generate default view, used for resetting default props
- if (!uiManager->_defaultViews[viewName]) {
- // Note the default is setup after the props are read for the first time ever
- // for this className - this is ok because we only use the default for restoring
- // defaults, which never happens on first creation.
- uiManager->_defaultViews[viewName] = [manager view];
- }
-
UIView *view = [manager view];
if (view) {
- // Set required properties
- view.reactTag = reactTag;
- view.multipleTouchEnabled = YES;
- view.userInteractionEnabled = YES; // required for touch handling
- view.layer.allowsGroupOpacity = YES; // required for touch handling
+ // Generate default view, used for resetting default props
+ if (!uiManager->_defaultViews[viewName]) {
+ // Note the default is setup after the props are read for the first time ever
+ // for this className - this is ok because we only use the default for restoring
+ // defaults, which never happens on first creation.
+ uiManager->_defaultViews[viewName] = [manager view];
+ }
- // Set custom properties
+ // Set properties
+ view.reactTag = reactTag;
+ if ([view isKindOfClass:[UIView class]]) {
+ view.multipleTouchEnabled = YES;
+ view.userInteractionEnabled = YES; // required for touch handling
+ view.layer.allowsGroupOpacity = YES; // required for touch handling
+ }
RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], manager);
}
- viewRegistry[view.reactTag] = view;
+ viewRegistry[reactTag] = view;
}];
}
-
// TODO: remove viewName param as it isn't needed
- (void)updateView:(NSNumber *)reactTag viewName:(__unused NSString *)_ props:(NSDictionary *)props
{
@@ -875,7 +901,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view) {
- RCTLogError(@"measure cannot find view with tag %@", reactTag);
+ RCTLogError(@"measure cannot find view with tag #%@", reactTag);
return;
}
CGRect frame = view.frame;
@@ -1039,7 +1065,7 @@ static void RCTMeasureLayout(RCTShadowView *view,
uiManager.mainScrollView = (id)rkObject;
((id)rkObject).nativeMainScrollDelegate = uiManager.nativeMainScrollDelegate;
} else {
- RCTCAssert(NO, @"Tag %@ does not conform to RCTScrollableProtocol", reactTag);
+ RCTCAssert(NO, @"Tag #%@ does not conform to RCTScrollableProtocol", reactTag);
}
} else {
uiManager.mainScrollView = nil;
@@ -1056,7 +1082,7 @@ static void RCTMeasureLayout(RCTShadowView *view,
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue]) animated:YES];
} else {
- RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag);
+ RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag);
}
}];
}
@@ -1070,7 +1096,7 @@ static void RCTMeasureLayout(RCTShadowView *view,
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue]) animated:NO];
} else {
- RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag);
+ RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag);
}
}];
}
@@ -1084,7 +1110,7 @@ static void RCTMeasureLayout(RCTShadowView *view,
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id)view zoomToRect:[RCTConvert CGRect:rectDict] animated:YES];
} else {
- RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag);
+ RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag);
}
}];
}
diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m
index 9c0c56f04..066d28adc 100644
--- a/React/Views/RCTScrollViewManager.m
+++ b/React/Views/RCTScrollViewManager.m
@@ -72,7 +72,7 @@ RCT_DEPRECATED_VIEW_PROPERTY(throttleScrollCallbackMS, scrollEventThrottle)
UIView *view = viewRegistry[reactTag];
if (!view) {
- RCTLogError(@"Cannot find view with tag %@", reactTag);
+ RCTLogError(@"Cannot find view with tag #%@", reactTag);
return;
}
diff --git a/package.json b/package.json
index eef77648d..300537564 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,6 @@
"Examples/SampleApp",
"Libraries",
"packager",
- "local-cli",
"cli.js",
"init.sh",
"LICENSE",
@@ -42,25 +41,26 @@
"react-native-start": "packager/packager.sh"
},
"dependencies": {
+ "absolute-path": "0.0.0",
+ "bluebird": "^2.9.21",
+ "chalk": "^1.0.0",
"connect": "2.8.3",
+ "debug": "~2.1.0",
+ "joi": "~5.1.0",
"jstransform": "10.1.0",
+ "module-deps": "3.5.6",
+ "optimist": "0.6.1",
"react-timer-mixin": "^0.13.1",
"react-tools": "0.13.1",
"rebound": "^0.0.12",
+ "sane": "1.0.1",
"source-map": "0.1.31",
"stacktrace-parser": "0.1.1",
- "absolute-path": "0.0.0",
- "debug": "~2.1.0",
- "joi": "~5.1.0",
- "module-deps": "3.5.6",
- "optimist": "0.6.1",
- "sane": "1.0.1",
"uglify-js": "~2.4.16",
"underscore": "1.7.0",
"worker-farm": "1.1.0",
- "yargs": "1.3.2",
"ws": "0.4.31",
- "bluebird": "^2.9.21"
+ "yargs": "1.3.2"
},
"devDependencies": {
"jest-cli": "0.2.1",
diff --git a/packager/packager.js b/packager/packager.js
index e2a32f2ca..55004b7cf 100644
--- a/packager/packager.js
+++ b/packager/packager.js
@@ -23,6 +23,7 @@ if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) {
process.exit();
}
+var chalk = require('chalk');
var connect = require('connect');
var ReactPackager = require('./react-packager');
var blacklist = require('./blacklist.js');
@@ -88,13 +89,35 @@ console.log('\n' +
' ===============================================================\n'
);
-console.log('Looking for JS files in\n ', options.projectRoots.join('\n '));
+console.log(
+ 'Looking for JS files in\n ',
+ chalk.dim(options.projectRoots.join('\n ')),
+ '\n'
+);
process.on('uncaughtException', function(e) {
- console.error(e);
- console.error(e.stack);
- console.error('\n >>> ERROR: could not create packager - please shut down ' +
- 'any existing instances that are already running.\n\n');
+ if (e.code === 'EADDRINUSE') {
+ console.log(
+ chalk.bgRed.bold(' ERROR '),
+ chalk.red('Packager can\'t listen on port', chalk.bold(options.port))
+ );
+ console.log('Most likely another process is already using this port');
+ console.log('Run the following command to find out which process:');
+ console.log('\n ', chalk.bold('lsof -n -i4TCP:' + options.port), '\n');
+ console.log('You can either shut down the other process:');
+ console.log('\n ', chalk.bold('kill -9 '), '\n');
+ console.log('or run packager on different port.');
+ } else {
+ console.log(chalk.bgRed.bold(' ERROR '), chalk.red(e.message));
+ var errorAttributes = JSON.stringify(e);
+ if (errorAttributes !== '{}') {
+ console.error(chalk.red(errorAttributes));
+ }
+ console.error(chalk.red(e.stack));
+ }
+ console.log('\nSee', chalk.underline('http://facebook.github.io/react-native/docs/troubleshooting.html'));
+ console.log('for common problems and solutions.');
+ process.exit(1);
});
var server = runServer(options, function() {
@@ -151,13 +174,13 @@ function getDevToolsLauncher(options) {
}
// A status page so the React/project.pbxproj build script
-// can verify that packager is running on 8081 and not
+// can verify that packager is running on 8081 and not
// another program / service.
function statusPageMiddleware(req, res, next) {
if (req.url === '/status') {
res.end('packager-status:running');
} else {
- next();
+ next();
}
}