Skip to content

Commit dd16e36

Browse files
committed
feat: add multi-window support (#117)
* feat: add multi-window support * feat: introduce WindowManager fix: RCTReactViewController properly check props to update fix: use clearColor instead of systemBackgroundColor for visionOS (#125) feat: allow to use WindowHandlingModifier outside of RCTMainWindow fix: deep and universal links when app is running (#140) Co-authored-by: Thiago Brezinski <thiagobrez@gmail.com> fix: remove window init feat: add support for ornaments & dev menu trigger (#149) * feat: add support for ornaments * feat: add ornaments support to second window fix: allow to manually move dev menu to avoid conflicts (#150) fix: remove unnecessary diff after upstreaming changes (#151) Make CMake 3.29.0 as minimum required version (#155) fix: move visionOS codegen specs, sync with upstream chore: sync with upstream fix: remove template Move template to a separate repo fix: update oot-release scripts chore: remove unnecessary diff (#159) fix: react-native-config chore: sync with upstream chore: sync with upstrteam
1 parent 3657025 commit dd16e36

File tree

76 files changed

+1119
-1887
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1119
-1887
lines changed

.github/workflows/test-all.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
name: Test All
22

3-
on:
4-
workflow_dispatch:
5-
pull_request:
6-
push:
7-
branches:
8-
- main
9-
- "*-stable"
3+
# on:
4+
# workflow_dispatch:
5+
# pull_request:
6+
# push:
7+
# tags:
8+
# - 'v*'
9+
# # nightly build @ 2:15 AM UTC
10+
# schedule:
11+
# - cron: '15 2 * * *'
1012

1113
concurrency:
1214
group: ${{ github.workflow }}-${{ github.ref }}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The source for the React Native visionOS documentation and website is hosted on
3131

3232
Prerequisites:
3333
- Download the latest Xcode (at least 15.2)
34-
- Install the latest version of CMake (at least v3.28.0)
34+
- Install the latest version of CMake (at least v3.29.0)
3535

3636
Check out `rn-tester` [README.md](./packages/rn-tester/README.md) to build React Native from the source.
3737

packages/helloworld/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
},
1414
"dependencies": {
1515
"react": "18.3.1",
16-
"react-native": "0.76.2"
16+
"react-native": "0.76.2",
17+
"@callstack/react-native-visionos": "0.76.2"
1718
},
1819
"devDependencies": {
1920
"@babel/core": "^7.25.2",

packages/out-of-tree-platforms/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@callstack/out-of-tree-platforms",
3-
"version": "0.75.0-main",
3+
"version": "0.76.0-main",
44
"description": "Utils for React Native out of tree platforms.",
55
"keywords": ["out-of-tree", "react-native"],
66
"homepage": "https://github.com/callstack/react-native-visionos/tree/HEAD/packages/out-of-tree-platforms#readme",

packages/react-native-test-library/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"devDependencies": {
2828
"@babel/core": "^7.25.2",
2929
"@react-native/babel-preset": "0.76.2",
30-
"react-native": "0.76.2"
30+
"react-native": "0.76.2",
31+
"@callstack/react-native-visionos": "0.76.2"
3132
},
3233
"peerDependencies": {
3334
"react": "*",

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ NS_ASSUME_NONNULL_BEGIN
5959

6060
/// The window object, used to render the UViewControllers
6161
@property (nonatomic, strong, nonnull) UIWindow *window;
62-
@property (nonatomic, nullable) RCTBridge *bridge;
62+
/// Store last focused window to properly handle multi-window scenarios
63+
@property (nonatomic, weak, nullable) UIWindow *lastFocusedWindow;
64+
@property (nonatomic, strong, nullable) RCTBridge *bridge;
6365
@property (nonatomic, strong, nullable) NSString *moduleName;
6466
@property (nonatomic, strong, nullable) NSDictionary *initialProps;
6567
@property (nonatomic, strong, nonnull) RCTRootViewFactory *rootViewFactory;

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

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,9 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
6262
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
6363
}
6464

65-
if (self.automaticallyLoadReactNativeWindow) {
66-
[self loadReactNativeWindow:launchOptions];
67-
}
68-
6965
return YES;
7066
}
7167

72-
- (void)loadReactNativeWindow:(NSDictionary *)launchOptions
73-
{
74-
UIView *rootView = [self.rootViewFactory viewWithModuleName:self.moduleName
75-
initialProperties:self.initialProps
76-
launchOptions:launchOptions];
77-
78-
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
79-
UIViewController *rootViewController = [self createRootViewController];
80-
[self setRootView:rootView toRootViewController:rootViewController];
81-
_window.windowScene.delegate = self;
82-
_window.rootViewController = rootViewController;
83-
[_window makeKeyAndVisible];
84-
}
85-
8668
- (void)applicationDidEnterBackground:(UIApplication *)application
8769
{
8870
// Noop
@@ -107,7 +89,11 @@ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
10789
BOOL enableFabric = self.fabricEnabled;
10890
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
10991

92+
#if TARGET_OS_VISION
93+
rootView.backgroundColor = [UIColor clearColor];
94+
#else
11095
rootView.backgroundColor = [UIColor systemBackgroundColor];
96+
#endif
11197

11298
return rootView;
11399
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,11 @@ - (UIView *)viewWithModuleName:(NSString *)moduleName
163163
initWithSurface:surface
164164
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];
165165

166+
#if TARGET_OS_VISION
167+
surfaceHostingProxyRootView.backgroundColor = [UIColor clearColor];
168+
#else
166169
surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor];
170+
#endif
167171
if (self->_configuration.customizeRootView != nil) {
168172
self->_configuration.customizeRootView(surfaceHostingProxyRootView);
169173
}
@@ -197,7 +201,11 @@ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
197201
BOOL enableFabric = self->_configuration.fabricEnabled;
198202
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);
199203

204+
#if TARGET_OS_VISION
205+
rootView.backgroundColor = [UIColor clearColor];
206+
#else
200207
rootView.backgroundColor = [UIColor systemBackgroundColor];
208+
#endif
201209

202210
return rootView;
203211
}

packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@
2424
continueUserActivity:(nonnull NSUserActivity *)userActivity
2525
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> *_Nullable))restorationHandler;
2626

27+
+ (void)onOpenURL:(nonnull NSURL *)url NS_SWIFT_NAME(onOpenURL(url:));
28+
2729
@end

packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.mm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#import "RCTLinkingPlugins.h"
1616

1717
static NSString *const kOpenURLNotification = @"RCTOpenURLNotification";
18+
static NSURL *initialURL = nil;
1819

1920
static void postNotificationWithURL(NSURL *URL, id sender)
2021
{
@@ -81,6 +82,16 @@ + (BOOL)application:(UIApplication *)application
8182
return YES;
8283
}
8384

85+
86+
+ (void)onOpenURL:(NSURL *)url
87+
{
88+
if (initialURL == nil) {
89+
initialURL = url;
90+
} else {
91+
postNotificationWithURL(url, self);
92+
}
93+
}
94+
8495
- (void)handleOpenURLNotification:(NSNotification *)notification
8596
{
8697
[self sendEventWithName:@"url" body:notification.userInfo];
@@ -153,6 +164,7 @@ - (void)handleOpenURLNotification:(NSNotification *)notification
153164

154165
RCT_EXPORT_METHOD(getInitialURL : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject)
155166
{
167+
#if !TARGET_OS_VISION
156168
NSURL *initialURL = nil;
157169
if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) {
158170
initialURL = self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey];
@@ -163,6 +175,8 @@ - (void)handleOpenURLNotification:(NSNotification *)notification
163175
initialURL = ((NSUserActivity *)userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]).webpageURL;
164176
}
165177
}
178+
#endif
179+
// React Native visionOS uses static property to retrieve initialURL.
166180
resolve(RCTNullIfNil(initialURL.absoluteString));
167181
}
168182

packages/react-native/Libraries/LinkingIOS/React-RCTLinking.podspec

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ Pod::Spec.new do |s|
4646
}
4747
s.framework = "UIKit"
4848

49-
s.framework = "UIKit"
50-
5149
s.dependency "React-Core/RCTLinkingHeaders", version
5250
s.dependency "ReactCommon/turbomodule/core", version
5351
s.dependency "React-jsi", version

packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ Pod::Spec.new do |s|
4545
}
4646
s.framework = ["UIKit", "QuartzCore"]
4747

48-
s.framework = ["UIKit", "QuartzCore"]
49-
5048
s.dependency "RCT-Folly", folly_version
5149
s.dependency "RCTTypeSafety"
5250
s.dependency "React-jsi"

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

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,121 @@ import SwiftUI
1515
}
1616
```
1717

18-
Note: If you want to create additional windows in your app, create a new `WindowGroup {}` and pass it a `RCTRootViewRepresentable`.
19-
*/
18+
Note: If you want to create additional windows in your app, use `RCTWindow()`.
19+
*/
2020
public struct RCTMainWindow: Scene {
2121
var moduleName: String
2222
var initialProps: RCTRootViewRepresentable.InitialPropsType
23+
var onOpenURLCallback: ((URL) -> ())?
24+
var devMenuSceneAnchor: UnitPoint?
25+
var contentView: AnyView?
2326

24-
public init(moduleName: String, initialProps: RCTRootViewRepresentable.InitialPropsType = nil) {
27+
var rootView: RCTRootViewRepresentable {
28+
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps, devMenuSceneAnchor: devMenuSceneAnchor)
29+
}
30+
31+
/// Creates new RCTMainWindowWindow.
32+
///
33+
/// - Parameters:
34+
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
35+
/// - initialProps: Initial properties for this view.
36+
/// - devMenuPlacement: Placement of the additional controls for triggering reload command and dev menu trigger.
37+
public init(
38+
moduleName: String,
39+
initialProps: RCTRootViewRepresentable.InitialPropsType = nil,
40+
devMenuSceneAnchor: UnitPoint? = .bottom
41+
) {
2542
self.moduleName = moduleName
2643
self.initialProps = initialProps
44+
self.devMenuSceneAnchor = devMenuSceneAnchor
45+
self.contentView = AnyView(rootView)
46+
}
47+
48+
/// Creates new RCTMainWindowWindow.
49+
///
50+
/// - Parameters:
51+
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
52+
/// - initialProps: Initial properties for this view.
53+
/// - devMenuPlacement: Placement of the additional controls for triggering reload command and dev menu trigger.
54+
/// - contentView: Closure which accepts rootView, allows to apply additional modifiers to React Native rootView.
55+
public init<Content: View>(
56+
moduleName: String,
57+
initialProps: RCTRootViewRepresentable.InitialPropsType = nil,
58+
devMenuSceneAnchor: UnitPoint? = .bottom,
59+
@ViewBuilder contentView: @escaping (_ view: RCTRootViewRepresentable) -> Content
60+
) {
61+
self.moduleName = moduleName
62+
self.initialProps = initialProps
63+
self.devMenuSceneAnchor = devMenuSceneAnchor
64+
self.contentView = AnyView(contentView(rootView))
2765
}
2866

2967
public var body: some Scene {
3068
WindowGroup {
31-
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
69+
contentView
70+
.modifier(WindowHandlingModifier())
71+
.onOpenURL(perform: { url in
72+
onOpenURLCallback?(url)
73+
})
74+
}
75+
}
76+
}
77+
78+
extension RCTMainWindow {
79+
public func onOpenURL(perform action: @escaping (URL) -> ()) -> Self {
80+
var scene = self
81+
scene.onOpenURLCallback = action
82+
return scene
83+
}
84+
}
85+
86+
/**
87+
Handles data sharing between React Native and SwiftUI views.
88+
*/
89+
public struct WindowHandlingModifier: ViewModifier {
90+
typealias UserInfoType = Dictionary<String, AnyHashable>
91+
92+
@Environment(\.reactContext) private var reactContext
93+
@Environment(\.openWindow) private var openWindow
94+
@Environment(\.dismissWindow) private var dismissWindow
95+
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
96+
97+
public init() {}
98+
99+
public func body(content: Content) -> some View {
100+
// Attach listeners only if app supports multiple windows
101+
if supportsMultipleWindows {
102+
content
103+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenWindow"))) { data in
104+
guard let id = data.userInfo?["id"] as? String else { return }
105+
reactContext.scenes.updateValue(RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType), forKey: id)
106+
openWindow(id: id)
107+
}
108+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTUpdateWindow"))) { data in
109+
guard
110+
let id = data.userInfo?["id"] as? String,
111+
let userInfo = data.userInfo?["userInfo"] as? UserInfoType else { return }
112+
reactContext.scenes[id]?.props = userInfo
113+
}
114+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissWindow"))) { data in
115+
guard let id = data.userInfo?["id"] as? String else { return }
116+
dismissWindow(id: id)
117+
reactContext.scenes.removeValue(forKey: id)
118+
}
119+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenImmersiveSpace"))) { data in
120+
guard let id = data.userInfo?["id"] as? String else { return }
121+
reactContext.scenes.updateValue(
122+
RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType),
123+
forKey: id
124+
)
125+
}
126+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissImmersiveSpace"))) { data in
127+
guard let id = data.userInfo?["id"] as? String else { return }
128+
reactContext.scenes.removeValue(forKey: id)
129+
}
130+
} else {
131+
content
32132
}
33133
}
34134
}
135+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import SwiftUI
2+
import Observation
3+
4+
@Observable
5+
public class RCTSceneData: Identifiable {
6+
public var id: String
7+
public var props: Dictionary<String, AnyHashable>?
8+
9+
init(id: String, props: Dictionary<String, AnyHashable>?) {
10+
self.id = id
11+
self.props = props
12+
}
13+
}
14+
15+
extension RCTSceneData: Equatable {
16+
public static func == (lhs: RCTSceneData, rhs: RCTSceneData) -> Bool {
17+
lhs.id == rhs.id && NSDictionary(dictionary: lhs.props ?? [:]).isEqual(to: rhs.props ?? [:])
18+
}
19+
}
20+
21+
@Observable
22+
public class RCTReactContext {
23+
public var scenes: Dictionary<String, RCTSceneData> = [:]
24+
25+
public func getSceneData(id: String) -> RCTSceneData? {
26+
return scenes[id]
27+
}
28+
}
29+
30+
extension RCTReactContext: Equatable {
31+
public static func == (lhs: RCTReactContext, rhs: RCTReactContext) -> Bool {
32+
NSDictionary(dictionary: lhs.scenes).isEqual(to: rhs.scenes)
33+
}
34+
}
35+
36+
public extension EnvironmentValues {
37+
var reactContext: RCTReactContext {
38+
get { self[RCTSceneContextKey.self] }
39+
set { self[RCTSceneContextKey.self] = newValue }
40+
}
41+
}
42+
43+
private struct RCTSceneContextKey: EnvironmentKey {
44+
static var defaultValue: RCTReactContext = RCTReactContext()
45+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@
1313
- (instancetype _Nonnull)initWithModuleName:(NSString *_Nonnull)moduleName
1414
initProps:(NSDictionary *_Nullable)initProps;
1515

16+
-(void)updateProps:(NSDictionary *_Nullable)newProps;
17+
1618
@end

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ - (void)updateProps:(NSDictionary *)newProps {
6464
return;
6565
}
6666

67+
68+
6769
if (newProps != nil && ![rootView.appProperties isEqualToDictionary:newProps]) {
68-
[rootView setAppProperties:newProps];
70+
NSMutableDictionary *newProperties = [rootView.appProperties mutableCopy];
71+
[newProperties setValuesForKeysWithDictionary:newProps];
72+
[rootView setAppProperties:newProperties];
6973
}
7074
}
7175
@end

0 commit comments

Comments
 (0)