Skip to content

feat: add support for ornaments & dev menu trigger #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import React

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

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

/// 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,
devMenuPlacement: ToolbarPlacement = .bottomOrnament
) {
self.moduleName = moduleName
self.initialProps = initialProps
self.devMenuPlacement = devMenuPlacement
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,
devMenuPlacement: ToolbarPlacement = .bottomOrnament,
@ViewBuilder contentView: @escaping (_ view: RCTRootViewRepresentable) -> Content
) {
self.moduleName = moduleName
self.initialProps = initialProps
self.devMenuPlacement = devMenuPlacement
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)
})
#if DEBUG
.toolbar {
DevMenuView(placement: .bottomOrnament)
}
#endif
}
}
}

extension RCTMainWindow {
public func onOpenURL(perform action: @escaping (URL) -> ()) -> some Scene {
public func onOpenURL(perform action: @escaping (URL) -> ()) -> Self {
var scene = self
scene.onOpenURLCallback = action
return scene
Expand Down Expand Up @@ -95,3 +138,30 @@ public struct WindowHandlingModifier: ViewModifier {
}
}
}

/**
Toolbar which displays additional controls to easily open dev menu and trigger reload command.
*/
struct DevMenuView: ToolbarContent {
let placement: ToolbarItemPlacement

var body: some ToolbarContent {
ToolbarItem(placement: placement) {
Button(action: {
RCTTriggerReloadCommandListeners("User Reload")
}, label: {
Image(systemName: "arrow.clockwise")
})
}
ToolbarItem(placement: placement) {
Button(action: {
NotificationCenter.default.post(
Notification(name: Notification.Name("RCTShowDevMenuNotification"), object: nil)
)
},
label: {
Image(systemName: "filemenu.and.selection")
})
}
}
}
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
68 changes: 60 additions & 8 deletions packages/react-native/Libraries/SwiftExtensions/RCTWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,16 @@ public struct RCTWindow : Scene {
var id: String
var sceneData: RCTSceneData?
var moduleName: String

public init(id: String, moduleName: String, sceneData: RCTSceneData?) {
self.id = id
self.moduleName = moduleName
self.sceneData = sceneData
var contentView: AnyView?

func getRootView(sceneData: RCTSceneData?) -> RCTRootViewRepresentable {
return RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData?.props ?? [:])
}

public var body: some Scene {
WindowGroup(id: id) {
Group {
if let sceneData {
RCTRootViewRepresentable(moduleName: moduleName, initialProps: sceneData.props)
}
contentView
}
.onAppear {
if sceneData == nil {
Expand All @@ -37,9 +34,64 @@ public struct RCTWindow : Scene {
}

extension RCTWindow {
/// Creates new RCTWindow.
///
/// - Parameters:
/// - id: Unique identifier of the window.
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
/// - sceneData: Data of the scene. Used to sync JS state between windows.
public init(id: String, moduleName: String, sceneData: RCTSceneData?) {
self.id = id
self.moduleName = moduleName
self.sceneData = sceneData
self.contentView = AnyView(getRootView(sceneData: sceneData))
}

/// Creates new RCTWindow with additional closure to allow applying modifiers to rootView.
///
/// - Parameters:
/// - id: Unique identifier of the window.
/// - moduleName: Name of the module registered using `AppRegistry.registerComponent()`
/// - sceneData: Data of the scene. Used to sync JS state between windows.
/// - contentView: Closure which accepts rootView, allows to apply additional modifiers to React Native rootView.
public init<Content: View>(
id: String,
moduleName: String,
sceneData: RCTSceneData?,
@ViewBuilder contentView: @escaping (_ view: RCTRootViewRepresentable) -> Content
) {
self.id = id
self.moduleName = moduleName
self.sceneData = sceneData
self.contentView = AnyView(contentView(getRootView(sceneData: sceneData)))
}

/// Creates new RCTWindow with additional closure to allow applying modifiers to rootView.
///
/// - Parameters:
/// - id: Unique identifier of the window. Same id will be used for moduleName.
/// - sceneData: Data of the scene. Used to sync JS state between windows.
/// - contentView: Closure which accepts rootView, allows to apply additional modifiers to React Native rootView.
public init<Content: View>(
id: String,
sceneData: RCTSceneData?,
@ViewBuilder contentView: @escaping (_ view: RCTRootViewRepresentable) -> Content
) {
self.id = id
self.moduleName = id
self.sceneData = sceneData
self.contentView = AnyView(contentView(getRootView(sceneData: sceneData)))
}

/// Creates new RCTWindow.
///
/// - Parameters:
/// - id: Unique identifier of the window. Same id will be used for moduleName.
/// - sceneData: Data of the scene. Used to sync JS state between windows.
public init(id: String, sceneData: RCTSceneData?) {
self.id = id
self.moduleName = id
self.sceneData = sceneData
self.contentView = AnyView(getRootView(sceneData: sceneData))
}
}
19 changes: 19 additions & 0 deletions packages/react-native/React/Base/RCTUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ BOOL RCTRunningInAppExtension(void)
if (scene.session.role == UISceneSessionRoleImmersiveSpaceApplication) {
continue;
}

#endif

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

#if TARGET_OS_VISION
// Ornaments are supported only on visionOS.
// When clicking on an ornament it becomes the keyWindow.
// Presenting a RN modal from ornament leads to a crash.
UIWindow* keyWindow = windowScene.keyWindow;
BOOL isOrnament = [keyWindow.debugDescription containsString:@"Ornament"];
if (isOrnament) {
for (UIWindow *window in windowScene.windows) {
BOOL isOrnament = [window.debugDescription containsString:@"Ornament"];
if (window != keyWindow && !isOrnament) {
return window;
}
}
}

return keyWindow;
#endif

if (@available(iOS 15.0, *)) {
return windowScene.keyWindow;
}
Expand Down
118 changes: 59 additions & 59 deletions packages/rn-tester/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1901,7 +1901,7 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
boost: b6392ab8d665ef3aa7069eea9e57f5224ec9970a
DoubleConversion: 26c660c8d88372cca1a67f8101d2d962a7064361
FBLazyVector: d434a232713b779f3fa592271f974d581a4e5efe
FBLazyVector: e34d006c28c01ab97ea89b92ee24164a32b333cd
fmt: 5d9ffa7ccba126c08b730252123601d514652320
glog: 63360cdb8e07e9542830fefdd73687e5af0db2ac
hermes-engine: c87fb20a7588a9a2e5112ca459a0faa58f90c0c6
Expand All @@ -1911,67 +1911,67 @@ SPEC CHECKSUMS:
OSSLibraryExample: d718b079a7ff6bb417fdbb6d98d58b9081b07b88
RCT-Folly: e75371281be586c821f9614489de0d370146e4a2
RCTDeprecation: 3808e36294137f9ee5668f4df2e73dc079cd1dcf
RCTRequired: bd3cb7ab8bb259206a338f85d80a0ce5a0c69342
RCTTypeSafety: 371a1430e65d2a81dd029fe05c88702b34301a0e
React: 7746e376e9f9672d4907b143447e5557f06f1f6d
React-callinvoker: 233f1d1af1e59f50f9d1b1087a102b5403c45e03
React-Core: 8742f86305a4015972884366e080fc335af7f6ea
React-CoreModules: f25f234a4a358089b467ae1949741a68cf711af9
React-cxxreact: c4b02045aef8489770f6ba18cce3705bea338221
React-debug: 705fdf75d6e8bdd7181fb98d2d9a2c1f82ab5b2e
React-defaultsnativemodule: 310ef2d184e528a8842ae6835d1c2f3d07767816
React-domnativemodule: 0aa77e0bf58663d580acb4275fa624c65591824c
React-Fabric: 964ca5b70ecea26c82afd34c89c6c55f196b3193
React-FabricComponents: 0a82f5869eaebb00ba1da8d26802ad248ae5d75c
React-FabricImage: a51adb7fb0fd90772a2a1c92ed6c7637cc647f57
React-featureflags: 39b6b9923874d625b1dcd5edddab40695f390103
React-featureflagsnativemodule: e0b8c2e66e78cda76d22c6da578099891e2dff1d
React-graphics: 2ba6c1d134da57b4d97ae41dab33b6790df13ccc
React-hermes: 178f48ef2a77ae54647abf1cbbd7fda34b906530
React-ImageManager: 6faccfcb1895e63a086e7384e3179ffe24003628
React-jserrorhandler: 7ac3f665b5905703336086b4f32e6396b46ddea1
React-jsi: 7da88a67aff82f7d76b90f5fe8a6bec852c73234
React-jsiexecutor: 91240cb6478d422592b4b8769502067042439966
React-jsinspector: 9064f47464de6cdca6ec57e546cd70eef6d9f366
React-jsitracing: 724dd967eba7dda025e8be9caaa4b1573502d956
React-logger: e6a399500b7bbf65184c59360b2c6ad2ee241c91
React-Mapbuffer: 0ac85f1cd636a9a8fbfb2b5ee5bf009a8f769156
React-microtasksnativemodule: cbf04e1a4df2fe67fd80432e19966bdef3af9f05
React-nativeconfig: afba2adbd92757704f1f7a3aaac4a00d0633635a
React-NativeModulesApple: 272e293844ad7dc3336c9fd0cf7cd17536f96d95
React-perflogger: 061cf6456acee26ac4f9bfecf748c60c2721ae45
React-performancetimeline: 9d5316dd384abc051be89d571475a7b42bd9a327
React-RCTActionSheet: ff66b5f91f5ae71c78fe8a64836cd4115d45791e
React-RCTAnimation: 0cf807e00f29aeef17c05bd0e9159f19c39ba758
React-RCTAppDelegate: 4be422a1d397a7a3bb90fd3c37e4a3b37f777d73
React-RCTBlob: ff9bee678dd6d2d72a71bb800e8545446250999d
React-RCTFabric: baae0e17553ff6316c2f83eff1cd2081ac5bfce8
React-RCTImage: 9527e3983434738416a0c70436de710f4c943bd7
React-RCTLinking: 6f473ca17b6d63bc7f62921d2be5f543c053e4af
React-RCTNetwork: ec67d871ec029149650c6c8b5c6422f2a74e355b
React-RCTPushNotification: 4eabcb8ed729200218042864499cb4fe96ad4b34
React-RCTSettings: cd8990b36bbfd6cadb269d222842213c42385667
React-RCTSwiftExtensions: 1db3a5e13c30b72a0c3026c1710747c0fcc40e5e
React-RCTTest: d510ef04bfea9b9754a8a9f1bbbd3369ce5a9ba8
React-RCTText: 1957c3fc0fbf58e87df985d8db86f3789f2f28b3
React-RCTVibration: bf8d05a9edda5e1db5a2211aaa41a4f86d7f57d8
React-RCTWindowManager: 7492cc3591dfce401fed7cc58ead6d1af5f71f69
React-RCTXR: 6501e95cf47fed880dd55ebffd399417f164b8c7
React-rendererconsistency: 79c0102d79bf5f81561ec18ef9b37550684cc01c
React-rendererdebug: d77ea3945a1218e2733e8d263c4abb798a7bac38
React-rncore: 6a527db66df7d7e1d8d60248558f24665f3a635f
React-RuntimeApple: 6e129838bcb4b878dd146f121f19d8b819a722d8
React-RuntimeCore: c87ff5797c22510f8b0cb6c23fc594017354d077
React-runtimeexecutor: 108116665f2662482edc93895f9608d04d5b3b05
React-RuntimeHermes: 31458c92a3ac3af0fe6b061aaab9cf5102ce502e
React-runtimescheduler: 5c8f98d2e1fa112304431f9f47e3ba269c67e49c
React-utils: e40c193816da18c2fa5b8673985422974ee02039
RCTRequired: 06090d9724622f58b28478cbb2e87862756df6b5
RCTTypeSafety: e6e32ba42a0bd3a0bd94971df9a244fe893e8903
React: cec6f46c46ba4b88128a89db149ecaa08e569716
React-callinvoker: 3990b1a68c657e45ce77d4d900bf8533af332e9e
React-Core: b6a78e75ad6bbf706937952b6e29061a038737a8
React-CoreModules: fd07d3592016c7ce1ad67e3b10109cca95fc2100
React-cxxreact: 691071426dc297b489d9cd11cae60a494ec8c074
React-debug: 3f8158c426c724b0ac3308766a0ecd913f49beee
React-defaultsnativemodule: 671d9be37ea7c0d0e500d8cb70ad1ca5d4869112
React-domnativemodule: 2dd51d24e3cf6c9af5de12dba8258ed498c7dda0
React-Fabric: 5dfa7df179c8a2e9734683826c4457a6312df211
React-FabricComponents: ae2fad2c444ca48efe45cd3915ec70028807803d
React-FabricImage: c9f019e603a116560723a2f196518ef1a6d03801
React-featureflags: 8954103335117a8dd7b5a18471808821480a7dfc
React-featureflagsnativemodule: 8f683d7f4f61002e514de69d5e61622bbc63538f
React-graphics: 8994b709c5fde91560aafcbac7a3c48ed5401e02
React-hermes: a2e77fbcd9f579388b1fc40eb7ddf98e439b6a14
React-ImageManager: 7f4e5710521768e7549d6539fdeea95ec08f3932
React-jserrorhandler: b9d21486b2b17501f2dd05b43e93ca28335c5727
React-jsi: 468dd641ca31f7c6cd6fe8ca97db1db6ae6f1f0f
React-jsiexecutor: fed9ae2a3f855dc419edc54c5d0c25fa9375af54
React-jsinspector: a4bc5b3664faccd9ba07829d06b8b1e400549c62
React-jsitracing: ddc7cbc91c4c2c5911330514fd0cb08a4d332cd6
React-logger: fa52be9ff4f8879d680ed32f228d2944eade37ff
React-Mapbuffer: 2d146d504f49bf056b78bdc9d2186f13bb9b317d
React-microtasksnativemodule: bd0d6b760670deace685e6894baf64692fcd5a64
React-nativeconfig: fed2d84d822b1cea543154e2f060e44d385ea585
React-NativeModulesApple: 736f1ec19de1f20e43da8e8f1c005a71e85ab740
React-perflogger: b6fb9386ca0d7ada973c2327866d45a201d69581
React-performancetimeline: e8467f2451217454d39b0ea06b22444ee3c95fd1
React-RCTActionSheet: bf125bb38e5d4389f91e540f96397b8e42b940fa
React-RCTAnimation: d8dbb59180fd9982299cbb0036dc3b1370ba0586
React-RCTAppDelegate: ebc474ce7a794b04bed7a7b0e2aae93db10aed09
React-RCTBlob: abf99efa4cb82cafd0f2761939c923cf4d5c5006
React-RCTFabric: 460c7d2ba391dfcf1a4daec2cf1c010f8fad1b63
React-RCTImage: bb1a37bb2934b658a1a42cce23b81839cecf9aa0
React-RCTLinking: ae979fe87f2cf8dd6c33f6d8b37946e839df1363
React-RCTNetwork: 670ab5244e7e98d1be4fec61d792b91b389d4630
React-RCTPushNotification: 6e8755b9ed818ebf0ccd09b9d80fc42cca9fbdcf
React-RCTSettings: e806a6681faca065d0d8d2faf91f0ab8b05a7543
React-RCTSwiftExtensions: a3cf8b5cf176b6f739df9de0dfb9e97c3003a9c1
React-RCTTest: 5a4aac44395525c6d17c088fbd24de9a88eea532
React-RCTText: 5b86e4f701ced8a6800be90110f56b000fa8b056
React-RCTVibration: 5b98c96b6df537608575d8a073492f002b7af92d
React-RCTWindowManager: b1749ada52a110b85807d97ba835466589773a56
React-RCTXR: 7d027d32850b44446a732501f707f33952871483
React-rendererconsistency: bc5ee25fa0f56310883263cc966c0d3d737a44a9
React-rendererdebug: 4a46b05ae5fedba2b8542b83ff8cd4d8efe66713
React-rncore: 82554771b1f7e5976cc9fcbf01a4f49ab9a288d6
React-RuntimeApple: c23dc3c0ccb7ae491c2e5128f9e480853bb28c01
React-RuntimeCore: 2e94339b8d2c4a5e56f04ae3fa1c6ae07022b075
React-runtimeexecutor: 2b4ed6ec4e40b990b83314c72a8be09a7043fe83
React-RuntimeHermes: d0b3c295f15c0f7dbfa9142a99f0dea7d6c6c063
React-runtimescheduler: bc86356bfdde63398cf78267848f82106d009978
React-utils: c1ac77965df60bf575b20b149439874efb1cc366
ReactCodegen: 2fa2bfb8df604e5bbf2462cf3cced313c3131b96
ReactCommon: 9ec392ae3a44c2bdaf77448dfbc883cad9487806
ReactCommon-Samples: 0f25a353dc295796cc6a7f1cf67c4ad92ca2c60f
ReactCommon: 6ff0c7ea0f195e0c5e3863382f6c21d74ad7704e
ReactCommon-Samples: 8a2a27ca2c12bd4d5790e7c3233f66bec354b8fc
ScreenshotManager: 662151998cf5859591e72c76ceb9ebc6d04b425b
SocketRocket: 0ba3e799f983d2dfa878777017659ef6c866e5c6
Yoga: 6bd003ca9113d936e38452fe76de858ec02cc6db
Yoga: 45ad594d98b47f5fafb1592f82c00479d3b15146

PODFILE CHECKSUM: ecf8d73b0aefca76e0e218d8845b105ea9282718

Expand Down
9 changes: 8 additions & 1 deletion packages/rn-tester/RNTester-visionOS/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ struct RNTesterApp: App {
RCTLinkingManager.onOpenURL(url: url)
})

RCTWindow(id: "SecondWindow", sceneData: reactContext.getSceneData(id: "SecondWindow"))
RCTWindow(id: "SecondWindow", sceneData: reactContext.getSceneData(id: "SecondWindow")) { rootView in
rootView.ornament(attachmentAnchor: .scene(.bottom)) {
VStack {
Button("Hey!") {}
}
.glassBackgroundEffect()
}
}
.defaultSize(CGSize(width: 400, height: 700))

ImmersiveSpace(id: "TestImmersiveSpace") {}
Expand Down
Loading