Skip to content

Commit 6caf418

Browse files
committed
feat: introduce WindowManager
1 parent de5ae39 commit 6caf418

27 files changed

+470
-255
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: 28 additions & 28 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
*/
@@ -46,38 +43,41 @@ struct WindowHandlingModifier: ViewModifier {
4643
@Environment(\.reactContext) private var reactContext
4744
@Environment(\.openWindow) private var openWindow
4845
@Environment(\.dismissWindow) private var dismissWindow
46+
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
4947

5048
func body(content: Content) -> some View {
51-
content
52-
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenWindow"))) { data in
53-
guard let id = data.userInfo?["id"] as? String else { return }
54-
reactContext.scenes.updateValue(RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType), forKey: id)
55-
openWindow(id: id)
56-
}
57-
.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 {
49+
// Attach listeners only if app supports multiple windows
50+
if supportsMultipleWindows {
51+
content
52+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenWindow"))) { data in
53+
guard let id = data.userInfo?["id"] as? String else { return }
54+
reactContext.scenes.updateValue(RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType), forKey: id)
55+
openWindow(id: id)
56+
}
57+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTUpdateWindow"))) { data in
58+
guard
59+
let id = data.userInfo?["id"] as? String,
60+
let userInfo = data.userInfo?["userInfo"] as? UserInfoType else { return }
6061
reactContext.scenes[id]?.props = userInfo
6162
}
62-
}
63-
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissWindow"))) { data in
64-
if let id = data.userInfo?["id"] as? String {
63+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissWindow"))) { data in
64+
guard let id = data.userInfo?["id"] as? String else { return }
6565
dismissWindow(id: id)
6666
reactContext.scenes.removeValue(forKey: id)
6767
}
68-
}
69-
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenImmersiveSpace"))) { data in
70-
guard let id = data.userInfo?["id"] as? String else { return }
71-
reactContext.scenes.updateValue(
72-
RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType),
73-
forKey: id
74-
)
75-
}
76-
.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)
68+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenImmersiveSpace"))) { data in
69+
guard let id = data.userInfo?["id"] as? String else { return }
70+
reactContext.scenes.updateValue(
71+
RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType),
72+
forKey: id
73+
)
74+
}
75+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissImmersiveSpace"))) { data in
76+
guard let id = data.userInfo?["id"] as? String else { return }
77+
reactContext.scenes.removeValue(forKey: id)
7978
}
80-
}
79+
} else {
80+
content
81+
}
8182
}
8283
}
83-
#endif

packages/react-native/Libraries/SwiftExtensions/RCTReactViewController.m

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ - (void)tapGesture:(UITapGestureRecognizer*)recognizer {
6060
}
6161

6262
- (void)updateProps:(NSDictionary *)newProps {
63-
[(RCTRootView *)self.view setAppProperties:newProps];
63+
RCTRootView *rootView = (RCTRootView *)self.view;
64+
if (![rootView.appProperties isEqualToDictionary:newProps]) {
65+
[rootView setAppProperties:newProps];
66+
}
6467
}
6568

6669
@end

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;

0 commit comments

Comments
 (0)