diff --git a/README.md b/README.md index cd80cec0d3fe9a..c8fcd35c277ae2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ This is a prop on `` component allowing to add hover effect. It's applie If you want to customize it you can use the `visionos_hoverEffect` prop, like so: -```jsx +```tsx Click me @@ -77,6 +77,102 @@ If you want to customize it you can use the `visionos_hoverEffect` prop, like so The available options are: `lift` or `highlight`. +### `XR` API +Manage Immersive Experiences. + +#### Methods +**`requestSession`** +```js +requestSession: (sessionId?: string) => Promise +``` +Opens a new [`ImmersiveSpace`](https://developer.apple.com/documentation/swiftui/immersive-spaces) given it's unique `Id`. + +**`endSession`** +```js +endSession: () => Promise +``` +Closes currently open `ImmersiveSpace`. + +#### Constants +**`supportsMultipleScenes`** +```js +supportsMultipleScenes: boolean +``` +A Boolean value that indicates whether the app may display multiple scenes simultaneously. Returns the value of `UIApplicationSupportsMultipleScenes` key from `Info.plist`. + +### Example Usage + +1. Set `UIApplicationSupportsMultipleScenes` to `true` in `Info.plist`: +```diff + + + + + UIApplicationSceneManifest + + UIApplicationPreferredDefaultSceneSessionRole + UIWindowSceneSessionRoleApplication + UIApplicationSupportsMultipleScenes +- ++ + UISceneConfigurations + + + + + +``` + + +1. Inside `App.swift` add new `ImmersiveSpace`: +```diff +@main +struct HelloWorldApp: App { + @UIApplicationDelegateAdaptor var delegate: AppDelegate ++ @State private var immersionLevel: ImmersionStyle = .mixed + + var body: some Scene { + RCTMainWindow(moduleName: "HelloWorldApp") ++ ImmersiveSpace(id: "TestImmersiveSpace") { ++ // RealityKit content goes here ++ } ++ .immersionStyle(selection: $immersionLevel, in: .mixed, .progressive, .full) + } +} +``` +For more information about `ImmersiveSpace` API refer to [Apple documentation](https://developer.apple.com/documentation/swiftui/immersive-spaces). + +In the above example we set `ImmersiveSpace` id to `TestImmersiveSpace`. + +Now in our JS code, we can call: + +```js +import {XR} from "@callstack/react-native-visionos" +//... +const openXRSession = async () => { + try { + if (!XR.supportsMultipleScenes) { + Alert.alert('Error', 'Multiple scenes are not supported'); + return; + } + await XR.requestSession('TestImmersiveSpace'); // Pass the same identifier from `App.swift` + } catch (e) { + Alert.alert('Error', e.message); + } +}; + +const closeXRSession = async () => { + await XR.endSession(); +}; +``` +> [!CAUTION] +> Opening an `ImmersiveSpace` can fail in this secarios: +> - `ImmersiveSpace` is not declared. +> - `UIApplicationSupportsMultipleScenes` is set to `false`. +> - User cancels the request. + +For a full example usage, refer to [`XRExample.js`](https://github.com/callstack/react-native-visionos/blob/main/packages/rn-tester/js/examples/XR/XRExample.js). + ## Contributing 1. Follow the same steps as in the `New project creation` section. diff --git a/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec b/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec index d5dbdafe37ff6d..531f1d8172ca7d 100644 --- a/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec +++ b/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec @@ -24,4 +24,5 @@ Pod::Spec.new do |s| s.frameworks = ["UIKit", "SwiftUI"] s.dependency "React-Core" + s.dependency "React-RCTXR" end diff --git a/packages/react-native/Libraries/XR/ImmersiveBridge.swift b/packages/react-native/Libraries/XR/ImmersiveBridge.swift new file mode 100644 index 00000000000000..45fdbcf4087b00 --- /dev/null +++ b/packages/react-native/Libraries/XR/ImmersiveBridge.swift @@ -0,0 +1,55 @@ +import Foundation +import SwiftUI + +@objc public enum ImmersiveSpaceResult: Int { + case opened + case userCancelled + case error +} + +public typealias CompletionHandlerType = (_ result: ImmersiveSpaceResult) -> Void + +/** + * Utility view used to bridge the gap between SwiftUI environment and UIKit. + * + * Calls `openImmersiveSpace` when view appears in the UIKit hierarchy and `dismissImmersiveSpace` when removed. + */ +struct ImmersiveBridgeView: View { + @Environment(\.openImmersiveSpace) private var openImmersiveSpace + @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace + + var spaceId: String + var completionHandler: CompletionHandlerType + + var body: some View { + EmptyView() + .onAppear { + Task { + let result = await openImmersiveSpace(id: spaceId) + + switch result { + case .opened: + completionHandler(.opened) + case .error: + completionHandler(.error) + case .userCancelled: + completionHandler(.userCancelled) + default: + break + } + } + } + .onDisappear { + Task { await dismissImmersiveSpace() } + } + } +} + +@objc public class ImmersiveBridgeFactory: NSObject { + @objc public static func makeImmersiveBridgeView( + spaceId: String, + completionHandler: @escaping CompletionHandlerType + ) -> UIViewController { + return UIHostingController(rootView: ImmersiveBridgeView(spaceId: spaceId, completionHandler: completionHandler)) + } +} diff --git a/packages/react-native/Libraries/XR/NativeXRModule.js b/packages/react-native/Libraries/XR/NativeXRModule.js new file mode 100644 index 00000000000000..c34b296bc3e4a5 --- /dev/null +++ b/packages/react-native/Libraries/XR/NativeXRModule.js @@ -0,0 +1,8 @@ +/** + * @flow strict + * @format + */ + +export * from '../../src/private/specs/visionos_modules/NativeXRModule'; +import NativeXRModule from '../../src/private/specs/visionos_modules/NativeXRModule'; +export default NativeXRModule; diff --git a/packages/react-native/Libraries/XR/RCTXRModule.h b/packages/react-native/Libraries/XR/RCTXRModule.h new file mode 100644 index 00000000000000..450d8ad4681619 --- /dev/null +++ b/packages/react-native/Libraries/XR/RCTXRModule.h @@ -0,0 +1,6 @@ +#import +#import + +@interface RCTXRModule : NSObject + +@end diff --git a/packages/react-native/Libraries/XR/RCTXRModule.mm b/packages/react-native/Libraries/XR/RCTXRModule.mm new file mode 100644 index 00000000000000..8b5db6c7cbd008 --- /dev/null +++ b/packages/react-native/Libraries/XR/RCTXRModule.mm @@ -0,0 +1,88 @@ +#import + +#import + +#import +#import +#import +#import "RCTXR-Swift.h" + +@interface RCTXRModule () +@end + +@implementation RCTXRModule { + UIViewController *_immersiveBridgeView; +} + +RCT_EXPORT_MODULE() + +RCT_EXPORT_METHOD(endSession + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) +{ + [self removeImmersiveBridge]; + resolve(nil); +} + + +RCT_EXPORT_METHOD(requestSession + : (NSString *)sessionId resolve + : (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +{ + RCTExecuteOnMainQueue(^{ + UIWindow *keyWindow = RCTKeyWindow(); + UIViewController *rootViewController = keyWindow.rootViewController; + + if (self->_immersiveBridgeView == nil) { + self->_immersiveBridgeView = [ImmersiveBridgeFactory makeImmersiveBridgeViewWithSpaceId:sessionId + completionHandler:^(enum ImmersiveSpaceResult result){ + if (result == ImmersiveSpaceResultError) { + reject(@"ERROR", @"Immersive Space failed to open, the system cannot fulfill the request.", nil); + [self removeImmersiveBridge]; + } else if (result == ImmersiveSpaceResultUserCancelled) { + reject(@"ERROR", @"Immersive Space canceled by user", nil); + [self removeImmersiveBridge]; + } else if (result == ImmersiveSpaceResultOpened) { + resolve(nil); + } + }]; + + [rootViewController.view addSubview:self->_immersiveBridgeView.view]; + [rootViewController addChildViewController:self->_immersiveBridgeView]; + [self->_immersiveBridgeView didMoveToParentViewController:rootViewController]; + } else { + reject(@"ERROR", @"Immersive Space already opened", nil); + } + }); +} + +- (facebook::react::ModuleConstants)constantsToExport { + return [self getConstants]; +} + +- (facebook::react::ModuleConstants)getConstants { + __block facebook::react::ModuleConstants constants; + RCTUnsafeExecuteOnMainQueueSync(^{ + constants = facebook::react::typedConstants({ + .supportsMultipleScenes = RCTSharedApplication().supportsMultipleScenes + }); + }); + + return constants; +} + +- (void) removeImmersiveBridge +{ + RCTExecuteOnMainQueue(^{ + [self->_immersiveBridgeView willMoveToParentViewController:nil]; + [self->_immersiveBridgeView.view removeFromSuperview]; + [self->_immersiveBridgeView removeFromParentViewController]; + self->_immersiveBridgeView = nil; + }); +} + +- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +@end diff --git a/packages/react-native/Libraries/XR/React-RCTXR.podspec b/packages/react-native/Libraries/XR/React-RCTXR.podspec new file mode 100644 index 00000000000000..1073574eb266a3 --- /dev/null +++ b/packages/react-native/Libraries/XR/React-RCTXR.podspec @@ -0,0 +1,51 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +folly_config = get_folly_config() +folly_compiler_flags = folly_config[:compiler_flags] +folly_version = folly_config[:version] + +header_search_paths = [ + "\"$(PODS_ROOT)/RCT-Folly\"", + "\"${PODS_ROOT}/Headers/Public/React-Codegen/react/renderer/components\"", +] + +Pod::Spec.new do |s| + s.name = "React-RCTXR" + s.version = version + s.summary = "XR module for React Native." + s.homepage = "https://reactnative.dev/" + s.documentation_url = "https://reactnative.dev/docs/settings" + s.license = package["license"] + s.author = "Callstack" + s.platforms = min_supported_versions + s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' + s.source = source + s.source_files = "*.{m,mm,swift}" + s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" + s.header_dir = "RCTXR" + s.pod_target_xcconfig = { + "USE_HEADERMAP" => "YES", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "HEADER_SEARCH_PATHS" => header_search_paths.join(' ') + } + + s.dependency "RCT-Folly", folly_version + s.dependency "RCTTypeSafety" + s.dependency "React-jsi" + s.dependency "React-Core/RCTXRHeaders" + + add_dependency(s, "React-Codegen", :additional_framework_paths => ["build/generated/ios"]) + add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"]) + add_dependency(s, "React-NativeModulesApple", :additional_framework_paths => ["build/generated/ios"]) +end diff --git a/packages/react-native/Libraries/XR/XR.d.ts b/packages/react-native/Libraries/XR/XR.d.ts new file mode 100644 index 00000000000000..11d397a4ebde34 --- /dev/null +++ b/packages/react-native/Libraries/XR/XR.d.ts @@ -0,0 +1,9 @@ + +export interface XRStatic { + requestSession(sessionId: string): Promise; + endSession(): Promise; + supportsMultipleScenes: boolean; +} + +export const XR: XRStatic; +export type XR = XRStatic; diff --git a/packages/react-native/Libraries/XR/XR.js b/packages/react-native/Libraries/XR/XR.js new file mode 100644 index 00000000000000..3487429aff1bb5 --- /dev/null +++ b/packages/react-native/Libraries/XR/XR.js @@ -0,0 +1,33 @@ +/** + * @format + * @flow strict + * @jsdoc + */ + +import NativeXRModule from './NativeXRModule'; + +const XR = { + requestSession: (sessionId?: string): Promise => { + if (NativeXRModule != null && NativeXRModule.requestSession != null) { + return NativeXRModule.requestSession(sessionId); + } + return Promise.reject(new Error('NativeXRModule is not available')); + }, + endSession: (): Promise => { + if (NativeXRModule != null && NativeXRModule.endSession != null) { + return NativeXRModule.endSession(); + } + return Promise.reject(new Error('NativeXRModule is not available')); + }, + // $FlowIgnore[unsafe-getters-setters] + get supportsMultipleScenes(): boolean { + if (NativeXRModule == null) { + return false; + } + + const nativeConstants = NativeXRModule.getConstants(); + return nativeConstants.supportsMultipleScenes || false; + }, +}; + +module.exports = XR; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 95a9042b77a7c4..3e4ee57b750939 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -9151,6 +9151,22 @@ exports[`public API should not change unintentionally Libraries/WebSocket/WebSoc exports[`public API should not change unintentionally Libraries/WebSocket/WebSocketInterceptor.js 1`] = `"UNTYPED MODULE"`; +exports[`public API should not change unintentionally Libraries/XR/NativeXRModule.js 1`] = ` +"export * from \\"../../src/private/specs/visionos_modules/NativeXRModule\\"; +declare export default typeof NativeXRModule; +" +`; + +exports[`public API should not change unintentionally Libraries/XR/XR.js 1`] = ` +"declare const XR: { + requestSession: (sessionId?: string) => Promise, + endSession: () => Promise, + get supportsMultipleScenes(): boolean, +}; +declare module.exports: XR; +" +`; + exports[`public API should not change unintentionally Libraries/YellowBox/YellowBoxDeprecated.js 1`] = ` "declare const React: $FlowFixMe; type Props = $ReadOnly<{||}>; @@ -9268,6 +9284,7 @@ declare module.exports: { get PushNotificationIOS(): PushNotificationIOS, get Settings(): Settings, get Share(): Share, + get XR(): XR, get StyleSheet(): StyleSheet, get Systrace(): Systrace, get ToastAndroid(): ToastAndroid, diff --git a/packages/react-native/React-Core.podspec b/packages/react-native/React-Core.podspec index fb37075d37457d..3097ad4b3b3edb 100644 --- a/packages/react-native/React-Core.podspec +++ b/packages/react-native/React-Core.podspec @@ -37,6 +37,7 @@ header_subspecs = { 'RCTSettingsHeaders' => 'Libraries/Settings/*.h', 'RCTTextHeaders' => 'Libraries/Text/**/*.h', 'RCTVibrationHeaders' => 'Libraries/Vibration/*.h', + 'RCTXRHeaders' => 'Libraries/XR/*.h', } frameworks_search_paths = [] diff --git a/packages/react-native/React.podspec b/packages/react-native/React.podspec index 59b9f8319ae306..2d085e63040906 100644 --- a/packages/react-native/React.podspec +++ b/packages/react-native/React.podspec @@ -53,4 +53,5 @@ Pod::Spec.new do |s| s.dependency "React-RCTSettings", version s.dependency "React-RCTText", version s.dependency "React-RCTVibration", version + s.dependency "React-RCTXR", version end diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index aaaf3deb74baf2..7222a51f57a1ec 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -571,6 +571,14 @@ BOOL RCTRunningInAppExtension(void) ![scene isKindOfClass:[UIWindowScene class]]) { continue; } + +#if TARGET_OS_VISION + /// Presenting scenes over Immersive Spaces leads to crash: "Presentations are not permitted within volumetric window scenes." + if (scene.session.role == UISceneSessionRoleImmersiveSpaceApplication) { + continue; + } +#endif + UIWindowScene *windowScene = (UIWindowScene *)scene; for (UIWindow *window in windowScene.windows) { diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 7cd220bc0755a0..78ac79b6c913ac 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -89,6 +89,7 @@ import typeof Platform from './Libraries/Utilities/Platform'; import typeof useColorScheme from './Libraries/Utilities/useColorScheme'; import typeof useWindowDimensions from './Libraries/Utilities/useWindowDimensions'; import typeof Vibration from './Libraries/Vibration/Vibration'; +import typeof XR from './Libraries/XR/XR'; import typeof YellowBox from './Libraries/YellowBox/YellowBoxDeprecated'; const warnOnce = require('./Libraries/Utilities/warnOnce'); @@ -295,6 +296,9 @@ module.exports = { get Share(): Share { return require('./Libraries/Share/Share'); }, + get XR(): XR { + return require('./Libraries/XR/XR'); + }, get StyleSheet(): StyleSheet { return require('./Libraries/StyleSheet/StyleSheet'); }, diff --git a/packages/react-native/package.json b/packages/react-native/package.json index ee02f9e13fff8b..94d76fb5dfb533 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -143,6 +143,13 @@ "android": {}, "jsSrcsDir": "src" }, + { + "name": "FBReactNativeSpec_visionOS", + "type": "modules", + "ios": {}, + "android": {}, + "jsSrcsDir": "src/private/specs/visionos_modules" + }, { "name": "rncore", "type": "components", diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 6df005cee4fc1e..e82b38ed772cb2 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -601,6 +601,7 @@ def self.react_native_pods "glog", "hermes-engine", "React-hermes", + "React-RCTXR", # visionOS ] end diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 92f591e9d205c8..a5233066e55eff 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -133,6 +133,7 @@ def use_react_native! ( pod 'React-nativeconfig', :path => "#{prefix}/ReactCommon" pod 'RCTDeprecation', :path => "#{prefix}/ReactApple/Libraries/RCTFoundation/RCTDeprecation" pod 'React-RCTSwiftExtensions', :path => "#{prefix}/Libraries/SwiftExtensions" + pod 'React-RCTXR', :path => "#{prefix}/Libraries/XR" if hermes_enabled setup_hermes!(:react_native_path => prefix) diff --git a/packages/react-native/src/private/specs/visionos_modules/NativeXRModule.js b/packages/react-native/src/private/specs/visionos_modules/NativeXRModule.js new file mode 100644 index 00000000000000..ce8d22dca68c49 --- /dev/null +++ b/packages/react-native/src/private/specs/visionos_modules/NativeXRModule.js @@ -0,0 +1,21 @@ +/** + * @flow strict + * @format + */ + +import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; + +export type XRModuleConstants = {| + +supportsMultipleScenes?: boolean, +|}; + +export interface Spec extends TurboModule { + +getConstants: () => XRModuleConstants; + + +requestSession: (sessionId?: string) => Promise; + +endSession: () => Promise; +} + +export default (TurboModuleRegistry.get('XRModule'): ?Spec); diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index 946b0f8a795e4e..676ad01127ab66 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -89,6 +89,7 @@ PODS: - React-RCTSettings (= 1000.0.0) - React-RCTText (= 1000.0.0) - React-RCTVibration (= 1000.0.0) + - React-RCTXR (= 1000.0.0) - React-callinvoker (1000.0.0) - React-Codegen (1000.0.0): - DoubleConversion @@ -148,6 +149,7 @@ PODS: - React-hermes - React-jsi - React-jsiexecutor + - React-jsinspector (= 1000.0.0) - React-perflogger - React-runtimescheduler - React-utils @@ -335,6 +337,21 @@ PODS: - React-utils - SocketRocket (= 0.7.0.1) - Yoga + - React-Core/RCTXRHeaders (1000.0.0): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0.1) + - Yoga - React-CoreModules (1000.0.0): - RCT-Folly (= 2024.01.01.00) - RCTTypeSafety (= 1000.0.0) @@ -947,6 +964,7 @@ PODS: - React-jsi (= 1000.0.0) - React-perflogger (= 1000.0.0) - React-jsinspector (1000.0.0): + - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) - React-nativeconfig @@ -1071,6 +1089,7 @@ PODS: - ReactCommon - React-RCTSwiftExtensions (1000.0.0): - React-Core + - React-RCTXR - React-RCTTest (1000.0.0): - RCT-Folly (= 2024.01.01.00) - React-Core (= 1000.0.0) @@ -1087,6 +1106,14 @@ PODS: - React-jsi - React-NativeModulesApple - ReactCommon + - React-RCTXR (1000.0.0): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTXRHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon - React-rendererdebug (1000.0.0): - DoubleConversion - fmt (= 9.1.0) @@ -1103,6 +1130,7 @@ PODS: - React-jserrorhandler - React-jsi - React-jsiexecutor + - React-jsinspector - React-Mapbuffer - React-NativeModulesApple - React-RCTFabric @@ -1118,6 +1146,7 @@ PODS: - React-jserrorhandler - React-jsi - React-jsiexecutor + - React-jsinspector - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -1192,6 +1221,7 @@ PODS: - RCT-Folly (= 2024.01.01.00) - React-callinvoker (= 1000.0.0) - React-cxxreact (= 1000.0.0) + - React-debug (= 1000.0.0) - React-jsi (= 1000.0.0) - React-logger (= 1000.0.0) - React-perflogger (= 1000.0.0) @@ -1270,6 +1300,7 @@ DEPENDENCIES: - React-RCTTest (from `./RCTTest`) - React-RCTText (from `../react-native/Libraries/Text`) - React-RCTVibration (from `../react-native/Libraries/Vibration`) + - React-RCTXR (from `../react-native/Libraries/XR`) - React-rendererdebug (from `../react-native/ReactCommon/react/renderer/debug`) - React-rncore (from `../react-native/ReactCommon`) - React-RuntimeApple (from `../react-native/ReactCommon/react/runtime/platform/ios`) @@ -1385,6 +1416,8 @@ EXTERNAL SOURCES: :path: "../react-native/Libraries/Text" React-RCTVibration: :path: "../react-native/Libraries/Vibration" + React-RCTXR: + :path: "../react-native/Libraries/XR" React-rendererdebug: :path: "../react-native/ReactCommon/react/renderer/debug" React-rncore: @@ -1419,8 +1452,8 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: boost: 8f1e9b214fa11f71081fc8ecd5fad3daf221cf7f - DoubleConversion: 71bf0761505a44e4dfddc0aa04afa049fdfb63b5 - FBLazyVector: e34ddc4948ab5a9f857262cf68a46d39d8c61ed1 + DoubleConversion: 26c660c8d88372cca1a67f8101d2d962a7064361 + FBLazyVector: 6d72bc9033085488dab3c20cee3dab2b6a771dda fmt: 5d9ffa7ccba126c08b730252123601d514652320 glog: 4f05d17aa39a829fee878689fc9a41af587fabba hermes-engine: 3fed7e58e811ae8f795063cc6450714395c0276d @@ -1429,57 +1462,58 @@ SPEC CHECKSUMS: OCMock: 267d92c078398b7ce11d99e811e3a402744c06bc RCT-Folly: 70c792c856324d6a518af75b3a307c14c226343a RCTDeprecation: 3808e36294137f9ee5668f4df2e73dc079cd1dcf - RCTRequired: 60c97b0b0cc33a209d33b3412eb1a218cbc1b82c - RCTTypeSafety: 2f597b431b910be1e49c6e6f40dd6879f6d1bdfd - React: dc827cd6966c06176ea9664cb4d74d63d1a4fcca - React-callinvoker: 3727946c98d5626aef39ac6a45b9d1be5db35527 + RCTRequired: 6426efd0d6123b35fdac9ad5796717cbcef85ff9 + RCTTypeSafety: 7c822d061f3b0efcfb058c61cda509369bcb8602 + React: 2f26ffcc453dbb43a07e7396d3cc568cf328f830 + React-callinvoker: 1a7c0d6de53398862ddf642677df14231f6697d9 React-Codegen: 04a9ac6fd2f392534ed432102c6cbf117dcbf62b - React-Core: eb5002d75c263aaa042b97853effd3b6e2f8668c - React-CoreModules: 1eb22960e980f9e0b80542da464c5935260fd180 - React-cxxreact: eb5eb55bffe0ccff4cbf192a04c978e37f137e8f - React-debug: 1d5a869d405ca2410bfc95c7fe340220331696ef - React-Fabric: 555fc115322e9b37b8cc1d4ac0ab8d4a87718077 - React-FabricImage: 657d05951f1a512496713d7c2d534d347915fe3b - React-graphics: 989577b97b4d5e8f3cd3ff86b275eb7e4c7ff657 - React-hermes: 0003c9f0f623ed87224f4415c567f90f0d15737f - React-ImageManager: ec80ccfd373893be8c347a23a693438b41a86e2a - React-jserrorhandler: c4126c1c2fb42540cf64fdc80917214a0c6ca905 - React-jsi: 81558ed18c4e91824280a3298d75d598ef4cf3fa - React-jsiexecutor: 139ad70390e61c6b00779d82c7d5f490cd3eb632 - React-jsinspector: bd27899ffc61660559bfbc8872e8d8de05b2612d - React-jsitracing: 9639d0659449fa9a71f31e28d7dde349bcec3394 - React-logger: 142bc8479d40940294428f652fb016b809335121 - React-Mapbuffer: 2762ea21725665466ea54f09848886f5ba7e97f7 - React-nativeconfig: f984aee5151a69d520ea830756b33c62acc5c269 - React-NativeModulesApple: 5fd38cf3bd4aca4adc9522a7a53681001f98f29d - React-perflogger: b19adeb816c255a04dff9528cf3f6bf209188d17 - React-RCTActionSheet: c07c3924b6a94602f40effc130b776faa8b6bab1 - React-RCTAnimation: 51f5a6be1479336b26d0cb282bd0e8f1feabf593 - React-RCTAppDelegate: 3cac28ed63e10086624a2d965ce2c3359bbe04b0 - React-RCTBlob: 0d6f17d4cacc18b6872b06b232186b176d4a8a18 - React-RCTFabric: 8b955856519621a30a8425ac6c594abb66788839 - React-RCTImage: c69ac72c257475c7860602ac11beaabd7862b99c - React-RCTLinking: c3f99ac575655569f5bc108ff13b816dcdf1dbfd - React-RCTNetwork: 39405b5687d5b16fdcc6466713930e71f8389cde - React-RCTPushNotification: 8b6b936f8e07096db581a446d4f9844ff8b35925 - React-RCTSettings: 96dad99a283d67c573b8ba3709725901e8ac3108 - React-RCTSwiftExtensions: b372ca0daf79796645c691841b470ad5d37bba64 - React-RCTTest: 11be8ba66b91bc914d31f2b7c9abd0c39d47a3d7 - React-RCTText: 8ca2156c782dc33ceb3fa80ce8c20cfcccbcc9d1 - React-RCTVibration: 284a9575d09b5c5517d5d5b8b86d4e2d3c324b5e - React-rendererdebug: d664023e12af482bb3911ce547c522ddbbf01ac5 - React-rncore: 5917d667ddd77e1938a9d1dabf5c503e427f9ec9 - React-RuntimeApple: 31c6a97b53e53ea3e0d071ca45471b9152d22dc3 - React-RuntimeCore: 25febc097f3f7d4043562fcf5c7bf406758ad1ce - React-runtimeexecutor: b66459278d0c6564b5d877107e0bb3ac953a8221 - React-RuntimeHermes: 3f46517238fed32a04b36c6bc41a4ac8508baf6e - React-runtimescheduler: 04123aa94f0fa33398b9e58cd4345cac230eaee7 - React-utils: bdfeb70a54b7b73533de6c8b679143f75c34c86a - ReactCommon: 8f6a345ebd9558892a83e1be6b83bdad4fd99078 - ReactCommon-Samples: 090dcc2659d35c5577955d89efac618595a72e61 + React-Core: 9397cdd14ae7018850c16dea3b31ac72c4ff0324 + React-CoreModules: 53a00162be44e3353f2a9efad243d3dafe258cdb + React-cxxreact: 46f736a9cace851d42a3704c0da637d8566fd7d6 + React-debug: b1f637660400365b9b6908bd521ffce20c713dc5 + React-Fabric: 34c01267cb5ace971a7ae1a9c720ce5b30e92465 + React-FabricImage: 2e076721abe31734ce2a01cf82e254801ce5503f + React-graphics: 5ca218f8180dda07f997fa5ad145b7d5d7f35bd9 + React-hermes: d5707721feadf6c74d53fbc7a6212a98a140954c + React-ImageManager: deb13029061d5b0b08b21b8ea5883c15b7f4ba93 + React-jserrorhandler: f719009db2ae821f7df30866fa7e16676a14e85d + React-jsi: 9bf49563b30132199feb9b7721289ebbe61258f5 + React-jsiexecutor: 81e5a31965bb0e29d66df963ec9bf13496e84482 + React-jsinspector: 3b3514d648791386506f2f4eb7f1791e8b44f663 + React-jsitracing: 84ec35538d5d28a74431e58fc245761327dcb537 + React-logger: a41fc9cccff7de126c97e366c8909304531813ec + React-Mapbuffer: 3f22d38e3c903fa729061b0e8b976d102c377306 + React-nativeconfig: 0d00e737812420344137c8b7010033675d527982 + React-NativeModulesApple: 803f8508c47c2963cf893d74904ccc605fae4696 + React-perflogger: 0d57e63db7d181c2c3b9d13305b89b7783615739 + React-RCTActionSheet: aa4ae1a2a14153412e9237b10234a40e30d2b410 + React-RCTAnimation: 7eea83acd5da45de3d2e0c3aaa8df9d2957783bf + React-RCTAppDelegate: d48ddb6a45470b5d5d75d35927c2342698c47e56 + React-RCTBlob: 038dce0c29488cfbc19daa6d60b1cfcb01e9172d + React-RCTFabric: 131c60dc266f637d45c75c02b81d337c4e3179f9 + React-RCTImage: af4bbba6368f6b219915ca569ae5cf67fc8585f8 + React-RCTLinking: ce884034ffbece4c6c2ae8e1e8b4b88fb0ee406b + React-RCTNetwork: 488fca1b0e9f4f9802696372216029f5d1da6bc6 + React-RCTPushNotification: 2bb0038afd0c02f65d7b74b93bddcb2639cc6edf + React-RCTSettings: 0ff75db9df85764bd838b1ea9411fde23247f229 + React-RCTSwiftExtensions: 0751aeb108e9c7ae39b26710b7fb47d79fe0bfb9 + React-RCTTest: 3144ece03e48904e7796c2a96bd931649027516d + React-RCTText: abc6d97f7e36f9aed66678fc712f703d5bce2dfc + React-RCTVibration: 2b3b6cda5f979b0d16db391e737651d0dd4147a6 + React-RCTXR: 07a185fd7221532ccc6a5a9f237102256f823e60 + React-rendererdebug: 48c745f05ebf3ae447ea521c2b77bb7ee3923fd6 + React-rncore: 84944ba68705b5de9959d2206cfe7dd3181e83f2 + React-RuntimeApple: 33e35a0c93367647120758a61c71e21c73ce54ee + React-RuntimeCore: 7a4d19efece49c4087fd7bb30a643397d9ba1ab1 + React-runtimeexecutor: fb3425d082ad47c4bc7153419b3eac899d318603 + React-RuntimeHermes: 556f92a11c0cf9b4d4e4d7240dca228bdebb4622 + React-runtimescheduler: c101938ad407340555eb5d56557b3409785a7396 + React-utils: c3b2a4535dfad1e3c68d371b3946a33db4ceac4a + ReactCommon: 93b947fa9f432e09577a5d8c3f09c0ad7a6a4fc5 + ReactCommon-Samples: 6a59337d27861b986269a95c52aa5353b87da4ba ScreenshotManager: a5f596498de38f3cf3377df636ca67355503f190 SocketRocket: 0ba3e799f983d2dfa878777017659ef6c866e5c6 - Yoga: ac56a983bdf421a579c197143f79aa568f1c74a1 + Yoga: 50572c6c7c8227a84feb5746efa5f841f75f6f1b PODFILE CHECKSUM: 7e999b8158f1055609ef4491bc35f1ad658fdd6c diff --git a/packages/rn-tester/RNTester-visionOS/App.swift b/packages/rn-tester/RNTester-visionOS/App.swift index 39224b803cce1b..8e28482981e8a6 100644 --- a/packages/rn-tester/RNTester-visionOS/App.swift +++ b/packages/rn-tester/RNTester-visionOS/App.swift @@ -5,8 +5,11 @@ import React_RCTSwiftExtensions @main struct RNTesterApp: App { @UIApplicationDelegateAdaptor var delegate: AppDelegate + @State private var immersionLevel: ImmersionStyle = .full var body: some Scene { RCTMainWindow(moduleName: "RNTesterApp") + ImmersiveSpace(id: "TestImmersiveSpace") {} + .immersionStyle(selection: $immersionLevel, in: .mixed, .progressive, .full) } } diff --git a/packages/rn-tester/RNTester-visionOS/Info.plist b/packages/rn-tester/RNTester-visionOS/Info.plist index 940d4ef45518db..20f75e2afa73d1 100644 --- a/packages/rn-tester/RNTester-visionOS/Info.plist +++ b/packages/rn-tester/RNTester-visionOS/Info.plist @@ -7,7 +7,7 @@ UIApplicationPreferredDefaultSceneSessionRole UIWindowSceneSessionRoleApplication UIApplicationSupportsMultipleScenes - + UISceneConfigurations diff --git a/packages/rn-tester/js/examples/XR/XRExample.js b/packages/rn-tester/js/examples/XR/XRExample.js new file mode 100644 index 00000000000000..b9bd48e3f5c2c3 --- /dev/null +++ b/packages/rn-tester/js/examples/XR/XRExample.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const {XR} = require('@callstack/react-native-visionos'); +const React = require('react'); +const {Alert, Button, StyleSheet, Text, View} = require('react-native'); + +const OpenXRSession = () => { + const [isOpen, setIsOpen] = React.useState(false); + + const openXRSession = async () => { + try { + if (!XR.supportsMultipleScenes) { + Alert.alert('Error', 'Multiple scenes are not supported'); + return; + } + await XR.requestSession('TestImmersiveSpace'); + setIsOpen(true); + } catch (e) { + Alert.alert('Error', e.message); + setIsOpen(false); + } + }; + + const closeXRSession = async () => { + if (isOpen) { + await XR.endSession(); + } + }; + + return ( + + Is XR session open: {isOpen} +