Skip to content

Commit 44b29cd

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 7579bdc commit 44b29cd

File tree

21 files changed

+279
-189
lines changed

21 files changed

+279
-189
lines changed

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,10 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
5757

5858
self.rootViewFactory = [self createRCTRootViewFactory];
5959

60-
UIView *rootView = [self.rootViewFactory viewWithModuleName:self.moduleName
61-
initialProperties:self.initialProps
62-
launchOptions:launchOptions];
63-
6460
if (self.newArchEnabled || self.fabricEnabled) {
6561
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
6662
}
6763
[self _logWarnIfCreateRootViewWithBridgeIsOverridden];
68-
[self customizeRootView:(RCTRootView *)rootView];
69-
70-
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
71-
UIViewController *rootViewController = [self createRootViewController];
72-
[self setRootView:rootView toRootViewController:rootViewController];
73-
self.window.rootViewController = rootViewController;
74-
self.window.windowScene.delegate = self;
75-
[self.window makeKeyAndVisible];
7664

7765
return YES;
7866
}
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
@@ -299,7 +299,7 @@ - (void)show
299299

300300
[self updateStats];
301301

302-
UIWindow *window = RCTSharedApplication().delegate.window;
302+
UIWindow *window = RCTKeyWindow();
303303
[window addSubview:self.container];
304304

305305
_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
@@ -134,6 +134,7 @@ def use_react_native! (
134134
pod 'React-jserrorhandler', :path => "#{prefix}/ReactCommon/jserrorhandler"
135135
pod 'React-nativeconfig', :path => "#{prefix}/ReactCommon"
136136
pod 'RCTDeprecation', :path => "#{prefix}/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
137+
pod 'React-RCTSwiftExtensions', :path => "#{prefix}/Libraries/SwiftExtensions"
137138

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

packages/react-native/template/visionos/HelloWorld.xcodeproj/project.pbxproj

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99
/* Begin PBXBuildFile section */
1010
00E356F31AD99517003FC87E /* HelloWorldTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* HelloWorldTests.m */; };
1111
0C80B921A6F3F58F76C31292 /* libPods-HelloWorld.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-HelloWorld.a */; };
12-
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
1312
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
14-
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
13+
767CEBBC2B582F6B000139AD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767CEBBB2B582F6B000139AD /* AppDelegate.swift */; };
14+
767CEBBE2B582F78000139AD /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767CEBBD2B582F78000139AD /* App.swift */; };
1515
7699B88040F8A987B510C191 /* libPods-HelloWorld-HelloWorldTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-HelloWorld-HelloWorldTests.a */; };
16-
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
1716
/* End PBXBuildFile section */
1817

1918
/* Begin PBXContainerItemProxy section */
@@ -31,17 +30,15 @@
3130
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3231
00E356F21AD99517003FC87E /* HelloWorldTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HelloWorldTests.m; sourceTree = "<group>"; };
3332
13B07F961A680F5B00A75B9A /* HelloWorld.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloWorld.app; sourceTree = BUILT_PRODUCTS_DIR; };
34-
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = HelloWorld/AppDelegate.h; sourceTree = "<group>"; };
35-
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = HelloWorld/AppDelegate.mm; sourceTree = "<group>"; };
3633
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = HelloWorld/Images.xcassets; sourceTree = "<group>"; };
3734
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = HelloWorld/Info.plist; sourceTree = "<group>"; };
38-
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = HelloWorld/main.m; sourceTree = "<group>"; };
3935
19F6CBCC0A4E27FBF8BF4A61 /* libPods-HelloWorld-HelloWorldTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HelloWorld-HelloWorldTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
4036
3B4392A12AC88292D35C810B /* Pods-HelloWorld.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld.debug.xcconfig"; path = "Target Support Files/Pods-HelloWorld/Pods-HelloWorld.debug.xcconfig"; sourceTree = "<group>"; };
4137
5709B34CF0A7D63546082F79 /* Pods-HelloWorld.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld.release.xcconfig"; path = "Target Support Files/Pods-HelloWorld/Pods-HelloWorld.release.xcconfig"; sourceTree = "<group>"; };
4238
5B7EB9410499542E8C5724F5 /* Pods-HelloWorld-HelloWorldTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld-HelloWorldTests.debug.xcconfig"; path = "Target Support Files/Pods-HelloWorld-HelloWorldTests/Pods-HelloWorld-HelloWorldTests.debug.xcconfig"; sourceTree = "<group>"; };
4339
5DCACB8F33CDC322A6C60F78 /* libPods-HelloWorld.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HelloWorld.a"; sourceTree = BUILT_PRODUCTS_DIR; };
44-
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = HelloWorld/LaunchScreen.storyboard; sourceTree = "<group>"; };
40+
767CEBBB2B582F6B000139AD /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = HelloWorld/AppDelegate.swift; sourceTree = "<group>"; };
41+
767CEBBD2B582F78000139AD /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = App.swift; path = HelloWorld/App.swift; sourceTree = "<group>"; };
4542
89C6BE57DB24E9ADA2F236DE /* Pods-HelloWorld-HelloWorldTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld-HelloWorldTests.release.xcconfig"; path = "Target Support Files/Pods-HelloWorld-HelloWorldTests/Pods-HelloWorld-HelloWorldTests.release.xcconfig"; sourceTree = "<group>"; };
4643
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
4744
/* End PBXFileReference section */
@@ -86,12 +83,10 @@
8683
13B07FAE1A68108700A75B9A /* HelloWorld */ = {
8784
isa = PBXGroup;
8885
children = (
89-
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
90-
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
86+
767CEBBB2B582F6B000139AD /* AppDelegate.swift */,
87+
767CEBBD2B582F78000139AD /* App.swift */,
9188
13B07FB51A68108700A75B9A /* Images.xcassets */,
9289
13B07FB61A68108700A75B9A /* Info.plist */,
93-
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
94-
13B07FB71A68108700A75B9A /* main.m */,
9590
);
9691
name = HelloWorld;
9792
sourceTree = "<group>";
@@ -241,7 +236,6 @@
241236
isa = PBXResourcesBuildPhase;
242237
buildActionMask = 2147483647;
243238
files = (
244-
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
245239
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
246240
);
247241
runOnlyForDeploymentPostprocessing = 0;
@@ -392,8 +386,8 @@
392386
isa = PBXSourcesBuildPhase;
393387
buildActionMask = 2147483647;
394388
files = (
395-
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
396-
13B07FC11A68108700A75B9A /* main.m in Sources */,
389+
767CEBBC2B582F6B000139AD /* AppDelegate.swift in Sources */,
390+
767CEBBE2B582F78000139AD /* App.swift in Sources */,
397391
);
398392
runOnlyForDeploymentPostprocessing = 0;
399393
};
@@ -484,9 +478,10 @@
484478
SUPPORTS_MACCATALYST = NO;
485479
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
486480
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
481+
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
487482
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
488483
SWIFT_VERSION = 5.0;
489-
TARGETED_DEVICE_FAMILY = "7";
484+
TARGETED_DEVICE_FAMILY = 7;
490485
VERSIONING_SYSTEM = "apple-generic";
491486
};
492487
name = Debug;
@@ -516,7 +511,7 @@
516511
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
517512
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
518513
SWIFT_VERSION = 5.0;
519-
TARGETED_DEVICE_FAMILY = "7";
514+
TARGETED_DEVICE_FAMILY = 7;
520515
VERSIONING_SYSTEM = "apple-generic";
521516
};
522517
name = Release;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import SwiftUI
2+
import React
3+
import React_RCTSwiftExtensions
4+
5+
@main
6+
struct HelloWorldApp: App {
7+
@UIApplicationDelegateAdaptor var delegate: AppDelegate
8+
9+
var body: some Scene {
10+
RCTMainWindow(moduleName: "HelloWorld")
11+
}
12+
}

packages/react-native/template/visionos/HelloWorld/AppDelegate.h

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/react-native/template/visionos/HelloWorld/AppDelegate.mm

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import UIKit
2+
import React
3+
import React_RCTAppDelegate
4+
5+
class AppDelegate: RCTAppDelegate {
6+
override func sourceURL(for bridge: RCTBridge) -> URL? {
7+
self.bundleURL()
8+
}
9+
10+
override func bundleURL() -> URL? {
11+
#if DEBUG
12+
RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index")
13+
#else
14+
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
15+
#endif
16+
}
17+
}

packages/react-native/template/visionos/HelloWorld/Info.plist

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
</dict>
3535
<key>NSLocationWhenInUseUsageDescription</key>
3636
<string></string>
37-
<key>UILaunchStoryboardName</key>
38-
<string>LaunchScreen</string>
3937
<key>UIRequiredDeviceCapabilities</key>
4038
<array>
4139
<string>armv7</string>

0 commit comments

Comments
 (0)