Skip to content

Commit

Permalink
feat: add support for ornaments & dev menu trigger (#149)
Browse files Browse the repository at this point in the history
* feat: add support for ornaments

* feat: add ornaments support to second window
  • Loading branch information
okwasniewski committed Jun 24, 2024
1 parent fe5238a commit c583333
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 13 deletions.
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
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

0 comments on commit c583333

Please sign in to comment.