Skip to content

Commit 5eb60f4

Browse files
committed
feat: implement window manager events
1 parent d82a0e2 commit 5eb60f4

File tree

8 files changed

+125
-9
lines changed

8 files changed

+125
-9
lines changed

packages/react-native/Libraries/SwiftExtensions/RCTMainWindow.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public struct RCTMainWindow: Scene {
2121
var moduleName: String
2222
var initialProps: RCTRootViewRepresentable.InitialPropsType
2323
var onOpenURLCallback: ((URL) -> ())?
24+
let windowId: String = "0"
25+
26+
@Environment(\.scenePhase) private var scenePhase
27+
2428

2529
public init(moduleName: String, initialProps: RCTRootViewRepresentable.InitialPropsType = nil) {
2630
self.moduleName = moduleName
@@ -31,13 +35,27 @@ public struct RCTMainWindow: Scene {
3135
WindowGroup {
3236
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
3337
.modifier(WindowHandlingModifier())
38+
.onChange(of: scenePhase, { _, newValue in
39+
postWindowStateNotification(windowId: windowId, state: newValue)
40+
})
3441
.onOpenURL(perform: { url in
3542
onOpenURLCallback?(url)
3643
})
3744
}
3845
}
3946
}
4047

48+
public func postWindowStateNotification(windowId: String, state: SwiftUI.ScenePhase) {
49+
NotificationCenter.default.post(
50+
name: NSNotification.Name(rawValue: "RCTWindowStateDidChange"),
51+
object: nil,
52+
userInfo: [
53+
"windowId": windowId,
54+
"state": "\(state)"
55+
]
56+
)
57+
}
58+
4159
extension RCTMainWindow {
4260
public func onOpenURL(perform action: @escaping (URL) -> ()) -> some Scene {
4361
var scene = self

packages/react-native/Libraries/SwiftExtensions/RCTWindow.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public struct RCTWindow : Scene {
1313
var id: String
1414
var sceneData: RCTSceneData?
1515
var moduleName: String
16+
@Environment(\.scenePhase) private var scenePhase
17+
1618

1719
public init(id: String, moduleName: String, sceneData: RCTSceneData?) {
1820
self.id = id
@@ -25,6 +27,9 @@ public struct RCTWindow : Scene {
2527
Group {
2628
if let sceneData {
2729
RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData.props)
30+
.onChange(of: scenePhase) { _, newValue in
31+
postWindowStateNotification(windowId: id, state: newValue)
32+
}
2833
}
2934
}
3035
.onAppear {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#import <Foundation/Foundation.h>
22
#import <React/RCTBridgeModule.h>
3+
#import <React/RCTEventEmitter.h>
34

4-
@interface RCTWindowManager : NSObject <RCTBridgeModule>
5+
@interface RCTWindowManager : RCTEventEmitter
56

67
@end

packages/react-native/Libraries/WindowManager/RCTWindowManager.mm

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,36 @@
1010
static NSString *const RCTOpenWindow = @"RCTOpenWindow";
1111
static NSString *const RCTDismissWindow = @"RCTDismissWindow";
1212
static NSString *const RCTUpdateWindow = @"RCTUpdateWindow";
13+
static NSString *const RCTWindowStateDidChangeEvent = @"windowStateDidChange";
1314

14-
@interface RCTWindowManager () <NativeWindowManagerSpec>
15+
static NSString *const RCTWindowStateDidChange = @"RCTWindowStateDidChange";
16+
17+
@interface RCTWindowManager () <NativeWindowManagerSpec> {
18+
BOOL _hasAnyListeners;
19+
}
1520
@end
1621

1722
@implementation RCTWindowManager
1823

1924
RCT_EXPORT_MODULE(WindowManager)
2025

26+
- (void)initialize {
27+
[[NSNotificationCenter defaultCenter] addObserver:self
28+
selector:@selector(handleWindowStateChanges:)
29+
name:RCTWindowStateDidChange
30+
object:nil];
31+
}
32+
33+
-(void)startObserving
34+
{
35+
_hasAnyListeners = YES;
36+
}
37+
38+
- (void)stopObserving
39+
{
40+
_hasAnyListeners = NO;
41+
}
42+
2143
RCT_EXPORT_METHOD(openWindow
2244
: (NSString *)windowId userInfo
2345
: (NSDictionary *)userInfo resolve
@@ -68,6 +90,17 @@ @implementation RCTWindowManager
6890
});
6991
}
7092

93+
- (void) handleWindowStateChanges:(NSNotification *)notification {
94+
95+
if (_hasAnyListeners) {
96+
[self sendEventWithName:RCTWindowStateDidChangeEvent body:notification.userInfo];
97+
}
98+
}
99+
100+
- (NSArray<NSString *> *)supportedEvents {
101+
return @[RCTWindowStateDidChangeEvent];
102+
}
103+
71104
- (facebook::react::ModuleConstants<JS::NativeWindowManager::Constants::Builder>)constantsToExport {
72105
return [self getConstants];
73106
}
@@ -87,4 +120,13 @@ @implementation RCTWindowManager
87120
return std::make_shared<facebook::react::NativeWindowManagerSpecJSI>(params);
88121
}
89122

123+
+ (BOOL)requiresMainQueueSetup {
124+
return YES;
125+
}
126+
127+
- (dispatch_queue_t)methodQueue
128+
{
129+
return dispatch_get_main_queue();
130+
}
131+
90132
@end

packages/react-native/Libraries/WindowManager/WindowManager.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1+
import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
2+
3+
type WindowManagerEvents = 'windowStateDidChange';
4+
5+
type WindowState = {
6+
windowId: string;
7+
state: 'active' | 'inactive' | 'background';
8+
};
9+
110
export interface WindowStatic {
211
id: String;
312
open (props?: Object): Promise<void>;
413
update (props: Object): Promise<void>;
514
close (): Promise<void>;
15+
addEventListener (type: WindowManagerEvents, handler: (info: WindowState) => void): NativeEventSubscription;
616
}
717

818
export interface WindowManagerStatic {

packages/react-native/Libraries/WindowManager/WindowManager.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,54 @@
11
/**
22
* @format
3-
* @flow strict
3+
* @flow strict-local
44
* @jsdoc
55
*/
66

7+
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
8+
import Platform from '../Utilities/Platform';
9+
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
710
import NativeWindowManager from './NativeWindowManager';
811

9-
const WindowManager = {
10-
getWindow: function (id: string): Window {
12+
export type WindowStateValues = 'inactive' | 'background' | 'active';
13+
14+
type WindowManagerEventDefinitions = {
15+
windowStateDidChange: [{state: WindowStateValues, windowId: string}],
16+
};
17+
18+
let emitter: ?NativeEventEmitter<WindowManagerEventDefinitions>;
19+
20+
if (NativeWindowManager != null) {
21+
emitter = new NativeEventEmitter<WindowManagerEventDefinitions>(
22+
Platform.OS !== 'ios' ? null : NativeWindowManager,
23+
);
24+
}
25+
26+
class WindowManager {
27+
static getWindow = function (id: string): Window {
1128
return new Window(id);
12-
},
29+
};
30+
31+
static addEventListener<K: $Keys<WindowManagerEventDefinitions>>(
32+
type: K,
33+
handler: (...$ElementType<WindowManagerEventDefinitions, K>) => void,
34+
): ?EventSubscription {
35+
return emitter?.addListener(type, handler);
36+
}
1337

1438
// $FlowIgnore[unsafe-getters-setters]
15-
get supportsMultipleScenes(): boolean {
39+
static get supportsMultipleScenes(): boolean {
1640
if (NativeWindowManager == null) {
1741
return false;
1842
}
1943

2044
const nativeConstants = NativeWindowManager.getConstants();
2145
return nativeConstants.supportsMultipleScenes || false;
22-
},
23-
};
46+
}
47+
}
2448

2549
class Window {
2650
id: string;
51+
isOpen: boolean = false;
2752

2853
constructor(id: string) {
2954
this.id = id;

packages/react-native/src/private/specs/visionos_modules/NativeWindowManager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export interface Spec extends TurboModule {
1919
// $FlowIgnore[unclear-type]
2020
+updateWindow: (windowId: string, userInfo: Object) => Promise<void>;
2121
+closeWindow: (windowId: string) => Promise<void>;
22+
23+
// RCTEventEmitter
24+
+addListener: (eventName: string) => void;
25+
+removeListeners: (count: number) => void;
2226
}
2327

2428
export default (TurboModuleRegistry.get<Spec>('WindowManager'): ?Spec);

packages/rn-tester/js/examples/XR/XRExample.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ const secondWindow = WindowManager.getWindow('SecondWindow');
1919
const OpenXRSession = () => {
2020
const [isOpen, setIsOpen] = React.useState(false);
2121

22+
React.useEffect(() => {
23+
const listener = WindowManager.addEventListener(
24+
'windowStateDidChange',
25+
data => {
26+
console.log('Window state changed to:', data);
27+
},
28+
);
29+
return () => {
30+
listener?.remove();
31+
};
32+
}, []);
2233
const openXRSession = async () => {
2334
try {
2435
if (!WindowManager.supportsMultipleScenes) {

0 commit comments

Comments
 (0)