Skip to content

Commit

Permalink
Expose static methods to manipulate the StatusBar stack imperatively (f…
Browse files Browse the repository at this point in the history
…acebook#21206)

Summary:
This PR exposes three static methods (`pushStackEntry`, `popStackEntry`, and `replaceStackEntry`) on StatusBar that enable imperative manipulation of the StatusBar style within the stack established by mounted StatusBar components.

Motivation:
----------

The StatusBar **component** provides a sensible API for manipulating that StatusBar style: every time a StatusBar component is mounted, its props are pushed onto a stack, and the props from the most recently mounted component are applied.

However, there are some scenarios where you may need to manipulate the StatusBar style from imperative code — particularly when invoking imperative third-party APIs that cause UI to appear. (For example, a user feedback utility or bug reporter that launches a full-screen modal.)

In modern iOS development, `UIViewControllerBasedStatusBarAppearance` is typically set to `YES`, which allows the third-party UIViewController to specify its preferred status bar style. However, as has been discussed at length in facebook#11710, React Native has disabled this setting, which means that either the app's code or the third-party's React Native wrapper needs to manually manipulate React Native's StatusBar API to achieve the desired outcome.

The existing imperative StatusBar APIs are not a good fit for these needs because they simply overwrite the existing StatusBar styles, and provide no means of reverting StatusBar style changes when the third-party UI is dismissed.

To improve upon this situation, this PR makes it possible to call `StatusBar.pushStackEntry` before launching the third-party UI, wait for the UI to dismiss, and then call `StatusBar.popStackEntry` (supplying the token returned from the push call).

I've featured the new stack-based imperative methods in the documentation, but stopped short of explicitly deprecating the older imperative methods — though I can think of no reason not to deprecate them. Feedback is welcome on this point.

Release Notes:
--------------

[GENERAL] [ENHANCEMENT] [StatusBar] - Add static methods to manipulate StatusBar stack imperatively

Pull Request resolved: facebook#21206

Differential Revision: D9945247

Pulled By: cpojer

fbshipit-source-id: ec118268cff5b47e87be81d0b9e1728ecc3a9b02
  • Loading branch information
jamesreggio authored and facebook-github-bot committed Jan 22, 2019
1 parent a63a0c4 commit cf886cb
Showing 1 changed file with 79 additions and 20 deletions.
99 changes: 79 additions & 20 deletions Libraries/Components/StatusBar/StatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,34 @@ function createStackEntry(props: any): any {
*
* ### Imperative API
*
* For cases where using a component is not ideal, there is also an imperative
* API exposed as static functions on the component. It is however not recommended
* to use the static API and the component for the same prop because any value
* set by the static API will get overriden by the one set by the component in
* the next render.
* For cases where using a component is not ideal, there are static methods
* to manipulate the `StatusBar` display stack. These methods have the same
* behavior as mounting and unmounting a `StatusBar` component.
*
* For example, you can call `StatusBar.pushStackEntry` to update the status bar
* before launching a third-party native UI component, and then call
* `StatusBar.popStackEntry` when completed.
*
* ```
* const openThirdPartyBugReporter = async () => {
* // The bug reporter has a dark background, so we push a new status bar style.
* const stackEntry = StatusBar.pushStackEntry({barStyle: 'light-content'});
*
* // `open` returns a promise that resolves when the UI is dismissed.
* await BugReporter.open();
*
* // Don't forget to call `popStackEntry` when you're done.
* StatusBar.popStackEntry(stackEntry);
* };
* ```
*
* There is a legacy imperative API that enables you to manually update the
* status bar styles. However, the legacy API does not update the internal
* `StatusBar` display stack, which means that any changes will be overridden
* whenever a `StatusBar` component is mounted or unmounted.
*
* It is strongly advised that you use `pushStackEntry`, `popStackEntry`, or
* `replaceStackEntry` instead of the static methods beginning with `set`.
*
* ### Constants
*
Expand Down Expand Up @@ -300,6 +323,48 @@ class StatusBar extends React.Component<Props> {
StatusBarManager.setTranslucent(translucent);
}

/**
* Push a StatusBar entry onto the stack.
* The return value should be passed to `popStackEntry` when complete.
*
* @param props Object containing the StatusBar props to use in the stack entry.
*/
static pushStackEntry(props: any) {
const entry = createStackEntry(props);
StatusBar._propsStack.push(entry);
StatusBar._updatePropsStack();
return entry;
}

/**
* Pop a StatusBar entry from the stack.
*
* @param entry Entry returned from `pushStackEntry`.
*/
static popStackEntry(entry: any) {
const index = StatusBar._propsStack.indexOf(entry);
if (index !== -1) {
StatusBar._propsStack.splice(index, 1);
}
StatusBar._updatePropsStack();
}

/**
* Replace an existing StatusBar stack entry with new props.
*
* @param entry Entry returned from `pushStackEntry` to replace.
* @param props Object containing the StatusBar props to use in the replacement stack entry.
*/
static replaceStackEntry(entry: any, props: any) {
const newEntry = createStackEntry(props);
const index = StatusBar._propsStack.indexOf(entry);
if (index !== -1) {
StatusBar._propsStack[index] = newEntry;
}
StatusBar._updatePropsStack();
return newEntry;
}

static defaultProps = {
animated: false,
showHideTransition: 'fade',
Expand All @@ -311,33 +376,27 @@ class StatusBar extends React.Component<Props> {
// Every time a StatusBar component is mounted, we push it's prop to a stack
// and always update the native status bar with the props from the top of then
// stack. This allows having multiple StatusBar components and the one that is
// added last or is deeper in the view hierarchy will have priority.
this._stackEntry = createStackEntry(this.props);
StatusBar._propsStack.push(this._stackEntry);
this._updatePropsStack();
// added last or is deeper in the view hierachy will have priority.
this._stackEntry = StatusBar.pushStackEntry(this.props);
}

componentWillUnmount() {
// When a StatusBar is unmounted, remove itself from the stack and update
// the native bar with the next props.
const index = StatusBar._propsStack.indexOf(this._stackEntry);
StatusBar._propsStack.splice(index, 1);

this._updatePropsStack();
StatusBar.popStackEntry(this._stackEntry);
}

componentDidUpdate() {
const index = StatusBar._propsStack.indexOf(this._stackEntry);
this._stackEntry = createStackEntry(this.props);
StatusBar._propsStack[index] = this._stackEntry;

this._updatePropsStack();
this._stackEntry = StatusBar.replaceStackEntry(
this._stackEntry,
this.props,
);
}

/**
* Updates the native status bar with the props from the stack.
*/
_updatePropsStack = () => {
static _updatePropsStack = () => {
// Send the update to the native module only once at the end of the frame.
clearImmediate(StatusBar._updateImmediate);
StatusBar._updateImmediate = setImmediate(() => {
Expand All @@ -355,7 +414,7 @@ class StatusBar extends React.Component<Props> {
) {
StatusBarManager.setStyle(
mergedProps.barStyle.value,
mergedProps.barStyle.animated,
mergedProps.barStyle.animated || false,
);
}
if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) {
Expand Down

0 comments on commit cf886cb

Please sign in to comment.