Skip to content

Commit b58e176

Browse files
radexfacebook-github-bot
authored andcommitted
Moving towards UIWindowScene support (#28058)
Summary: Pull Request resolved: #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: #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: #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 #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) [iOS] [Changed] - RCTPresentedViewController now takes a nullable `window` arg [Internal] [Changed] - Do not assume `UIApplicationDelegate` has a `window` property Pull Request resolved: #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: ae2a4478e2e7f8d2be3022c9c4861561ec244a26
1 parent fcec815 commit b58e176

23 files changed

+236
-89
lines changed

Libraries/ActionSheetIOS/ActionSheetIOS.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'use strict';
1212

1313
import RCTActionSheetManager from './NativeActionSheetManager';
14+
import ReactNative from '../Renderer/shims/ReactNative';
1415

1516
const invariant = require('invariant');
1617
const processColor = require('../StyleSheet/processColor');
@@ -43,12 +44,13 @@ const ActionSheetIOS = {
4344
options: {|
4445
+title?: ?string,
4546
+message?: ?string,
46-
+options: Array<string>,
47+
+options: ?Array<string>,
4748
+destructiveButtonIndex?: ?number | ?Array<number>,
4849
+cancelButtonIndex?: ?number,
4950
+anchor?: ?number,
5051
+tintColor?: ColorValue | ProcessedColorValue,
5152
+userInterfaceStyle?: string,
53+
+surface?: mixed,
5254
|},
5355
callback: (buttonIndex: number) => void,
5456
) {
@@ -59,7 +61,13 @@ const ActionSheetIOS = {
5961
invariant(typeof callback === 'function', 'Must provide a valid callback');
6062
invariant(RCTActionSheetManager, "ActionSheetManager does't exist");
6163

62-
const {tintColor, destructiveButtonIndex, ...remainingOptions} = options;
64+
const {
65+
tintColor,
66+
destructiveButtonIndex,
67+
surface,
68+
...remainingOptions
69+
} = options;
70+
const reactTag = ReactNative.findNodeHandle(surface) ?? -1;
6371
let destructiveButtonIndices = null;
6472

6573
if (Array.isArray(destructiveButtonIndex)) {
@@ -76,6 +84,7 @@ const ActionSheetIOS = {
7684
RCTActionSheetManager.showActionSheetWithOptions(
7785
{
7886
...remainingOptions,
87+
reactTag,
7988
tintColor: processedTintColor,
8089
destructiveButtonIndices,
8190
},
@@ -124,8 +133,16 @@ const ActionSheetIOS = {
124133
'Must provide a valid successCallback',
125134
);
126135
invariant(RCTActionSheetManager, "ActionSheetManager does't exist");
136+
137+
const {tintColor, surface, ...remainingOptions} = options;
138+
const reactTag = ReactNative.findNodeHandle(surface) ?? -1;
139+
127140
RCTActionSheetManager.showShareActionSheetWithOptions(
128-
{...options, tintColor: processColor(options.tintColor)},
141+
{
142+
...remainingOptions,
143+
reactTag,
144+
tintColor: processColor(options.tintColor),
145+
},
129146
failureCallback,
130147
successCallback,
131148
);

Libraries/ActionSheetIOS/NativeActionSheetManager.js

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface Spec extends TurboModule {
2525
+anchor?: ?number,
2626
+tintColor?: ?number,
2727
+userInterfaceStyle?: ?string,
28+
+reactTag?: number,
2829
|},
2930
callback: (buttonIndex: number) => void,
3031
) => void;
@@ -37,6 +38,7 @@ export interface Spec extends TurboModule {
3738
+tintColor?: ?number,
3839
+excludedActivityTypes?: ?Array<string>,
3940
+userInterfaceStyle?: ?string,
41+
+reactTag?: number,
4042
|},
4143
failureCallback: (error: {|
4244
+domain: string,

Libraries/Alert/Alert.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import NativeDialogManagerAndroid, {
1515
type DialogOptions,
1616
} from '../NativeModules/specs/NativeDialogManagerAndroid';
1717
import RCTAlertManager from './RCTAlertManager';
18+
import ReactNative from '../Renderer/shims/ReactNative';
1819

1920
export type AlertType =
2021
| 'default'
@@ -32,9 +33,14 @@ export type Buttons = Array<{
3233
type Options = {
3334
cancelable?: ?boolean,
3435
onDismiss?: ?() => void,
36+
surface?: mixed,
3537
...
3638
};
3739

40+
type PromptOptions = {
41+
surface?: mixed,
42+
};
43+
3844
/**
3945
* Launches an alert dialog with the specified title and message.
4046
*
@@ -45,10 +51,12 @@ class Alert {
4551
title: ?string,
4652
message?: ?string,
4753
buttons?: Buttons,
48-
options?: Options,
54+
options?: Options = {},
4955
): void {
5056
if (Platform.OS === 'ios') {
51-
Alert.prompt(title, message, buttons, 'default');
57+
Alert.prompt(title, message, buttons, 'default', undefined, undefined, {
58+
surface: options.surface,
59+
});
5260
} else if (Platform.OS === 'android') {
5361
if (!NativeDialogManagerAndroid) {
5462
return;
@@ -61,7 +69,7 @@ class Alert {
6169
cancelable: false,
6270
};
6371

64-
if (options && options.cancelable) {
72+
if (options.cancelable) {
6573
config.cancelable = options.cancelable;
6674
}
6775
// At most three buttons (neutral, negative, positive). Ignore rest.
@@ -94,7 +102,7 @@ class Alert {
94102
buttonPositive.onPress && buttonPositive.onPress();
95103
}
96104
} else if (action === constants.dismissed) {
97-
options && options.onDismiss && options.onDismiss();
105+
options.onDismiss && options.onDismiss();
98106
}
99107
};
100108
const onError = errorMessage => console.warn(errorMessage);
@@ -109,6 +117,7 @@ class Alert {
109117
type?: ?AlertType = 'plain-text',
110118
defaultValue?: string,
111119
keyboardType?: string,
120+
options?: PromptOptions = {surface: undefined},
112121
): void {
113122
if (Platform.OS === 'ios') {
114123
let callbacks = [];
@@ -143,6 +152,7 @@ class Alert {
143152
cancelButtonKey,
144153
destructiveButtonKey,
145154
keyboardType,
155+
reactTag: ReactNative.findNodeHandle(options.surface) ?? -1,
146156
},
147157
(id, value) => {
148158
const cb = callbacks[id];

Libraries/Alert/NativeAlertManager.js

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type Args = {|
2222
cancelButtonKey?: string,
2323
destructiveButtonKey?: string,
2424
keyboardType?: string,
25+
reactTag?: number,
2526
|};
2627

2728
export interface Spec extends TurboModule {

Libraries/CameraRoll/RCTImagePickerManager.mm

+2-2
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ - (void)_presentPicker:(UIImagePickerController *)imagePicker
204204
[_pickerCallbacks addObject:callback];
205205
[_pickerCancelCallbacks addObject:cancelCallback];
206206

207-
UIViewController *rootViewController = RCTPresentedViewController();
207+
UIViewController *rootViewController = RCTPresentedViewController(nil);
208208
[rootViewController presentViewController:imagePicker animated:YES completion:nil];
209209
}
210210

@@ -223,7 +223,7 @@ - (void)_dismissPicker:(UIImagePickerController *)picker args:(NSArray *)args
223223
[_pickerCallbacks removeObjectAtIndex:index];
224224
[_pickerCancelCallbacks removeObjectAtIndex:index];
225225

226-
UIViewController *rootViewController = RCTPresentedViewController();
226+
UIViewController *rootViewController = RCTPresentedViewController(nil);
227227
[rootViewController dismissViewControllerAnimated:YES completion:nil];
228228

229229
if (args) {

Libraries/Components/StatusBar/NativeStatusBarManagerIOS.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,19 @@ export interface Spec extends TurboModule {
3131
* - 'dark-content'
3232
* - 'light-content'
3333
*/
34-
+setStyle: (statusBarStyle?: ?string, animated: boolean) => void;
34+
+setStyle: (
35+
statusBarStyle?: ?string,
36+
animated: boolean,
37+
reactTag?: number,
38+
) => void;
3539
/**
3640
* - withAnimation can be: 'none' | 'fade' | 'slide'
3741
*/
38-
+setHidden: (hidden: boolean, withAnimation: string) => void;
42+
+setHidden: (
43+
hidden: boolean,
44+
withAnimation: string,
45+
reactTag?: number,
46+
) => void;
3947
}
4048

4149
export default (TurboModuleRegistry.getEnforcing<Spec>(

Libraries/Components/StatusBar/StatusBar.js

+40-13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
'use strict';
1212

1313
const Platform = require('../../Utilities/Platform');
14+
const RootTagContext = require('../../ReactNative/RootTagContext');
15+
import ReactNative from '../../Renderer/shims/ReactNative';
1416
const React = require('react');
1517

1618
const invariant = require('invariant');
@@ -264,11 +266,19 @@ class StatusBar extends React.Component<Props> {
264266
* @param animation Optional animation when
265267
* changing the status bar hidden property.
266268
*/
267-
static setHidden(hidden: boolean, animation?: StatusBarAnimation) {
269+
static setHidden(
270+
hidden: boolean,
271+
animation?: StatusBarAnimation,
272+
surface?: mixed,
273+
) {
268274
animation = animation || 'none';
269275
StatusBar._defaultProps.hidden.value = hidden;
270276
if (Platform.OS === 'ios') {
271-
NativeStatusBarManagerIOS.setHidden(hidden, animation);
277+
NativeStatusBarManagerIOS.setHidden(
278+
hidden,
279+
animation,
280+
ReactNative.findNodeHandle(surface) ?? -1,
281+
);
272282
} else if (Platform.OS === 'android') {
273283
NativeStatusBarManagerAndroid.setHidden(hidden);
274284
}
@@ -279,11 +289,19 @@ class StatusBar extends React.Component<Props> {
279289
* @param style Status bar style to set
280290
* @param animated Animate the style change.
281291
*/
282-
static setBarStyle(style: StatusBarStyle, animated?: boolean) {
292+
static setBarStyle(
293+
style: StatusBarStyle,
294+
animated?: boolean,
295+
surface?: mixed,
296+
) {
283297
animated = animated || false;
284298
StatusBar._defaultProps.barStyle.value = style;
285299
if (Platform.OS === 'ios') {
286-
NativeStatusBarManagerIOS.setStyle(style, animated);
300+
NativeStatusBarManagerIOS.setStyle(
301+
style,
302+
animated,
303+
ReactNative.findNodeHandle(surface) ?? -1,
304+
);
287305
} else if (Platform.OS === 'android') {
288306
NativeStatusBarManagerAndroid.setStyle(style);
289307
}
@@ -292,6 +310,7 @@ class StatusBar extends React.Component<Props> {
292310
/**
293311
* Control the visibility of the network activity indicator
294312
* @param visible Show the indicator.
313+
* @platform ios
295314
*/
296315
static setNetworkActivityIndicatorVisible(visible: boolean) {
297316
if (Platform.OS !== 'ios') {
@@ -308,6 +327,7 @@ class StatusBar extends React.Component<Props> {
308327
* Set the background color for the status bar
309328
* @param color Background color.
310329
* @param animated Animate the style change.
330+
* @platform android
311331
*/
312332
static setBackgroundColor(color: string, animated?: boolean) {
313333
if (Platform.OS !== 'android') {
@@ -335,6 +355,7 @@ class StatusBar extends React.Component<Props> {
335355
/**
336356
* Control the translucency of the status bar
337357
* @param translucent Set as translucent.
358+
* @platform android
338359
*/
339360
static setTranslucent(translucent: boolean) {
340361
if (Platform.OS !== 'android') {
@@ -351,10 +372,10 @@ class StatusBar extends React.Component<Props> {
351372
*
352373
* @param props Object containing the StatusBar props to use in the stack entry.
353374
*/
354-
static pushStackEntry(props: any): any {
375+
static pushStackEntry(props: any, surface?: mixed): any {
355376
const entry = createStackEntry(props);
356377
StatusBar._propsStack.push(entry);
357-
StatusBar._updatePropsStack();
378+
StatusBar._updatePropsStack(surface);
358379
return entry;
359380
}
360381

@@ -363,12 +384,12 @@ class StatusBar extends React.Component<Props> {
363384
*
364385
* @param entry Entry returned from `pushStackEntry`.
365386
*/
366-
static popStackEntry(entry: any) {
387+
static popStackEntry(entry: any, surface?: mixed) {
367388
const index = StatusBar._propsStack.indexOf(entry);
368389
if (index !== -1) {
369390
StatusBar._propsStack.splice(index, 1);
370391
}
371-
StatusBar._updatePropsStack();
392+
StatusBar._updatePropsStack(surface);
372393
}
373394

374395
/**
@@ -377,13 +398,13 @@ class StatusBar extends React.Component<Props> {
377398
* @param entry Entry returned from `pushStackEntry` to replace.
378399
* @param props Object containing the StatusBar props to use in the replacement stack entry.
379400
*/
380-
static replaceStackEntry(entry: any, props: any): any {
401+
static replaceStackEntry(entry: any, props: any, surface?: mixed): any {
381402
const newEntry = createStackEntry(props);
382403
const index = StatusBar._propsStack.indexOf(entry);
383404
if (index !== -1) {
384405
StatusBar._propsStack[index] = newEntry;
385406
}
386-
StatusBar._updatePropsStack();
407+
StatusBar._updatePropsStack(surface);
387408
return newEntry;
388409
}
389410

@@ -395,33 +416,37 @@ class StatusBar extends React.Component<Props> {
395416
showHideTransition: 'fade',
396417
};
397418

419+
// $FlowFixMe (signature-verification-failure)
420+
static contextType = RootTagContext;
421+
398422
_stackEntry = null;
399423

400424
componentDidMount() {
401425
// Every time a StatusBar component is mounted, we push it's prop to a stack
402426
// and always update the native status bar with the props from the top of then
403427
// stack. This allows having multiple StatusBar components and the one that is
404428
// added last or is deeper in the view hierarchy will have priority.
405-
this._stackEntry = StatusBar.pushStackEntry(this.props);
429+
this._stackEntry = StatusBar.pushStackEntry(this.props, this.context);
406430
}
407431

408432
componentWillUnmount() {
409433
// When a StatusBar is unmounted, remove itself from the stack and update
410434
// the native bar with the next props.
411-
StatusBar.popStackEntry(this._stackEntry);
435+
StatusBar.popStackEntry(this._stackEntry, this.context);
412436
}
413437

414438
componentDidUpdate() {
415439
this._stackEntry = StatusBar.replaceStackEntry(
416440
this._stackEntry,
417441
this.props,
442+
this.context,
418443
);
419444
}
420445

421446
/**
422447
* Updates the native status bar with the props from the stack.
423448
*/
424-
static _updatePropsStack = () => {
449+
static _updatePropsStack = (surface?: mixed) => {
425450
// Send the update to the native module only once at the end of the frame.
426451
clearImmediate(StatusBar._updateImmediate);
427452
StatusBar._updateImmediate = setImmediate(() => {
@@ -440,6 +465,7 @@ class StatusBar extends React.Component<Props> {
440465
NativeStatusBarManagerIOS.setStyle(
441466
mergedProps.barStyle.value,
442467
mergedProps.barStyle.animated || false,
468+
ReactNative.findNodeHandle(surface) ?? -1,
443469
);
444470
}
445471
if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) {
@@ -448,6 +474,7 @@ class StatusBar extends React.Component<Props> {
448474
mergedProps.hidden.animated
449475
? mergedProps.hidden.transition
450476
: 'none',
477+
ReactNative.findNodeHandle(surface) ?? -1,
451478
);
452479
}
453480

0 commit comments

Comments
 (0)