-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
* feat: implement Spatial API * feat: make RCTSpatial decoupled from RCTMainWindow() * feat: implement XR module
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
Check failure on line 18 in packages/react-native/Libraries/XR/ImmersiveBridge.swift GitHub Actions / test_ios_rntester-Hermes
Check failure on line 18 in packages/react-native/Libraries/XR/ImmersiveBridge.swift GitHub Actions / test_ios_rntester-Hermes
Check failure on line 18 in packages/react-native/Libraries/XR/ImmersiveBridge.swift GitHub Actions / test_ios_rntester-Hermes
|
||
@Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace | ||
Check failure on line 19 in packages/react-native/Libraries/XR/ImmersiveBridge.swift GitHub Actions / test_ios_rntester-Hermes
Check failure on line 19 in packages/react-native/Libraries/XR/ImmersiveBridge.swift GitHub Actions / test_ios_rntester-Hermes
Check failure on line 19 in packages/react-native/Libraries/XR/ImmersiveBridge.swift GitHub Actions / test_ios_rntester-Hermes
|
||
|
||
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)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#import <Foundation/Foundation.h> | ||
#import <React/RCTBridgeModule.h> | ||
|
||
@interface RCTXRModule : NSObject <RCTBridgeModule> | ||
|
||
@end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
#import <React/RCTXRModule.h> | ||
|
||
#import <FBReactNativeSpec_visionOS/FBReactNativeSpec_visionOS.h> | ||
|
||
#import <React/RCTBridge.h> | ||
#import <React/RCTConvert.h> | ||
#import <React/RCTUtils.h> | ||
#import "RCTXR-Swift.h" | ||
|
||
@interface RCTXRModule () <NativeXRModuleSpec> | ||
@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<JS::NativeXRModule::Constants::Builder>)constantsToExport { | ||
return [self getConstants]; | ||
} | ||
|
||
- (facebook::react::ModuleConstants<JS::NativeXRModule::Constants>)getConstants { | ||
__block facebook::react::ModuleConstants<JS::NativeXRModule::Constants> constants; | ||
RCTUnsafeExecuteOnMainQueueSync(^{ | ||
constants = facebook::react::typedConstants<JS::NativeXRModule::Constants>({ | ||
.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<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { | ||
return std::make_shared<facebook::react::NativeXRModuleSpecJSI>(params); | ||
} | ||
|
||
@end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
|
||
export interface XRStatic { | ||
requestSession(sessionId: string): Promise<void>; | ||
endSession(): Promise<void>; | ||
supportsMultipleScenes: boolean; | ||
} | ||
|
||
export const XR: XRStatic; | ||
export type XR = XRStatic; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* @format | ||
* @flow strict | ||
* @jsdoc | ||
*/ | ||
|
||
import NativeXRModule from './NativeXRModule'; | ||
|
||
const XR = { | ||
requestSession: (sessionId?: string): Promise<void> => { | ||
if (NativeXRModule != null && NativeXRModule.requestSession != null) { | ||
return NativeXRModule.requestSession(sessionId); | ||
} | ||
return Promise.reject(new Error('NativeXRModule is not available')); | ||
}, | ||
endSession: (): Promise<void> => { | ||
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; |