Skip to content

Commit

Permalink
Moving towards UIWindowScene support (facebook#28058)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#28058

I'm taking the first step towards supporting iOS 13 UIScene APIs and modernizing React Native not to assume an app only has a single window. See discussion here: facebook#25181 (comment)

The approach I'm taking is to take advantage of `RootTagContext` and passing it to NativeModules so that they can identify correctly which window they refer to. Here I'm just laying groundwork.

- [x] `Alert` and `ActionSheetIOS` take an optional `rootTag` argument that will cause them to appear on the correct window
- [x] `StatusBar` methods also have `rootTag` argument added, but it's not fully hooked up on the native side — this turns out to require some more work, see: facebook#25181 (comment)
- [x] `setNetworkActivityIndicatorVisible` is deprecated in iOS 13
- [x] `RCTPerfMonitor`, `RCTProfile` no longer assume `UIApplicationDelegate` has a `window` property (no longer the best practice) — they now just render on the key window

Next steps: Add VC-based status bar management (if I get the OK on facebook#25181 (comment) ), add multiple window demo to RNTester, deprecate Dimensions in favor of a layout context, consider adding hook-based APIs for native modules such as Alert that automatically know which rootTag to pass

## Changelog

[Internal] [Changed] - Modernize Modal to use RootTagContext
[iOS] [Changed] - `Alert`, `ActionSheetIOS`, `StatusBar` methods now take an optional `surface` argument (for future iPadOS 13 support)
[Internal] [Changed] - Do not assume `UIApplicationDelegate` has a `window` property
Pull Request resolved: facebook#25425

Test Plan:
- Open RNTester and:
- go to Modal and check if it still works
- Alert → see if works
- ACtionSheetIOS → see if it works
- StatusBar → see if it works
- Share → see if it works

Reviewed By: PeteTheHeat

Differential Revision: D16957751

Pulled By: hramos

fbshipit-source-id: 4dae1a8126822038891e3bc3e0aa9640b86dfe66
  • Loading branch information
radex authored and facebook-github-bot committed Feb 14, 2020
1 parent 5703abd commit 8ed4e4f
Show file tree
Hide file tree
Showing 21 changed files with 242 additions and 85 deletions.
35 changes: 30 additions & 5 deletions Libraries/ActionSheetIOS/ActionSheetIOS.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'use strict';

import RCTActionSheetManager from './NativeActionSheetManager';
import ReactNative from '../Renderer/shims/ReactNative';

const invariant = require('invariant');
const processColor = require('../StyleSheet/processColor');
Expand Down Expand Up @@ -41,11 +42,12 @@ const ActionSheetIOS = {
options: {|
+title?: ?string,
+message?: ?string,
+options: Array<string>,
+options: ?Array<string>,
+destructiveButtonIndex?: ?number | ?Array<number>,
+cancelButtonIndex?: ?number,
+anchor?: ?number,
+tintColor?: number | string,
+surface?: mixed,
|},
callback: (buttonIndex: number) => void,
) {
Expand All @@ -56,7 +58,13 @@ const ActionSheetIOS = {
invariant(typeof callback === 'function', 'Must provide a valid callback');
invariant(RCTActionSheetManager, "ActionSheetManager does't exist");

const {tintColor, destructiveButtonIndex, ...remainingOptions} = options;
const {
tintColor,
destructiveButtonIndex,
surface,
...remainingOptions
} = options;
const reactTag = ReactNative.findNodeHandle(surface) ?? -1;
let destructiveButtonIndices = null;

if (Array.isArray(destructiveButtonIndex)) {
Expand All @@ -68,6 +76,7 @@ const ActionSheetIOS = {
RCTActionSheetManager.showActionSheetWithOptions(
{
...remainingOptions,
reactTag,
tintColor: processColor(tintColor),
destructiveButtonIndices,
},
Expand Down Expand Up @@ -99,9 +108,17 @@ const ActionSheetIOS = {
* See http://facebook.github.io/react-native/docs/actionsheetios.html#showshareactionsheetwithoptions
*/
showShareActionSheetWithOptions(
options: Object,
options: {|
+message?: ?string,
+url?: ?string,
+subject?: ?string,
+anchor?: ?number,
+tintColor?: ?number | string,
+excludedActivityTypes?: ?Array<string>,
+surface?: mixed,
|},
failureCallback: Function,
successCallback: Function,
successCallback: (completed: boolean, activityType: ?string) => void,
) {
invariant(
typeof options === 'object' && options !== null,
Expand All @@ -116,8 +133,16 @@ const ActionSheetIOS = {
'Must provide a valid successCallback',
);
invariant(RCTActionSheetManager, "ActionSheetManager does't exist");

const {tintColor, surface, ...remainingOptions} = options;
const reactTag = ReactNative.findNodeHandle(surface) ?? -1;

RCTActionSheetManager.showShareActionSheetWithOptions(
{...options, tintColor: processColor(options.tintColor)},
{
...remainingOptions,
reactTag,
tintColor: processColor(options.tintColor),
},
failureCallback,
successCallback,
);
Expand Down
2 changes: 2 additions & 0 deletions Libraries/ActionSheetIOS/NativeActionSheetManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface Spec extends TurboModule {
+cancelButtonIndex?: ?number,
+anchor?: ?number,
+tintColor?: ?number,
+reactTag?: number,
|},
callback: (buttonIndex: number) => void,
) => void;
Expand All @@ -35,6 +36,7 @@ export interface Spec extends TurboModule {
+anchor?: ?number,
+tintColor?: ?number,
+excludedActivityTypes?: ?Array<string>,
+reactTag?: number,
|},
failureCallback: (error: {|
+domain: string,
Expand Down
22 changes: 16 additions & 6 deletions Libraries/Alert/Alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import NativeDialogManagerAndroid, {
type DialogOptions,
} from '../NativeModules/specs/NativeDialogManagerAndroid';
import RCTAlertManager from './RCTAlertManager';
import ReactNative from '../Renderer/shims/ReactNative';

export type AlertType =
| 'default'
Expand All @@ -30,11 +31,16 @@ export type Buttons = Array<{
}>;

type Options = {
cancelable?: ?boolean,
onDismiss?: ?() => void,
+cancelable?: ?boolean,
+onDismiss?: ?() => void,
+surface?: mixed,
...
};

type PromptOptions = {
surface?: mixed,
};

/**
* Launches an alert dialog with the specified title and message.
*
Expand All @@ -45,10 +51,12 @@ class Alert {
title: ?string,
message?: ?string,
buttons?: Buttons,
options?: Options,
options?: Options = {},
): void {
if (Platform.OS === 'ios') {
Alert.prompt(title, message, buttons, 'default');
Alert.prompt(title, message, buttons, 'default', undefined, undefined, {
surface: options.surface,
});
} else if (Platform.OS === 'android') {
if (!NativeDialogManagerAndroid) {
return;
Expand All @@ -61,7 +69,7 @@ class Alert {
cancelable: false,
};

if (options && options.cancelable) {
if (options.cancelable) {
config.cancelable = options.cancelable;
}
// At most three buttons (neutral, negative, positive). Ignore rest.
Expand Down Expand Up @@ -94,7 +102,7 @@ class Alert {
buttonPositive.onPress && buttonPositive.onPress();
}
} else if (action === constants.dismissed) {
options && options.onDismiss && options.onDismiss();
options.onDismiss && options.onDismiss();
}
};
const onError = errorMessage => console.warn(errorMessage);
Expand All @@ -109,6 +117,7 @@ class Alert {
type?: ?AlertType = 'plain-text',
defaultValue?: string,
keyboardType?: string,
options?: PromptOptions = {surface: undefined},
): void {
if (Platform.OS === 'ios') {
let callbacks = [];
Expand Down Expand Up @@ -143,6 +152,7 @@ class Alert {
cancelButtonKey,
destructiveButtonKey,
keyboardType,
reactTag: ReactNative.findNodeHandle(options.surface) ?? -1,
},
(id, value) => {
const cb = callbacks[id];
Expand Down
1 change: 1 addition & 0 deletions Libraries/Alert/NativeAlertManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type Args = {|
cancelButtonKey?: string,
destructiveButtonKey?: string,
keyboardType?: string,
reactTag?: number,
|};

export interface Spec extends TurboModule {
Expand Down
12 changes: 10 additions & 2 deletions Libraries/Components/StatusBar/NativeStatusBarManagerIOS.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,19 @@ export interface Spec extends TurboModule {
* - 'dark-content'
* - 'light-content'
*/
+setStyle: (statusBarStyle?: ?string, animated: boolean) => void;
+setStyle: (
statusBarStyle?: ?string,
animated: boolean,
reactTag?: number,
) => void;
/**
* - withAnimation can be: 'none' | 'fade' | 'slide'
*/
+setHidden: (hidden: boolean, withAnimation: string) => void;
+setHidden: (
hidden: boolean,
withAnimation: string,
reactTag?: number,
) => void;
}

export default (TurboModuleRegistry.getEnforcing<Spec>(
Expand Down
53 changes: 40 additions & 13 deletions Libraries/Components/StatusBar/StatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
'use strict';

const Platform = require('../../Utilities/Platform');
const RootTagContext = require('../../ReactNative/RootTagContext');
import ReactNative from '../../Renderer/shims/ReactNative';
const React = require('react');

const processColor = require('../../StyleSheet/processColor');
Expand Down Expand Up @@ -262,11 +264,19 @@ class StatusBar extends React.Component<Props> {
* @param animation Optional animation when
* changing the status bar hidden property.
*/
static setHidden(hidden: boolean, animation?: StatusBarAnimation) {
static setHidden(
hidden: boolean,
animation?: StatusBarAnimation,
surface?: mixed,
) {
animation = animation || 'none';
StatusBar._defaultProps.hidden.value = hidden;
if (Platform.OS === 'ios') {
NativeStatusBarManagerIOS.setHidden(hidden, animation);
NativeStatusBarManagerIOS.setHidden(
hidden,
animation,
ReactNative.findNodeHandle(surface) ?? -1,
);
} else if (Platform.OS === 'android') {
NativeStatusBarManagerAndroid.setHidden(hidden);
}
Expand All @@ -277,11 +287,19 @@ class StatusBar extends React.Component<Props> {
* @param style Status bar style to set
* @param animated Animate the style change.
*/
static setBarStyle(style: StatusBarStyle, animated?: boolean) {
static setBarStyle(
style: StatusBarStyle,
animated?: boolean,
surface?: mixed,
) {
animated = animated || false;
StatusBar._defaultProps.barStyle.value = style;
if (Platform.OS === 'ios') {
NativeStatusBarManagerIOS.setStyle(style, animated);
NativeStatusBarManagerIOS.setStyle(
style,
animated,
ReactNative.findNodeHandle(surface) ?? -1,
);
} else if (Platform.OS === 'android') {
NativeStatusBarManagerAndroid.setStyle(style);
}
Expand All @@ -290,6 +308,7 @@ class StatusBar extends React.Component<Props> {
/**
* Control the visibility of the network activity indicator
* @param visible Show the indicator.
* @platform ios
*/
static setNetworkActivityIndicatorVisible(visible: boolean) {
if (Platform.OS !== 'ios') {
Expand All @@ -306,6 +325,7 @@ class StatusBar extends React.Component<Props> {
* Set the background color for the status bar
* @param color Background color.
* @param animated Animate the style change.
* @platform android
*/
static setBackgroundColor(color: string, animated?: boolean) {
if (Platform.OS !== 'android') {
Expand All @@ -329,6 +349,7 @@ class StatusBar extends React.Component<Props> {
/**
* Control the translucency of the status bar
* @param translucent Set as translucent.
* @platform android
*/
static setTranslucent(translucent: boolean) {
if (Platform.OS !== 'android') {
Expand All @@ -345,10 +366,10 @@ class StatusBar extends React.Component<Props> {
*
* @param props Object containing the StatusBar props to use in the stack entry.
*/
static pushStackEntry(props: any): any {
static pushStackEntry(props: any, surface?: mixed): any {
const entry = createStackEntry(props);
StatusBar._propsStack.push(entry);
StatusBar._updatePropsStack();
StatusBar._updatePropsStack(surface);
return entry;
}

Expand All @@ -357,12 +378,12 @@ class StatusBar extends React.Component<Props> {
*
* @param entry Entry returned from `pushStackEntry`.
*/
static popStackEntry(entry: any) {
static popStackEntry(entry: any, surface?: mixed) {
const index = StatusBar._propsStack.indexOf(entry);
if (index !== -1) {
StatusBar._propsStack.splice(index, 1);
}
StatusBar._updatePropsStack();
StatusBar._updatePropsStack(surface);
}

/**
Expand All @@ -371,13 +392,13 @@ class StatusBar extends React.Component<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): any {
static replaceStackEntry(entry: any, props: any, surface?: mixed): any {
const newEntry = createStackEntry(props);
const index = StatusBar._propsStack.indexOf(entry);
if (index !== -1) {
StatusBar._propsStack[index] = newEntry;
}
StatusBar._updatePropsStack();
StatusBar._updatePropsStack(surface);
return newEntry;
}

Expand All @@ -389,33 +410,37 @@ class StatusBar extends React.Component<Props> {
showHideTransition: 'fade',
};

// $FlowFixMe (signature-verification-failure)
static contextType = RootTagContext;

_stackEntry = null;

componentDidMount() {
// 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 = StatusBar.pushStackEntry(this.props);
this._stackEntry = StatusBar.pushStackEntry(this.props, this.context);
}

componentWillUnmount() {
// When a StatusBar is unmounted, remove itself from the stack and update
// the native bar with the next props.
StatusBar.popStackEntry(this._stackEntry);
StatusBar.popStackEntry(this._stackEntry, this.context);
}

componentDidUpdate() {
this._stackEntry = StatusBar.replaceStackEntry(
this._stackEntry,
this.props,
this.context,
);
}

/**
* Updates the native status bar with the props from the stack.
*/
static _updatePropsStack = () => {
static _updatePropsStack = (surface?: mixed) => {
// Send the update to the native module only once at the end of the frame.
clearImmediate(StatusBar._updateImmediate);
StatusBar._updateImmediate = setImmediate(() => {
Expand All @@ -434,6 +459,7 @@ class StatusBar extends React.Component<Props> {
NativeStatusBarManagerIOS.setStyle(
mergedProps.barStyle.value,
mergedProps.barStyle.animated || false,
ReactNative.findNodeHandle(surface) ?? -1,
);
}
if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) {
Expand All @@ -442,6 +468,7 @@ class StatusBar extends React.Component<Props> {
mergedProps.hidden.animated
? mergedProps.hidden.transition
: 'none',
ReactNative.findNodeHandle(surface) ?? -1,
);
}

Expand Down
Loading

0 comments on commit 8ed4e4f

Please sign in to comment.