Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make SwiftUI React Native entry point #68

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upstreamed: facebook#42231

_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
1 change: 1 addition & 0 deletions packages/react-native/template/_gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ yarn-error.log

# testing
/coverage
.yarn
Loading