Skip to content

Commit

Permalink
feat: make SwiftUI React Native entry point (#68)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
okwasniewski committed Jan 23, 2024
1 parent c33c583 commit 319f3a8
Show file tree
Hide file tree
Showing 23 changed files with 340 additions and 220 deletions.
5 changes: 4 additions & 1 deletion packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)bridgelessEnabled;

/// Return the bundle URL for the main bundle.
- (NSURL *)bundleURL;
- (NSURL *__nullable)bundleURL;

/// Don't use this method, it's going to be removed soon.
- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary*)initialProperties launchOptions:(NSDictionary*)launchOptions;

@end

Expand Down
94 changes: 54 additions & 40 deletions packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -83,51 +83,16 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
{
RCTSetNewArchEnabled([self newArchEnabled]);
BOOL enableTM = self.turboModuleEnabled;
BOOL fabricEnabled = self.fabricEnabled;
BOOL enableBridgeless = self.bridgelessEnabled;

NSDictionary *initProps = updateInitialProps([self prepareInitialProps], fabricEnabled);

RCTAppSetupPrepareApp(application, enableTM, *_reactNativeConfig);

UIView *rootView;
if (enableBridgeless) {
// Enable native view config interop only if both bridgeless mode and Fabric is enabled.
RCTSetUseNativeViewConfigsInBridgelessMode(fabricEnabled);

// Enable TurboModule interop by default in Bridgeless mode
RCTEnableTurboModuleInterop(YES);
RCTEnableTurboModuleInteropBridgeProxy(YES);

[self createReactHost];
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
RCTFabricSurface *surface = [_reactHost createSurfaceWithModuleName:self.moduleName initialProperties:initProps];

RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView = [[RCTSurfaceHostingProxyRootView alloc]
initWithSurface:surface
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];

rootView = (RCTRootView *)surfaceHostingProxyRootView;
} else {
if (!self.bridge) {
self.bridge = [self createBridgeWithDelegate:self launchOptions:launchOptions];
}
if ([self newArchEnabled]) {
self.bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:self.bridge
contextContainer:_contextContainer];
self.bridge.surfacePresenter = self.bridgeAdapter.surfacePresenter;

[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
}
rootView = [self createRootViewWithBridge:self.bridge moduleName:self.moduleName initProps:initProps];
}

[self customizeRootView:(RCTRootView *)rootView];

#if TARGET_OS_VISION
self.window = [[UIWindow alloc] initWithFrame:RCTForegroundWindow().bounds];
/// Bail out of UIWindow initializaiton to support multi-window scenarios in SwiftUI lifecycle.
return YES;
#else
UIView* rootView = [self viewWithModuleName:self.moduleName initialProperties:[self prepareInitialProps] launchOptions:launchOptions];

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
#endif

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

return YES;
#endif
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Noop
}

- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary*)initialProperties launchOptions:(NSDictionary*)launchOptions {
BOOL fabricEnabled = self.fabricEnabled;
BOOL enableBridgeless = self.bridgelessEnabled;

NSDictionary *initProps = updateInitialProps(initialProperties, fabricEnabled);

UIView *rootView;
if (enableBridgeless) {
// Enable native view config interop only if both bridgeless mode and Fabric is enabled.
RCTSetUseNativeViewConfigsInBridgelessMode(self.fabricEnabled);

// Enable TurboModule interop by default in Bridgeless mode
RCTEnableTurboModuleInterop(YES);
RCTEnableTurboModuleInteropBridgeProxy(YES);

[self createReactHost];
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
RCTFabricSurface *surface = [_reactHost createSurfaceWithModuleName:self.moduleName initialProperties:initProps];

RCTSurfaceHostingProxyRootView *surfaceHostingProxyRootView = [[RCTSurfaceHostingProxyRootView alloc]
initWithSurface:surface
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];

rootView = (RCTRootView *)surfaceHostingProxyRootView;
} else {
if (!self.bridge) {
self.bridge = [self createBridgeWithDelegate:self launchOptions:launchOptions];
}
if ([self newArchEnabled]) {
if (!self.bridgeAdapter) {
self.bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:self.bridge
contextContainer:_contextContainer];
self.bridge.surfacePresenter = self.bridgeAdapter.surfacePresenter;

[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
}
}
rootView = [self createRootViewWithBridge:self.bridge moduleName:moduleName initProps:initProps];
}

[self customizeRootView:(RCTRootView *)rootView];

return rootView;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
[NSException raise:@"RCTBridgeDelegate::sourceURLForBridge not implemented"
Expand Down Expand Up @@ -301,6 +312,9 @@ - (Class)getModuleClassFromName:(const char *)name

- (void)createReactHost
{
if (_reactHost != nil) {
return;
}
__weak __typeof(self) weakSelf = self;
_reactHost = [[RCTHost alloc] initWithBundleURL:[self bundleURL]
hostDelegate:nil
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import SwiftUI

/**
This SwiftUI struct returns main React Native scene. It should be used only once as it conains setup code.

Example:
```swift
@main
struct YourApp: App {
@UIApplicationDelegateAdaptor var delegate: AppDelegate

var body: some Scene {
RCTMainWindow(moduleName: "YourApp")
}
}
```

Note: If you want to create additional windows in your app, create a new `WindowGroup {}` and pass it a `RCTRootViewRepresentable`.
*/
public struct RCTMainWindow: Scene {
var moduleName: String
var initialProps: RCTRootViewRepresentable.InitialPropsType

public init(moduleName: String, initialProps: RCTRootViewRepresentable.InitialPropsType = nil) {
self.moduleName = moduleName
self.initialProps = initialProps
}

public var body: some Scene {
WindowGroup {
RCTRootViewRepresentable(moduleName: moduleName, initialProps: initialProps)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#import <UIKit/UIKit.h>

/**
A `UIViewController` responsible for embeding `RCTRootView` inside. Uses Factory pattern to retrive new view instances.
Note: Used to in `RCTRootViewRepresentable` to display React views.
*/
@interface RCTReactViewController : UIViewController

@property (nonatomic, strong, nonnull) NSString *moduleName;
@property (nonatomic, strong, nullable) NSDictionary *initialProps;

- (instancetype _Nonnull)initWithModuleName:(NSString *_Nonnull)moduleName
initProps:(NSDictionary *_Nullable)initProps;

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#import "RCTReactViewController.h"
#import <React/RCTConstants.h>

@protocol RCTRootViewFactoryProtocol <NSObject>

- (UIView *)viewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary*)initialProperties launchOptions:(NSDictionary*)launchOptions;

@end

@implementation RCTReactViewController

- (instancetype)initWithModuleName:(NSString *)moduleName initProps:(NSDictionary *)initProps {
if (self = [super init]) {
_moduleName = moduleName;
_initialProps = initProps;
}
return self;
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTWindowFrameDidChangeNotification object:self];
}

// 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
- (void)loadView {
id<UIApplicationDelegate> appDelegate = [UIApplication sharedApplication].delegate;
if ([appDelegate respondsToSelector:@selector(viewWithModuleName:initialProperties:launchOptions:)]) {
id<RCTRootViewFactoryProtocol> delegate = (id<RCTRootViewFactoryProtocol>)appDelegate;
self.view = [delegate viewWithModuleName:_moduleName initialProperties:_initialProps launchOptions:@{}];
} else {
[NSException raise:@"UIApplicationDelegate:viewWithModuleName:initialProperties:launchOptions: not implemented"
format:@"Make sure you subclass RCTAppDelegate"];
}
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import SwiftUI

/**
SwiftUI view enclosing `RCTReactViewController`. Its main purpose is to display React Native views inside of SwiftUI lifecycle.

Use it create new windows in your app:
Example:
```swift
WindowGroup {
RCTRootViewRepresentable(moduleName: "YourAppName")
}
```
*/
public struct RCTRootViewRepresentable: UIViewControllerRepresentable {
public typealias InitialPropsType = [AnyHashable: Any]?

var moduleName: String
var initialProps: InitialPropsType

public init(moduleName: String, initialProps: InitialPropsType = nil) {
self.moduleName = moduleName
self.initialProps = initialProps
}

public func makeUIViewController(context: Context) -> UIViewController {
RCTReactViewController(moduleName: moduleName, initProps: initialProps)
}

public func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
// noop
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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

Pod::Spec.new do |s|
s.name = "React-RCTSwiftExtensions"
s.version = version
s.summary = "A library for easier React Native integration with SwiftUI."
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Callstack"
s.platforms = min_supported_versions
s.source = source
s.source_files = "*.{swift,h,m}"
s.frameworks = ["UIKit", "SwiftUI"]

s.dependency "React-Core"
end
2 changes: 1 addition & 1 deletion packages/react-native/React/Base/RCTBridgeDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
* When running from a locally bundled JS file, this should be a `file://` url
* pointing to a path inside the app resources, e.g. `file://.../main.jsbundle`.
*/
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge;
- (NSURL *__nullable)sourceURLForBridge:(RCTBridge *)bridge;

@optional

Expand Down
7 changes: 4 additions & 3 deletions packages/react-native/React/CoreModules/RCTAppearance.mm
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ void RCTOverrideAppearancePreference(NSString *const colorSchemeOverride)
// Return the default if the app doesn't allow different color schemes.
return RCTAppearanceColorSchemeLight;
}

return appearances[@(traitCollection.userInterfaceStyle)] ?: RCTAppearanceColorSchemeLight;
// Fallback to dark mode on visionOS
return appearances[@(traitCollection.userInterfaceStyle)] ?: RCTAppearanceColorSchemeDark;
}

@interface RCTAppearance () <NativeAppearanceSpec>
Expand All @@ -70,7 +70,8 @@ @implementation RCTAppearance {
- (instancetype)init
{
if ((self = [super init])) {
UITraitCollection *traitCollection = RCTSharedApplication().delegate.window.traitCollection;
// TODO: Remove this after merging this PR upstream: https://github.com/facebook/react-native/pull/42231
UITraitCollection *traitCollection = RCTKeyWindow().traitCollection;
_currentColorScheme = RCTColorSchemePreference(traitCollection);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appearanceChanged:)
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/CoreModules/RCTPerfMonitor.mm
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ - (void)show

[self updateStats];

UIWindow *window = RCTSharedApplication().delegate.window;
UIWindow *window = RCTKeyWindow();
[window addSubview:self.container];

_uiDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)];
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/scripts/react_native_pods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def use_react_native! (
pod 'React-jserrorhandler', :path => "#{prefix}/ReactCommon/jserrorhandler"
pod 'React-nativeconfig', :path => "#{prefix}/ReactCommon"
pod 'RCTDeprecation', :path => "#{prefix}/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
pod 'React-RCTSwiftExtensions', :path => "#{prefix}/Libraries/SwiftExtensions"

if hermes_enabled
setup_hermes!(:react_native_path => prefix)
Expand Down
Loading

0 comments on commit 319f3a8

Please sign in to comment.