Skip to content

Commit 319f3a8

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 c33c583 commit 319f3a8

File tree

23 files changed

+340
-220
lines changed

23 files changed

+340
-220
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ NS_ASSUME_NONNULL_BEGIN
159159
- (BOOL)bridgelessEnabled;
160160

161161
/// Return the bundle URL for the main bundle.
162-
- (NSURL *)bundleURL;
162+
- (NSURL *__nullable)bundleURL;
163+
164+
/// Don't use this method, it's going to be removed soon.
165+
- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary*)initialProperties launchOptions:(NSDictionary*)launchOptions;
163166

164167
@end
165168

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

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -83,51 +83,16 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
8383
{
8484
RCTSetNewArchEnabled([self newArchEnabled]);
8585
BOOL enableTM = self.turboModuleEnabled;
86-
BOOL fabricEnabled = self.fabricEnabled;
87-
BOOL enableBridgeless = self.bridgelessEnabled;
88-
89-
NSDictionary *initProps = updateInitialProps([self prepareInitialProps], fabricEnabled);
9086

9187
RCTAppSetupPrepareApp(application, enableTM, *_reactNativeConfig);
92-
93-
UIView *rootView;
94-
if (enableBridgeless) {
95-
// Enable native view config interop only if both bridgeless mode and Fabric is enabled.
96-
RCTSetUseNativeViewConfigsInBridgelessMode(fabricEnabled);
97-
98-
// Enable TurboModule interop by default in Bridgeless mode
99-
RCTEnableTurboModuleInterop(YES);
100-
RCTEnableTurboModuleInteropBridgeProxy(YES);
101-
102-
[self createReactHost];
103-
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
104-
RCTFabricSurface *surface = [_reactHost createSurfaceWithModuleName:self.moduleName initialProperties:initProps];
105-
106-
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView = [[RCTSurfaceHostingProxyRootView alloc]
107-
initWithSurface:surface
108-
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];
109-
110-
rootView = (RCTRootView *)surfaceHostingProxyRootView;
111-
} else {
112-
if (!self.bridge) {
113-
self.bridge = [self createBridgeWithDelegate:self launchOptions:launchOptions];
114-
}
115-
if ([self newArchEnabled]) {
116-
self.bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:self.bridge
117-
contextContainer:_contextContainer];
118-
self.bridge.surfacePresenter = self.bridgeAdapter.surfacePresenter;
119-
120-
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
121-
}
122-
rootView = [self createRootViewWithBridge:self.bridge moduleName:self.moduleName initProps:initProps];
123-
}
124-
125-
[self customizeRootView:(RCTRootView *)rootView];
88+
12689
#if TARGET_OS_VISION
127-
self.window = [[UIWindow alloc] initWithFrame:RCTForegroundWindow().bounds];
90+
/// Bail out of UIWindow initializaiton to support multi-window scenarios in SwiftUI lifecycle.
91+
return YES;
12892
#else
93+
UIView* rootView = [self viewWithModuleName:self.moduleName initialProperties:[self prepareInitialProps] launchOptions:launchOptions];
94+
12995
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
130-
#endif
13196

13297
UIViewController *rootViewController = [self createRootViewController];
13398
[self setRootView:rootView toRootViewController:rootViewController];
@@ -136,13 +101,59 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
136101
[self.window makeKeyAndVisible];
137102

138103
return YES;
104+
#endif
139105
}
140106

141107
- (void)applicationDidEnterBackground:(UIApplication *)application
142108
{
143109
// Noop
144110
}
145111

112+
- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary*)initialProperties launchOptions:(NSDictionary*)launchOptions {
113+
BOOL fabricEnabled = self.fabricEnabled;
114+
BOOL enableBridgeless = self.bridgelessEnabled;
115+
116+
NSDictionary *initProps = updateInitialProps(initialProperties, fabricEnabled);
117+
118+
UIView *rootView;
119+
if (enableBridgeless) {
120+
// Enable native view config interop only if both bridgeless mode and Fabric is enabled.
121+
RCTSetUseNativeViewConfigsInBridgelessMode(self.fabricEnabled);
122+
123+
// Enable TurboModule interop by default in Bridgeless mode
124+
RCTEnableTurboModuleInterop(YES);
125+
RCTEnableTurboModuleInteropBridgeProxy(YES);
126+
127+
[self createReactHost];
128+
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
129+
RCTFabricSurface *surface = [_reactHost createSurfaceWithModuleName:self.moduleName initialProperties:initProps];
130+
131+
RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView = [[RCTSurfaceHostingProxyRootView alloc]
132+
initWithSurface:surface
133+
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];
134+
135+
rootView = (RCTRootView *)surfaceHostingProxyRootView;
136+
} else {
137+
if (!self.bridge) {
138+
self.bridge = [self createBridgeWithDelegate:self launchOptions:launchOptions];
139+
}
140+
if ([self newArchEnabled]) {
141+
if (!self.bridgeAdapter) {
142+
self.bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:self.bridge
143+
contextContainer:_contextContainer];
144+
self.bridge.surfacePresenter = self.bridgeAdapter.surfacePresenter;
145+
146+
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
147+
}
148+
}
149+
rootView = [self createRootViewWithBridge:self.bridge moduleName:moduleName initProps:initProps];
150+
}
151+
152+
[self customizeRootView:(RCTRootView *)rootView];
153+
154+
return rootView;
155+
}
156+
146157
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
147158
{
148159
[NSException raise:@"RCTBridgeDelegate::sourceURLForBridge not implemented"
@@ -301,6 +312,9 @@ - (Class)getModuleClassFromName:(const char *)name
301312

302313
- (void)createReactHost
303314
{
315+
if (_reactHost != nil) {
316+
return;
317+
}
304318
__weak __typeof(self) weakSelf = self;
305319
_reactHost = [[RCTHost alloc] initWithBundleURL:[self bundleURL]
306320
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/Base/RCTBridgeDelegate.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
2020
* When running from a locally bundled JS file, this should be a `file://` url
2121
* pointing to a path inside the app resources, e.g. `file://.../main.jsbundle`.
2222
*/
23-
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge;
23+
- (NSURL *__nullable)sourceURLForBridge:(RCTBridge *)bridge;
2424

2525
@optional
2626

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

Lines changed: 4 additions & 3 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>
@@ -70,7 +70,8 @@ @implementation RCTAppearance {
7070
- (instancetype)init
7171
{
7272
if ((self = [super init])) {
73-
UITraitCollection *traitCollection = RCTSharedApplication().delegate.window.traitCollection;
73+
// TODO: Remove this after merging this PR upstream: https://github.com/facebook/react-native/pull/42231
74+
UITraitCollection *traitCollection = RCTKeyWindow().traitCollection;
7475
_currentColorScheme = RCTColorSchemePreference(traitCollection);
7576
[[NSNotificationCenter defaultCenter] addObserver:self
7677
selector:@selector(appearanceChanged:)

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

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

316316
[self updateStats];
317317

318-
UIWindow *window = RCTSharedApplication().delegate.window;
318+
UIWindow *window = RCTKeyWindow();
319319
[window addSubview:self.container];
320320

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

0 commit comments

Comments
 (0)