Skip to content

Commit 1a97f77

Browse files
committed
feat: introduce WindowManager
1 parent de5ae39 commit 1a97f77

26 files changed

+447
-241
lines changed

packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
8181

8282
RCTAppSetupPrepareApp(application, enableTM, *_reactNativeConfig);
8383

84-
#if TARGET_OS_VISION
85-
/// Bail out of UIWindow initializaiton to support multi-window scenarios in SwiftUI lifecycle.
8684
return YES;
87-
#else
88-
UIView* rootView = [self viewWithModuleName:self.moduleName initialProperties:[self prepareInitialProps] launchOptions:launchOptions];
89-
90-
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
91-
92-
UIViewController *rootViewController = [self createRootViewController];
93-
[self setRootView:rootView toRootViewController:rootViewController];
94-
self.window.rootViewController = rootViewController;
95-
self.window.windowScene.delegate = self;
96-
[self.window makeKeyAndVisible];
97-
98-
return YES;
99-
#endif
10085
}
10186

10287
- (void)applicationDidEnterBackground:(UIApplication *)application

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

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,11 @@ public struct RCTMainWindow: Scene {
2929
public var body: some Scene {
3030
WindowGroup {
3131
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
32-
#if os(visionOS)
3332
.modifier(WindowHandlingModifier())
34-
#endif
3533
}
3634
}
3735
}
3836

39-
#if os(visionOS)
4037
/**
4138
Handles data sharing between React Native and SwiftUI views.
4239
*/
@@ -55,16 +52,15 @@ struct WindowHandlingModifier: ViewModifier {
5552
openWindow(id: id)
5653
}
5754
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTUpdateWindow"))) { data in
58-
guard let id = data.userInfo?["id"] as? String else { return }
59-
if let userInfo = data.userInfo?["userInfo"] as? UserInfoType {
60-
reactContext.scenes[id]?.props = userInfo
61-
}
55+
guard
56+
let id = data.userInfo?["id"] as? String,
57+
let userInfo = data.userInfo?["userInfo"] as? UserInfoType else { return }
58+
reactContext.scenes[id]?.props = userInfo
6259
}
6360
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissWindow"))) { data in
64-
if let id = data.userInfo?["id"] as? String {
65-
dismissWindow(id: id)
66-
reactContext.scenes.removeValue(forKey: id)
67-
}
61+
guard let id = data.userInfo?["id"] as? String else { return }
62+
dismissWindow(id: id)
63+
reactContext.scenes.removeValue(forKey: id)
6864
}
6965
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenImmersiveSpace"))) { data in
7066
guard let id = data.userInfo?["id"] as? String else { return }
@@ -74,10 +70,8 @@ struct WindowHandlingModifier: ViewModifier {
7470
)
7571
}
7672
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissImmersiveSpace"))) { data in
77-
if let spaceId = data.userInfo?["id"] as? String {
78-
reactContext.scenes.removeValue(forKey: spaceId)
79-
}
73+
guard let id = data.userInfo?["id"] as? String else { return }
74+
reactContext.scenes.removeValue(forKey: id)
8075
}
8176
}
8277
}
83-
#endif

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import SwiftUI
2+
import React
23

34
/**
4-
`RCTWindow` is a SwiftUI struct that returns additional scenes.
5+
`RCTWindow` is a SwiftUI struct that returns additional scenes.
56

67
Example usage:
78
```
@@ -21,8 +22,15 @@ public struct RCTWindow : Scene {
2122

2223
public var body: some Scene {
2324
WindowGroup(id: id) {
24-
if let sceneData {
25-
RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData.props)
25+
Group {
26+
if let sceneData {
27+
RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData.props)
28+
}
29+
}
30+
.onAppear {
31+
if sceneData == nil {
32+
RCTFatal(RCTErrorWithMessage("Passed scene data is nil, make sure to pass sceneContext to RCTWindow() in App.swift"))
33+
}
2634
}
2735
}
2836
}

packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ Pod::Spec.new do |s|
2525

2626
s.dependency "React-Core"
2727
s.dependency "React-RCTXR"
28+
s.dependency "React-RCTWindowManager"
2829
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @flow strict
3+
* @format
4+
*/
5+
6+
export * from '../../src/private/specs/visionos_modules/NativeWindowManager';
7+
import NativeWindowManager from '../../src/private/specs/visionos_modules/NativeWindowManager';
8+
export default NativeWindowManager;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#import <Foundation/Foundation.h>
2+
#import <React/RCTBridgeModule.h>
3+
4+
@interface RCTWindowManager : NSObject <RCTBridgeModule>
5+
6+
@end
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#import <React/RCTWindowManager.h>
2+
3+
#import <FBReactNativeSpec_visionOS/FBReactNativeSpec_visionOS.h>
4+
5+
#import <React/RCTBridge.h>
6+
#import <React/RCTConvert.h>
7+
#import <React/RCTUtils.h>
8+
9+
// Events
10+
static NSString *const RCTOpenWindow = @"RCTOpenWindow";
11+
static NSString *const RCTDismissWindow = @"RCTDismissWindow";
12+
static NSString *const RCTUpdateWindow = @"RCTUpdateWindow";
13+
14+
@interface RCTWindowManager () <NativeWindowManagerSpec>
15+
@end
16+
17+
@implementation RCTWindowManager
18+
19+
RCT_EXPORT_MODULE(WindowManager)
20+
21+
RCT_EXPORT_METHOD(openWindow
22+
: (NSString *)windowId userInfo
23+
: (NSDictionary *)userInfo resolve
24+
: (RCTPromiseResolveBlock)resolve reject
25+
: (RCTPromiseRejectBlock)reject)
26+
{
27+
RCTExecuteOnMainQueue(^{
28+
if (!RCTSharedApplication().supportsMultipleScenes) {
29+
reject(@"ERROR", @"Multiple scenes not supported", nil);
30+
}
31+
NSMutableDictionary *userInfoDict = [[NSMutableDictionary alloc] init];
32+
[userInfoDict setValue:windowId forKey:@"id"];
33+
if (userInfo != nil) {
34+
[userInfoDict setValue:userInfo forKey:@"userInfo"];
35+
}
36+
[[NSNotificationCenter defaultCenter] postNotificationName:RCTOpenWindow object:self userInfo:userInfoDict];
37+
resolve(nil);
38+
});
39+
}
40+
41+
RCT_EXPORT_METHOD(closeWindow
42+
: (NSString *)windowId resolve
43+
: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
44+
{
45+
RCTExecuteOnMainQueue(^{
46+
[[NSNotificationCenter defaultCenter] postNotificationName:RCTDismissWindow object:self userInfo:@{@"id": windowId}];
47+
resolve(nil);
48+
});
49+
}
50+
51+
RCT_EXPORT_METHOD(updateWindow
52+
: (NSString *)windowId userInfo
53+
: (NSDictionary *)userInfo resolve
54+
: (RCTPromiseResolveBlock)resolve reject
55+
: (RCTPromiseRejectBlock)reject)
56+
{
57+
RCTExecuteOnMainQueue(^{
58+
if (!RCTSharedApplication().supportsMultipleScenes) {
59+
reject(@"ERROR", @"Multiple scenes not supported", nil);
60+
}
61+
NSMutableDictionary *userInfoDict = [[NSMutableDictionary alloc] init];
62+
[userInfoDict setValue:windowId forKey:@"id"];
63+
if (userInfo != nil) {
64+
[userInfoDict setValue:userInfo forKey:@"userInfo"];
65+
}
66+
[[NSNotificationCenter defaultCenter] postNotificationName:RCTUpdateWindow object:self userInfo:userInfoDict];
67+
resolve(nil);
68+
});
69+
}
70+
71+
- (facebook::react::ModuleConstants<JS::NativeWindowManager::Constants::Builder>)constantsToExport {
72+
return [self getConstants];
73+
}
74+
75+
- (facebook::react::ModuleConstants<JS::NativeWindowManager::Constants>)getConstants {
76+
__block facebook::react::ModuleConstants<JS::NativeWindowManager::Constants> constants;
77+
RCTUnsafeExecuteOnMainQueueSync(^{
78+
constants = facebook::react::typedConstants<JS::NativeWindowManager::Constants>({
79+
.supportsMultipleScenes = RCTSharedApplication().supportsMultipleScenes
80+
});
81+
});
82+
83+
return constants;
84+
}
85+
86+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
87+
return std::make_shared<facebook::react::NativeWindowManagerSpecJSI>(params);
88+
}
89+
90+
@end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require "json"
2+
3+
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
4+
version = package['version']
5+
6+
source = { :git => 'https://github.com/facebook/react-native.git' }
7+
if version == '1000.0.0'
8+
# This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in.
9+
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
10+
else
11+
source[:tag] = "v#{version}"
12+
end
13+
14+
folly_config = get_folly_config()
15+
folly_compiler_flags = folly_config[:compiler_flags]
16+
folly_version = folly_config[:version]
17+
18+
header_search_paths = [
19+
"\"$(PODS_ROOT)/RCT-Folly\"",
20+
"\"${PODS_ROOT}/Headers/Public/React-Codegen/react/renderer/components\"",
21+
]
22+
23+
Pod::Spec.new do |s|
24+
s.name = "React-RCTWindowManager"
25+
s.version = version
26+
s.summary = "Window manager module for React Native."
27+
s.homepage = "https://callstack.github.io/react-native-visionos-docs"
28+
s.documentation_url = "https://callstack.github.io/react-native-visionos-docs/api/windowmanager"
29+
s.license = package["license"]
30+
s.author = "Callstack"
31+
s.platforms = min_supported_versions
32+
s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness'
33+
s.source = source
34+
s.source_files = "*.{m,mm,swift}"
35+
s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
36+
s.header_dir = "RCTWindowManager"
37+
s.pod_target_xcconfig = {
38+
"USE_HEADERMAP" => "YES",
39+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
40+
"HEADER_SEARCH_PATHS" => header_search_paths.join(' ')
41+
}
42+
43+
s.dependency "RCT-Folly", folly_version
44+
s.dependency "RCTTypeSafety"
45+
s.dependency "React-jsi"
46+
s.dependency "React-Core/RCTWindowManagerHeaders"
47+
48+
add_dependency(s, "React-Codegen", :additional_framework_paths => ["build/generated/ios"])
49+
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
50+
add_dependency(s, "React-NativeModulesApple", :additional_framework_paths => ["build/generated/ios"])
51+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface WindowStatic {
2+
id: String;
3+
open (props: Object): Promise<void>;
4+
update (props: Object): Promise<void>;
5+
close (): Promise<void>;
6+
}
7+
8+
export interface WindowManagerStatic {
9+
getWindow(id: String): Window;
10+
supportsMultipleScenes: boolean;
11+
}
12+
13+
export const WindowManager: WindowManagerStatic;
14+
export type WindowManager = WindowManagerStatic;
15+
export const Window: WindowStatic;
16+
export type Window = WindowStatic;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @format
3+
* @flow strict
4+
* @jsdoc
5+
*/
6+
7+
import NativeWindowManager from './NativeWindowManager';
8+
9+
const WindowManager = {
10+
getWindow: function (id: string): Window {
11+
return new Window(id);
12+
},
13+
14+
// $FlowIgnore[unsafe-getters-setters]
15+
get supportsMultipleScenes(): boolean {
16+
if (NativeWindowManager == null) {
17+
return false;
18+
}
19+
20+
const nativeConstants = NativeWindowManager.getConstants();
21+
return nativeConstants.supportsMultipleScenes || false;
22+
},
23+
};
24+
25+
class Window {
26+
id: string;
27+
28+
constructor(id: string) {
29+
this.id = id;
30+
}
31+
32+
// $FlowIgnore[unclear-type]
33+
open(props: ?Object): Promise<void> {
34+
if (NativeWindowManager != null && NativeWindowManager.openWindow != null) {
35+
return NativeWindowManager.openWindow(this.id, props);
36+
}
37+
return Promise.reject(new Error('NativeWindowManager is not available'));
38+
}
39+
40+
// $FlowIgnore[unclear-type]
41+
close(): Promise<void> {
42+
if (
43+
NativeWindowManager != null &&
44+
NativeWindowManager.closeWindow != null
45+
) {
46+
return NativeWindowManager.closeWindow(this.id);
47+
}
48+
return Promise.reject(new Error('NativeWindowManager is not available'));
49+
}
50+
51+
// $FlowIgnore[unclear-type]
52+
update(props: ?Object): Promise<void> {
53+
if (
54+
NativeWindowManager != null &&
55+
NativeWindowManager.updateWindow != null
56+
) {
57+
return NativeWindowManager.updateWindow(this.id, props);
58+
}
59+
return Promise.reject(new Error('NativeWindowManager is not available'));
60+
}
61+
}
62+
63+
module.exports = WindowManager;

0 commit comments

Comments
 (0)