Skip to content

Commit 17a2d2b

Browse files
committed
feat: make SwiftUI React Native entry point (#68)
* feat: add Swift entrypoint [wip] add module maps to some RN modules to allow for swift c++ imports feat: implement RCTReactController and RCTSwiftUIAppDelegate feat: introduce new method to RCTAppDelegate * feat: modify template to use SwiftUI * fix: dimensions, use RCTMainWindow() * fix: fallback to DarkMode on visionOS * fix: use KeyWindow() in RCTPerfMonitor
1 parent 81ffd93 commit 17a2d2b

File tree

22 files changed

+336
-216
lines changed

22 files changed

+336
-216
lines changed

packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ NS_ASSUME_NONNULL_BEGIN
160160
/// Return the bundle URL for the main bundle.
161161
- (NSURL *__nullable)bundleURL;
162162

163+
/// Don't use this method, it's going to be removed soon.
164+
- (UIView *)viewWithModuleName:(NSString *)moduleName
165+
initialProperties:(NSDictionary *)initialProperties
166+
launchOptions:(NSDictionary *)launchOptions;
167+
163168
@end
164169

165170
NS_ASSUME_NONNULL_END

packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -78,29 +78,54 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
7878
{
7979
RCTSetNewArchEnabled([self newArchEnabled]);
8080
BOOL enableTM = self.turboModuleEnabled;
81-
BOOL fabricEnabled = self.fabricEnabled;
82-
BOOL enableBridgeless = self.bridgelessEnabled;
83-
84-
NSDictionary *initProps = updateInitialProps([self prepareInitialProps], fabricEnabled);
8581

8682
RCTAppSetupPrepareApp(application, enableTM, *_reactNativeConfig);
83+
84+
#if TARGET_OS_VISION
85+
/// Bail out of UIWindow initializaiton to support multi-window scenarios in SwiftUI lifecycle.
86+
return YES;
87+
#else
88+
UIView* rootView = [self viewWithModuleName:self.moduleName initialProperties:[self prepareInitialProps] launchOptions:launchOptions];
89+
90+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
91+
92+
UIViewController *rootViewController = [self createRootViewController];
93+
[self setRootView:rootView toRootViewController:rootViewController];
94+
self.window.rootViewController = rootViewController;
95+
self.window.windowScene.delegate = self;
96+
[self.window makeKeyAndVisible];
97+
98+
return YES;
99+
#endif
100+
}
101+
102+
- (void)applicationDidEnterBackground:(UIApplication *)application
103+
{
104+
// Noop
105+
}
106+
107+
- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary*)initialProperties launchOptions:(NSDictionary*)launchOptions {
108+
BOOL fabricEnabled = self.fabricEnabled;
109+
BOOL enableBridgeless = self.bridgelessEnabled;
87110

88-
UIView *rootView;
89-
if (enableBridgeless) {
90-
// Enable native view config interop only if both bridgeless mode and Fabric is enabled.
91-
RCTSetUseNativeViewConfigsInBridgelessMode(fabricEnabled);
111+
NSDictionary *initProps = updateInitialProps(initialProperties, fabricEnabled);
112+
113+
UIView *rootView;
114+
if (enableBridgeless) {
115+
// Enable native view config interop only if both bridgeless mode and Fabric is enabled.
116+
RCTSetUseNativeViewConfigsInBridgelessMode(self.fabricEnabled);
92117

93-
// Enable TurboModule interop by default in Bridgeless mode
94-
RCTEnableTurboModuleInterop(YES);
95-
RCTEnableTurboModuleInteropBridgeProxy(YES);
118+
// Enable TurboModule interop by default in Bridgeless mode
119+
RCTEnableTurboModuleInterop(YES);
120+
RCTEnableTurboModuleInteropBridgeProxy(YES);
96121

97-
[self createReactHost];
98-
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
99-
RCTFabricSurface *surface = [_reactHost createSurfaceWithModuleName:self.moduleName initialProperties:initProps];
122+
[self createReactHost];
123+
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
124+
RCTFabricSurface *surface = [_reactHost createSurfaceWithModuleName:self.moduleName initialProperties:initProps];
100125

101-
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView = [[RCTSurfaceHostingProxyRootView alloc]
102-
initWithSurface:surface
103-
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];
126+
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView = [[RCTSurfaceHostingProxyRootView alloc]
127+
initWithSurface:surface
128+
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];
104129

105130
rootView = (RCTRootView *)surfaceHostingProxyRootView;
106131
rootView.backgroundColor = [UIColor systemBackgroundColor];
@@ -113,30 +138,15 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
113138
contextContainer:_contextContainer];
114139
self.bridge.surfacePresenter = self.bridgeAdapter.surfacePresenter;
115140

116-
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
141+
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
142+
}
143+
}
144+
rootView = [self createRootViewWithBridge:self.bridge moduleName:moduleName initProps:initProps];
117145
}
118-
rootView = [self createRootViewWithBridge:self.bridge moduleName:self.moduleName initProps:initProps];
119-
}
120146

121-
[self customizeRootView:(RCTRootView *)rootView];
122-
#if TARGET_OS_VISION
123-
self.window = [[UIWindow alloc] initWithFrame:RCTForegroundWindow().bounds];
124-
#else
125-
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
126-
#endif
127-
128-
UIViewController *rootViewController = [self createRootViewController];
129-
[self setRootView:rootView toRootViewController:rootViewController];
130-
self.window.rootViewController = rootViewController;
131-
self.window.windowScene.delegate = self;
132-
[self.window makeKeyAndVisible];
133-
134-
return YES;
135-
}
136-
137-
- (void)applicationDidEnterBackground:(UIApplication *)application
138-
{
139-
// Noop
147+
[self customizeRootView:(RCTRootView *)rootView];
148+
149+
return rootView;
140150
}
141151

142152
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
@@ -297,6 +307,9 @@ - (Class)getModuleClassFromName:(const char *)name
297307

298308
- (void)createReactHost
299309
{
310+
if (_reactHost != nil) {
311+
return;
312+
}
300313
__weak __typeof(self) weakSelf = self;
301314
_reactHost = [[RCTHost alloc] initWithBundleURL:[self bundleURL]
302315
hostDelegate:nil
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import SwiftUI
2+
3+
/**
4+
This SwiftUI struct returns main React Native scene. It should be used only once as it conains setup code.
5+
6+
Example:
7+
```swift
8+
@main
9+
struct YourApp: App {
10+
@UIApplicationDelegateAdaptor var delegate: AppDelegate
11+
12+
var body: some Scene {
13+
RCTMainWindow(moduleName: "YourApp")
14+
}
15+
}
16+
```
17+
18+
Note: If you want to create additional windows in your app, create a new `WindowGroup {}` and pass it a `RCTRootViewRepresentable`.
19+
*/
20+
public struct RCTMainWindow: Scene {
21+
var moduleName: String
22+
var initialProps: RCTRootViewRepresentable.InitialPropsType
23+
24+
public init(moduleName: String, initialProps: RCTRootViewRepresentable.InitialPropsType = nil) {
25+
self.moduleName = moduleName
26+
self.initialProps = initialProps
27+
}
28+
29+
public var body: some Scene {
30+
WindowGroup {
31+
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
32+
}
33+
}
34+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#import <UIKit/UIKit.h>
2+
3+
/**
4+
A `UIViewController` responsible for embeding `RCTRootView` inside. Uses Factory pattern to retrive new view instances.
5+
6+
Note: Used to in `RCTRootViewRepresentable` to display React views.
7+
*/
8+
@interface RCTReactViewController : UIViewController
9+
10+
@property (nonatomic, strong, nonnull) NSString *moduleName;
11+
@property (nonatomic, strong, nullable) NSDictionary *initialProps;
12+
13+
- (instancetype _Nonnull)initWithModuleName:(NSString *_Nonnull)moduleName
14+
initProps:(NSDictionary *_Nullable)initProps;
15+
16+
@end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#import "RCTReactViewController.h"
2+
#import <React/RCTConstants.h>
3+
4+
@protocol RCTRootViewFactoryProtocol <NSObject>
5+
6+
- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary*)initialProperties launchOptions:(NSDictionary*)launchOptions;
7+
8+
@end
9+
10+
@implementation RCTReactViewController
11+
12+
- (instancetype)initWithModuleName:(NSString *)moduleName initProps:(NSDictionary *)initProps {
13+
if (self = [super init]) {
14+
_moduleName = moduleName;
15+
_initialProps = initProps;
16+
}
17+
return self;
18+
}
19+
20+
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
21+
[[NSNotificationCenter defaultCenter] postNotificationName:RCTWindowFrameDidChangeNotification object:self];
22+
}
23+
24+
// TODO: Temporary solution for creating RCTRootView on demand. This should be done through factory pattern, see here: https://github.com/facebook/react-native/pull/42263
25+
- (void)loadView {
26+
id<UIApplicationDelegate> appDelegate = [UIApplication sharedApplication].delegate;
27+
if ([appDelegate respondsToSelector:@selector(viewWithModuleName:initialProperties:launchOptions:)]) {
28+
id<RCTRootViewFactoryProtocol> delegate = (id<RCTRootViewFactoryProtocol>)appDelegate;
29+
self.view = [delegate viewWithModuleName:_moduleName initialProperties:_initialProps launchOptions:@{}];
30+
} else {
31+
[NSException raise:@"UIApplicationDelegate:viewWithModuleName:initialProperties:launchOptions: not implemented"
32+
format:@"Make sure you subclass RCTAppDelegate"];
33+
}
34+
}
35+
36+
@end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import SwiftUI
2+
3+
/**
4+
SwiftUI view enclosing `RCTReactViewController`. Its main purpose is to display React Native views inside of SwiftUI lifecycle.
5+
6+
Use it create new windows in your app:
7+
Example:
8+
```swift
9+
WindowGroup {
10+
RCTRootViewRepresentable(moduleName: "YourAppName")
11+
}
12+
```
13+
*/
14+
public struct RCTRootViewRepresentable: UIViewControllerRepresentable {
15+
public typealias InitialPropsType = [AnyHashable: Any]?
16+
17+
var moduleName: String
18+
var initialProps: InitialPropsType
19+
20+
public init(moduleName: String, initialProps: InitialPropsType = nil) {
21+
self.moduleName = moduleName
22+
self.initialProps = initialProps
23+
}
24+
25+
public func makeUIViewController(context: Context) -> UIViewController {
26+
RCTReactViewController(moduleName: moduleName, initProps: initialProps)
27+
}
28+
29+
public func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
30+
// noop
31+
}
32+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require "json"
2+
3+
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
4+
version = package['version']
5+
6+
source = { :git => 'https://github.com/facebook/react-native.git' }
7+
if version == '1000.0.0'
8+
# This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in.
9+
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
10+
else
11+
source[:tag] = "v#{version}"
12+
end
13+
14+
Pod::Spec.new do |s|
15+
s.name = "React-RCTSwiftExtensions"
16+
s.version = version
17+
s.summary = "A library for easier React Native integration with SwiftUI."
18+
s.homepage = "https://reactnative.dev/"
19+
s.license = package["license"]
20+
s.author = "Callstack"
21+
s.platforms = min_supported_versions
22+
s.source = source
23+
s.source_files = "*.{swift,h,m}"
24+
s.frameworks = ["UIKit", "SwiftUI"]
25+
26+
s.dependency "React-Core"
27+
end

packages/react-native/React/CoreModules/RCTAppearance.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ void RCTOverrideAppearancePreference(NSString *const colorSchemeOverride)
5656
// Return the default if the app doesn't allow different color schemes.
5757
return RCTAppearanceColorSchemeLight;
5858
}
59-
60-
return appearances[@(traitCollection.userInterfaceStyle)] ?: RCTAppearanceColorSchemeLight;
59+
// Fallback to dark mode on visionOS
60+
return appearances[@(traitCollection.userInterfaceStyle)] ?: RCTAppearanceColorSchemeDark;
6161
}
6262

6363
@interface RCTAppearance () <NativeAppearanceSpec>

packages/react-native/React/CoreModules/RCTPerfMonitor.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ - (void)show
311311

312312
[self updateStats];
313313

314-
UIWindow *window = RCTSharedApplication().delegate.window;
314+
UIWindow *window = RCTKeyWindow();
315315
[window addSubview:self.container];
316316

317317
_uiDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)];

packages/react-native/scripts/react_native_pods.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def use_react_native! (
133133
pod 'React-jserrorhandler', :path => "#{prefix}/ReactCommon/jserrorhandler"
134134
pod 'React-nativeconfig', :path => "#{prefix}/ReactCommon"
135135
pod 'RCTDeprecation', :path => "#{prefix}/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
136+
pod 'React-RCTSwiftExtensions', :path => "#{prefix}/Libraries/SwiftExtensions"
136137

137138
if hermes_enabled
138139
setup_hermes!(:react_native_path => prefix)

0 commit comments

Comments
 (0)