Skip to content

Commit

Permalink
feat: add multi-window support (#117)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

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
  • Loading branch information
okwasniewski committed Sep 12, 2024
1 parent e043b79 commit 6c570a7
Show file tree
Hide file tree
Showing 76 changed files with 1,366 additions and 1,976 deletions.
16 changes: 9 additions & 7 deletions .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: Test All

on:
workflow_dispatch:
pull_request:
push:
branches:
- main
- "*-stable"
# on:
# workflow_dispatch:
# pull_request:
# push:
# tags:
# - 'v*'
# # nightly build @ 2:15 AM UTC
# schedule:
# - cron: '15 2 * * *'

jobs:
set_release_type:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The source for the React Native visionOS documentation and website is hosted on

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

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

Expand Down
2 changes: 1 addition & 1 deletion packages/helloworld/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"dependencies": {
"react": "19.0.0-rc-fb9a90fa48-20240614",
"react-native": "1000.0.0"
"@callstack/react-native-visionos": "1000.0.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/out-of-tree-platforms/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@callstack/out-of-tree-platforms",
"version": "0.75.0-main",
"version": "0.76.0-main",
"description": "Utils for React Native out of tree platforms.",
"keywords": ["out-of-tree", "react-native"],
"homepage": "https://github.com/callstack/react-native-visionos/tree/HEAD/packages/out-of-tree-platforms#readme",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-test-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@react-native/babel-preset": "0.76.0-main",
"react-native": "1000.0.0"
"@callstack/react-native-visionos": "1000.0.0"
},
"peerDependencies": {
"react": "*",
Expand Down
4 changes: 3 additions & 1 deletion packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ NS_ASSUME_NONNULL_BEGIN

/// The window object, used to render the UViewControllers
@property (nonatomic, strong, nonnull) UIWindow *window;
@property (nonatomic, nullable) RCTBridge *bridge;
/// Store last focused window to properly handle multi-window scenarios
@property (nonatomic, weak, nullable) UIWindow *lastFocusedWindow;
@property (nonatomic, strong, nullable) RCTBridge *bridge;
@property (nonatomic, strong, nullable) NSString *moduleName;
@property (nonatomic, strong, nullable) NSDictionary *initialProps;
@property (nonatomic, strong, nonnull) RCTRootViewFactory *rootViewFactory;
Expand Down
11 changes: 4 additions & 7 deletions packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
}

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [self createRootViewController];
[self setRootView:rootView toRootViewController:rootViewController];
self.window.rootViewController = rootViewController;
self.window.windowScene.delegate = self;
[self.window makeKeyAndVisible];

return YES;
}

Expand Down Expand Up @@ -87,7 +80,11 @@ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
BOOL enableFabric = self.fabricEnabled;
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);

#if TARGET_OS_VISION
rootView.backgroundColor = [UIColor clearColor];
#else
rootView.backgroundColor = [UIColor systemBackgroundColor];
#endif

return rootView;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,11 @@ - (UIView *)viewWithModuleName:(NSString *)moduleName
initWithSurface:surface
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];

#if TARGET_OS_VISION
surfaceHostingProxyRootView.backgroundColor = [UIColor clearColor];
#else
surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor];
#endif
if (self->_configuration.customizeRootView != nil) {
self->_configuration.customizeRootView(surfaceHostingProxyRootView);
}
Expand Down Expand Up @@ -183,7 +187,11 @@ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
BOOL enableFabric = self->_configuration.fabricEnabled;
UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric);

#if TARGET_OS_VISION
rootView.backgroundColor = [UIColor clearColor];
#else
rootView.backgroundColor = [UIColor systemBackgroundColor];
#endif

return rootView;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@
continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> *_Nullable))restorationHandler;

+ (void)onOpenURL:(nonnull NSURL *)url NS_SWIFT_NAME(onOpenURL(url:));

@end
14 changes: 14 additions & 0 deletions packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#import "RCTLinkingPlugins.h"

static NSString *const kOpenURLNotification = @"RCTOpenURLNotification";
static NSURL *initialURL = nil;

static void postNotificationWithURL(NSURL *URL, id sender)
{
Expand Down Expand Up @@ -81,6 +82,16 @@ + (BOOL)application:(UIApplication *)application
return YES;
}


+ (void)onOpenURL:(NSURL *)url
{
if (initialURL == nil) {
initialURL = url;
} else {
postNotificationWithURL(url, self);
}
}

- (void)handleOpenURLNotification:(NSNotification *)notification
{
[self sendEventWithName:@"url" body:notification.userInfo];
Expand Down Expand Up @@ -153,6 +164,7 @@ - (void)handleOpenURLNotification:(NSNotification *)notification

RCT_EXPORT_METHOD(getInitialURL : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject)
{
#if !TARGET_OS_VISION
NSURL *initialURL = nil;
if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) {
initialURL = self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey];
Expand All @@ -163,6 +175,8 @@ - (void)handleOpenURLNotification:(NSNotification *)notification
initialURL = ((NSUserActivity *)userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]).webpageURL;
}
}
#endif
// React Native visionOS uses static property to retrieve initialURL.
resolve(RCTNullIfNil(initialURL.absoluteString));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ Pod::Spec.new do |s|
}
s.framework = "UIKit"

s.framework = "UIKit"

s.dependency "React-Core/RCTLinkingHeaders", version
s.dependency "ReactCommon/turbomodule/core", version
s.dependency "React-jsi", version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ Pod::Spec.new do |s|
}
s.framework = ["UIKit", "QuartzCore"]

s.framework = ["UIKit", "QuartzCore"]

s.dependency "RCT-Folly", folly_version
s.dependency "RCTTypeSafety"
s.dependency "React-jsi"
Expand Down
109 changes: 105 additions & 4 deletions packages/react-native/Libraries/SwiftExtensions/RCTMainWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,121 @@ import SwiftUI
}
```
Note: If you want to create additional windows in your app, create a new `WindowGroup {}` and pass it a `RCTRootViewRepresentable`.
*/
Note: If you want to create additional windows in your app, use `RCTWindow()`.
*/
public struct RCTMainWindow: Scene {
var moduleName: String
var initialProps: RCTRootViewRepresentable.InitialPropsType
var onOpenURLCallback: ((URL) -> ())?
var devMenuSceneAnchor: UnitPoint?
var contentView: AnyView?

public init(moduleName: String, initialProps: RCTRootViewRepresentable.InitialPropsType = nil) {
var rootView: RCTRootViewRepresentable {
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps, devMenuSceneAnchor: devMenuSceneAnchor)
}

/// Creates new RCTMainWindowWindow.
///
/// - Parameters:
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
/// - initialProps: Initial properties for this view.
/// - devMenuPlacement: Placement of the additional controls for triggering reload command and dev menu trigger.
public init(
moduleName: String,
initialProps: RCTRootViewRepresentable.InitialPropsType = nil,
devMenuSceneAnchor: UnitPoint? = .bottom
) {
self.moduleName = moduleName
self.initialProps = initialProps
self.devMenuSceneAnchor = devMenuSceneAnchor
self.contentView = AnyView(rootView)
}

/// Creates new RCTMainWindowWindow.
///
/// - Parameters:
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
/// - initialProps: Initial properties for this view.
/// - devMenuPlacement: Placement of the additional controls for triggering reload command and dev menu trigger.
/// - contentView: Closure which accepts rootView, allows to apply additional modifiers to React Native rootView.
public init<Content: View>(
moduleName: String,
initialProps: RCTRootViewRepresentable.InitialPropsType = nil,
devMenuSceneAnchor: UnitPoint? = .bottom,
@ViewBuilder contentView: @escaping (_ view: RCTRootViewRepresentable) -> Content
) {
self.moduleName = moduleName
self.initialProps = initialProps
self.devMenuSceneAnchor = devMenuSceneAnchor
self.contentView = AnyView(contentView(rootView))
}

public var body: some Scene {
WindowGroup {
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
contentView
.modifier(WindowHandlingModifier())
.onOpenURL(perform: { url in
onOpenURLCallback?(url)
})
}
}
}

extension RCTMainWindow {
public func onOpenURL(perform action: @escaping (URL) -> ()) -> Self {
var scene = self
scene.onOpenURLCallback = action
return scene
}
}

/**
Handles data sharing between React Native and SwiftUI views.
*/
public struct WindowHandlingModifier: ViewModifier {
typealias UserInfoType = Dictionary<String, AnyHashable>

@Environment(\.reactContext) private var reactContext
@Environment(\.openWindow) private var openWindow
@Environment(\.dismissWindow) private var dismissWindow
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows

public init() {}

public func body(content: Content) -> some View {
// Attach listeners only if app supports multiple windows
if supportsMultipleWindows {
content
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenWindow"))) { data in
guard let id = data.userInfo?["id"] as? String else { return }
reactContext.scenes.updateValue(RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType), forKey: id)
openWindow(id: id)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTUpdateWindow"))) { data in
guard
let id = data.userInfo?["id"] as? String,
let userInfo = data.userInfo?["userInfo"] as? UserInfoType else { return }
reactContext.scenes[id]?.props = userInfo
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissWindow"))) { data in
guard let id = data.userInfo?["id"] as? String else { return }
dismissWindow(id: id)
reactContext.scenes.removeValue(forKey: id)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTOpenImmersiveSpace"))) { data in
guard let id = data.userInfo?["id"] as? String else { return }
reactContext.scenes.updateValue(
RCTSceneData(id: id, props: data.userInfo?["userInfo"] as? UserInfoType),
forKey: id
)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RCTDismissImmersiveSpace"))) { data in
guard let id = data.userInfo?["id"] as? String else { return }
reactContext.scenes.removeValue(forKey: id)
}
} else {
content
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import SwiftUI
import Observation

@Observable
public class RCTSceneData: Identifiable {
public var id: String
public var props: Dictionary<String, AnyHashable>?

init(id: String, props: Dictionary<String, AnyHashable>?) {
self.id = id
self.props = props
}
}

extension RCTSceneData: Equatable {
public static func == (lhs: RCTSceneData, rhs: RCTSceneData) -> Bool {
lhs.id == rhs.id && NSDictionary(dictionary: lhs.props ?? [:]).isEqual(to: rhs.props ?? [:])
}
}

@Observable
public class RCTReactContext {
public var scenes: Dictionary<String, RCTSceneData> = [:]

public func getSceneData(id: String) -> RCTSceneData? {
return scenes[id]
}
}

extension RCTReactContext: Equatable {
public static func == (lhs: RCTReactContext, rhs: RCTReactContext) -> Bool {
NSDictionary(dictionary: lhs.scenes).isEqual(to: rhs.scenes)
}
}

public extension EnvironmentValues {
var reactContext: RCTReactContext {
get { self[RCTSceneContextKey.self] }
set { self[RCTSceneContextKey.self] = newValue }
}
}

private struct RCTSceneContextKey: EnvironmentKey {
static var defaultValue: RCTReactContext = RCTReactContext()
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@
- (instancetype _Nonnull)initWithModuleName:(NSString *_Nonnull)moduleName
initProps:(NSDictionary *_Nullable)initProps;

-(void)updateProps:(NSDictionary *_Nullable)newProps;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ - (void)updateProps:(NSDictionary *)newProps {
return;
}



if (newProps != nil && ![rootView.appProperties isEqualToDictionary:newProps]) {
[rootView setAppProperties:newProps];
NSMutableDictionary *newProperties = [rootView.appProperties mutableCopy];
[newProperties setValuesForKeysWithDictionary:newProps];
[rootView setAppProperties:newProperties];
}
}
@end
Loading

0 comments on commit 6c570a7

Please sign in to comment.