Skip to content

feat: implement window manager events #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public struct RCTMainWindow: Scene {
var moduleName: String
var initialProps: RCTRootViewRepresentable.InitialPropsType
var onOpenURLCallback: ((URL) -> ())?
let windowId: String = "0"

@Environment(\.scenePhase) private var scenePhase


public init(moduleName: String, initialProps: RCTRootViewRepresentable.InitialPropsType = nil) {
self.moduleName = moduleName
Expand All @@ -31,13 +35,27 @@ public struct RCTMainWindow: Scene {
WindowGroup {
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
.modifier(WindowHandlingModifier())
.onChange(of: scenePhase, { _, newValue in
postWindowStateNotification(windowId: windowId, state: newValue)
})
.onOpenURL(perform: { url in
onOpenURLCallback?(url)
})
}
}
}

public func postWindowStateNotification(windowId: String, state: SwiftUI.ScenePhase) {
NotificationCenter.default.post(
name: NSNotification.Name(rawValue: "RCTWindowStateDidChange"),
object: nil,
userInfo: [
"windowId": windowId,
"state": "\(state)"
]
)
}

extension RCTMainWindow {
public func onOpenURL(perform action: @escaping (URL) -> ()) -> some Scene {
var scene = self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public struct RCTWindow : Scene {
var id: String
var sceneData: RCTSceneData?
var moduleName: String
@Environment(\.scenePhase) private var scenePhase


public init(id: String, moduleName: String, sceneData: RCTSceneData?) {
self.id = id
Expand All @@ -25,6 +27,9 @@ public struct RCTWindow : Scene {
Group {
if let sceneData {
RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData.props)
.onChange(of: scenePhase) { _, newValue in
postWindowStateNotification(windowId: id, state: newValue)
}
}
}
.onAppear {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface RCTWindowManager : NSObject <RCTBridgeModule>
@interface RCTWindowManager : RCTEventEmitter

@end
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,41 @@
static NSString *const RCTOpenWindow = @"RCTOpenWindow";
static NSString *const RCTDismissWindow = @"RCTDismissWindow";
static NSString *const RCTUpdateWindow = @"RCTUpdateWindow";
static NSString *const RCTWindowStateDidChangeEvent = @"windowStateDidChange";

@interface RCTWindowManager () <NativeWindowManagerSpec>
static NSString *const RCTWindowStateDidChange = @"RCTWindowStateDidChange";

@interface RCTWindowManager () <NativeWindowManagerSpec> {
BOOL _hasAnyListeners;
}
@end

@implementation RCTWindowManager

RCT_EXPORT_MODULE(WindowManager)

- (void)initialize {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleWindowStateChanges:)
name:RCTWindowStateDidChange
object:nil];
}

- (void)invalidate {
[super invalidate];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(void)startObserving
{
_hasAnyListeners = YES;
}

- (void)stopObserving
{
_hasAnyListeners = NO;
}

RCT_EXPORT_METHOD(openWindow
: (NSString *)windowId userInfo
: (NSDictionary *)userInfo resolve
Expand Down Expand Up @@ -68,6 +95,17 @@ @implementation RCTWindowManager
});
}

- (void) handleWindowStateChanges:(NSNotification *)notification {

if (_hasAnyListeners) {
[self sendEventWithName:RCTWindowStateDidChangeEvent body:notification.userInfo];
}
}

- (NSArray<NSString *> *)supportedEvents {
return @[RCTWindowStateDidChangeEvent];
}

- (facebook::react::ModuleConstants<JS::NativeWindowManager::Constants::Builder>)constantsToExport {
return [self getConstants];
}
Expand All @@ -87,4 +125,13 @@ @implementation RCTWindowManager
return std::make_shared<facebook::react::NativeWindowManagerSpecJSI>(params);
}

+ (BOOL)requiresMainQueueSetup {
return YES;
}

- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}

@end
10 changes: 10 additions & 0 deletions packages/react-native/Libraries/WindowManager/WindowManager.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';

type WindowManagerEvents = 'windowStateDidChange';

type WindowState = {
windowId: string;
state: 'active' | 'inactive' | 'background';
};

export interface WindowStatic {
id: String;
open (props?: Object): Promise<void>;
update (props: Object): Promise<void>;
close (): Promise<void>;
addEventListener (type: WindowManagerEvents, handler: (info: WindowState) => void): NativeEventSubscription;
}

export interface WindowManagerStatic {
Expand Down
38 changes: 31 additions & 7 deletions packages/react-native/Libraries/WindowManager/WindowManager.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
/**
* @format
* @flow strict
* @flow strict-local
* @jsdoc
*/

import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import Platform from '../Utilities/Platform';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import NativeWindowManager from './NativeWindowManager';

const WindowManager = {
getWindow: function (id: string): Window {
export type WindowStateValues = 'inactive' | 'background' | 'active';

type WindowManagerEventDefinitions = {
windowStateDidChange: [{state: WindowStateValues, windowId: string}],
};

let emitter: ?NativeEventEmitter<WindowManagerEventDefinitions>;

if (NativeWindowManager != null) {
emitter = new NativeEventEmitter<WindowManagerEventDefinitions>(
Platform.OS !== 'ios' ? null : NativeWindowManager,
);
}

class WindowManager {
static getWindow = function (id: string): Window {
return new Window(id);
},
};

static addEventListener<K: $Keys<WindowManagerEventDefinitions>>(
type: K,
handler: (...$ElementType<WindowManagerEventDefinitions, K>) => void,
): ?EventSubscription {
return emitter?.addListener(type, handler);
}

// $FlowIgnore[unsafe-getters-setters]
get supportsMultipleScenes(): boolean {
static get supportsMultipleScenes(): boolean {
if (NativeWindowManager == null) {
return false;
}

const nativeConstants = NativeWindowManager.getConstants();
return nativeConstants.supportsMultipleScenes || false;
},
};
}
}

class Window {
id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8982,16 +8982,17 @@ declare export default typeof NativeWindowManager;
`;

exports[`public API should not change unintentionally Libraries/WindowManager/WindowManager.js 1`] = `
"declare const WindowManager: {
getWindow: (id: string) => Window,
get supportsMultipleScenes(): boolean,
};
declare class Window {
id: string;
constructor(id: string): void;
open(props: ?Object): Promise<void>;
close(): Promise<void>;
update(props: ?Object): Promise<void>;
"export type WindowStateValues = \\"inactive\\" | \\"background\\" | \\"active\\";
type WindowManagerEventDefinitions = {
windowStateDidChange: [{ state: WindowStateValues, windowId: string }],
};
declare class WindowManager {
static getWindow: $FlowFixMe;
static addEventListener<K: $Keys<WindowManagerEventDefinitions>>(
type: K,
handler: (...$ElementType<WindowManagerEventDefinitions, K>) => void
): ?EventSubscription;
static get supportsMultipleScenes(): boolean;
}
declare module.exports: WindowManager;
"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface Spec extends TurboModule {
// $FlowIgnore[unclear-type]
+updateWindow: (windowId: string, userInfo: Object) => Promise<void>;
+closeWindow: (windowId: string) => Promise<void>;

// RCTEventEmitter
+addListener: (eventName: string) => void;
+removeListeners: (count: number) => void;
}

export default (TurboModuleRegistry.get<Spec>('WindowManager'): ?Spec);
11 changes: 11 additions & 0 deletions packages/rn-tester/js/examples/XR/XRExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ const secondWindow = WindowManager.getWindow('SecondWindow');
const OpenXRSession = () => {
const [isOpen, setIsOpen] = React.useState(false);

React.useEffect(() => {
const listener = WindowManager.addEventListener(
'windowStateDidChange',
data => {
console.log('Window state changed to:', data);
},
);
return () => {
listener?.remove();
};
}, []);
const openXRSession = async () => {
try {
if (!WindowManager.supportsMultipleScenes) {
Expand Down