Skip to content

Commit cef3fa3

Browse files
committed
feat: add support for ornaments & dev menu trigger (#149)
* feat: add support for ornaments * feat: add ornaments support to second window
1 parent ca64282 commit cef3fa3

File tree

6 files changed

+167
-15
lines changed

6 files changed

+167
-15
lines changed

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

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

34
/**
45
This SwiftUI struct returns main React Native scene. It should be used only once as it conains setup code.
@@ -21,25 +22,67 @@ public struct RCTMainWindow: Scene {
2122
var moduleName: String
2223
var initialProps: RCTRootViewRepresentable.InitialPropsType
2324
var onOpenURLCallback: ((URL) -> ())?
25+
var devMenuPlacement: ToolbarPlacement = .bottomOrnament
26+
var contentView: AnyView?
2427

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

3068
public var body: some Scene {
3169
WindowGroup {
32-
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
70+
contentView
3371
.modifier(WindowHandlingModifier())
3472
.onOpenURL(perform: { url in
3573
onOpenURLCallback?(url)
3674
})
75+
#if DEBUG
76+
.toolbar {
77+
DevMenuView(placement: .bottomOrnament)
78+
}
79+
#endif
3780
}
3881
}
3982
}
4083

4184
extension RCTMainWindow {
42-
public func onOpenURL(perform action: @escaping (URL) -> ()) -> some Scene {
85+
public func onOpenURL(perform action: @escaping (URL) -> ()) -> Self {
4386
var scene = self
4487
scene.onOpenURLCallback = action
4588
return scene
@@ -95,3 +138,30 @@ public struct WindowHandlingModifier: ViewModifier {
95138
}
96139
}
97140
}
141+
142+
/**
143+
Toolbar which displays additional controls to easily open dev menu and trigger reload command.
144+
*/
145+
struct DevMenuView: ToolbarContent {
146+
let placement: ToolbarItemPlacement
147+
148+
var body: some ToolbarContent {
149+
ToolbarItem(placement: placement) {
150+
Button(action: {
151+
RCTTriggerReloadCommandListeners("User Reload")
152+
}, label: {
153+
Image(systemName: "arrow.clockwise")
154+
})
155+
}
156+
ToolbarItem(placement: placement) {
157+
Button(action: {
158+
NotificationCenter.default.post(
159+
Notification(name: Notification.Name("RCTShowDevMenuNotification"), object: nil)
160+
)
161+
},
162+
label: {
163+
Image(systemName: "filemenu.and.selection")
164+
})
165+
}
166+
}
167+
}

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

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

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,16 @@ public struct RCTWindow : Scene {
1313
var id: String
1414
var sceneData: RCTSceneData?
1515
var moduleName: String
16-
17-
public init(id: String, moduleName: String, sceneData: RCTSceneData?) {
18-
self.id = id
19-
self.moduleName = moduleName
20-
self.sceneData = sceneData
16+
var contentView: AnyView?
17+
18+
func getRootView(sceneData: RCTSceneData?) -> RCTRootViewRepresentable {
19+
return RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData?.props ?? [:])
2120
}
2221

2322
public var body: some Scene {
2423
WindowGroup(id: id) {
2524
Group {
26-
if let sceneData {
27-
RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData.props)
28-
}
25+
contentView
2926
}
3027
.onAppear {
3128
if sceneData == nil {
@@ -37,9 +34,64 @@ public struct RCTWindow : Scene {
3734
}
3835

3936
extension RCTWindow {
37+
/// Creates new RCTWindow.
38+
///
39+
/// - Parameters:
40+
/// - id: Unique identifier of the window.
41+
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
42+
/// - sceneData: Data of the scene. Used to sync JS state between windows.
43+
public init(id: String, moduleName: String, sceneData: RCTSceneData?) {
44+
self.id = id
45+
self.moduleName = moduleName
46+
self.sceneData = sceneData
47+
self.contentView = AnyView(getRootView(sceneData: sceneData))
48+
}
49+
50+
/// Creates new RCTWindow with additional closure to allow applying modifiers to rootView.
51+
///
52+
/// - Parameters:
53+
/// - id: Unique identifier of the window.
54+
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
55+
/// - sceneData: Data of the scene. Used to sync JS state between windows.
56+
/// - contentView: Closure which accepts rootView, allows to apply additional modifiers to React Native rootView.
57+
public init<Content: View>(
58+
id: String,
59+
moduleName: String,
60+
sceneData: RCTSceneData?,
61+
@ViewBuilder contentView: @escaping (_ view: RCTRootViewRepresentable) -> Content
62+
) {
63+
self.id = id
64+
self.moduleName = moduleName
65+
self.sceneData = sceneData
66+
self.contentView = AnyView(contentView(getRootView(sceneData: sceneData)))
67+
}
68+
69+
/// Creates new RCTWindow with additional closure to allow applying modifiers to rootView.
70+
///
71+
/// - Parameters:
72+
/// - id: Unique identifier of the window. Same id will be used for moduleName.
73+
/// - sceneData: Data of the scene. Used to sync JS state between windows.
74+
/// - contentView: Closure which accepts rootView, allows to apply additional modifiers to React Native rootView.
75+
public init<Content: View>(
76+
id: String,
77+
sceneData: RCTSceneData?,
78+
@ViewBuilder contentView: @escaping (_ view: RCTRootViewRepresentable) -> Content
79+
) {
80+
self.id = id
81+
self.moduleName = id
82+
self.sceneData = sceneData
83+
self.contentView = AnyView(contentView(getRootView(sceneData: sceneData)))
84+
}
85+
86+
/// Creates new RCTWindow.
87+
///
88+
/// - Parameters:
89+
/// - id: Unique identifier of the window. Same id will be used for moduleName.
90+
/// - sceneData: Data of the scene. Used to sync JS state between windows.
4091
public init(id: String, sceneData: RCTSceneData?) {
4192
self.id = id
4293
self.moduleName = id
4394
self.sceneData = sceneData
95+
self.contentView = AnyView(getRootView(sceneData: sceneData))
4496
}
4597
}

packages/react-native/React/Base/RCTUtils.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ BOOL RCTRunningInAppExtension(void)
592592
if (scene.session.role == UISceneSessionRoleImmersiveSpaceApplication) {
593593
continue;
594594
}
595+
595596
#endif
596597

597598
if (scene.activationState == UISceneActivationStateForegroundActive) {
@@ -608,6 +609,24 @@ BOOL RCTRunningInAppExtension(void)
608609
UIScene *sceneToUse = foregroundActiveScene ? foregroundActiveScene : foregroundInactiveScene;
609610
UIWindowScene *windowScene = (UIWindowScene *)sceneToUse;
610611

612+
#if TARGET_OS_VISION
613+
// Ornaments are supported only on visionOS.
614+
// When clicking on an ornament it becomes the keyWindow.
615+
// Presenting a RN modal from ornament leads to a crash.
616+
UIWindow* keyWindow = windowScene.keyWindow;
617+
BOOL isOrnament = [keyWindow.debugDescription containsString:@"Ornament"];
618+
if (isOrnament) {
619+
for (UIWindow *window in windowScene.windows) {
620+
BOOL isOrnament = [window.debugDescription containsString:@"Ornament"];
621+
if (window != keyWindow && !isOrnament) {
622+
return window;
623+
}
624+
}
625+
}
626+
627+
return keyWindow;
628+
#endif
629+
611630
if (@available(iOS 15.0, *)) {
612631
return windowScene.keyWindow;
613632
}

packages/rn-tester/RNTester-visionOS/App.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ struct RNTesterApp: App {
1515
RCTLinkingManager.onOpenURL(url: url)
1616
})
1717

18-
RCTWindow(id: "SecondWindow", sceneData: reactContext.getSceneData(id: "SecondWindow"))
18+
RCTWindow(id: "SecondWindow", sceneData: reactContext.getSceneData(id: "SecondWindow")) { rootView in
19+
rootView.ornament(attachmentAnchor: .scene(.bottom)) {
20+
VStack {
21+
Button("Hey!") {}
22+
}
23+
.glassBackgroundEffect()
24+
}
25+
}
1926
.defaultSize(CGSize(width: 400, height: 700))
2027

2128
ImmersiveSpace(id: "TestImmersiveSpace") {}

packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
359825B9A5AE4A3F4AA612DD /* Pods-RNTesterUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterUnitTests.debug.xcconfig"; path = "Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests.debug.xcconfig"; sourceTree = "<group>"; };
9999
383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_UIColorTests.m; sourceTree = "<group>"; };
100100
3D2AFAF41D646CF80089D1A3 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "[email protected]"; path = "RNTester/[email protected]"; sourceTree = "<group>"; };
101-
51202427770AB438DEA21CE7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = ../RNTester/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
101+
51202427770AB438DEA21CE7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../RNTester/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
102102
54DDA3DF154A732E76DCCEE8 /* Pods-RNTester-visionOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester-visionOS.release.xcconfig"; path = "Target Support Files/Pods-RNTester-visionOS/Pods-RNTester-visionOS.release.xcconfig"; sourceTree = "<group>"; };
103103
5C60EB1B226440DB0018C04F /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RNTester/AppDelegate.mm; sourceTree = "<group>"; };
104104
63C6B5E1C2465D85E9BDB6E5 /* libPods-RNTesterIntegrationTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTesterIntegrationTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -117,7 +117,7 @@
117117
9B8542B8C590B51BD0588751 /* Pods-RNTester.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.release.xcconfig"; path = "Target Support Files/Pods-RNTester/Pods-RNTester.release.xcconfig"; sourceTree = "<group>"; };
118118
A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkTaskTests.m; sourceTree = "<group>"; };
119119
AC474BFB29BBD4A1002BDAED /* RNTester.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = RNTester.xctestplan; path = RNTester/RNTester.xctestplan; sourceTree = "<group>"; };
120-
C1142D4D3F85531561B1F08E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = RNTester/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
120+
C1142D4D3F85531561B1F08E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = RNTester/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
121121
CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTEventEmitterTests.m; sourceTree = "<group>"; };
122122
D6942D0981036096211E5BDC /* libPods-RNTesterUnitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTesterUnitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
123123
E771AEEA22B44E3100EA1189 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RNTester/Info.plist; sourceTree = "<group>"; };

0 commit comments

Comments
 (0)