diff --git a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift index 089326cc..f4d35f78 100644 --- a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift +++ b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift @@ -10,11 +10,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { -#if USE_EXPO_HOST - return ReactNativeHostManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions) -#else - return true -#endif + return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions) } } @@ -23,14 +19,15 @@ struct BrownfieldAppleApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate init() { -#if USE_EXPO_HOST - ReactNativeHostManager.shared.initialize() -#else ReactNativeBrownfield.shared.bundle = ReactNativeBundle ReactNativeBrownfield.shared.startReactNative { print("React Native has been loaded") } + +#if USE_EXPO_HOST + ReactNativeBrownfield.shared.ensureExpoModulesProvider() #endif + BrownfieldStore.register(initialState) } diff --git a/apps/AppleApp/Brownfield Apple App/ContentView.swift b/apps/AppleApp/Brownfield Apple App/ContentView.swift index e6152f1b..4be7c44c 100644 --- a/apps/AppleApp/Brownfield Apple App/ContentView.swift +++ b/apps/AppleApp/Brownfield Apple App/ContentView.swift @@ -1,5 +1,4 @@ import ReactBrownfield -import BrownfieldLib import Brownie import SwiftUI @@ -17,33 +16,12 @@ struct ContentView: View { } } -#if USE_EXPO_HOST -struct RNViewRepresentable: UIViewRepresentable { - - func makeUIView(context: Context) -> UIView { - return ReactNativeHostManager.shared.loadView(moduleName: "RNApp", initialProps: nil, launchOptions: nil) - } - - func updateUIView(_ uiView: UIView, context: Context) { - // Update the view when SwiftUI state changes - } -} -#endif - -struct MainScreen: View { - var rnView: some View { -#if USE_EXPO_HOST - RNViewRepresentable() -#else - ReactNativeView(moduleName: "RNApp") -#endif - } - +struct MainScreen: View { var body: some View { VStack(spacing: 16) { GreetingCard(name: "iOS") - rnView + ReactNativeView(moduleName: "RNApp") .navigationBarHidden(true) .clipShape(RoundedRectangle(cornerRadius: 16)) .background(Color(UIColor.systemBackground)) diff --git a/apps/RNApp/ios/Podfile.lock b/apps/RNApp/ios/Podfile.lock index e84e4c43..238047ad 100644 --- a/apps/RNApp/ios/Podfile.lock +++ b/apps/RNApp/ios/Podfile.lock @@ -2350,7 +2350,7 @@ PODS: - SocketRocket - ReactAppDependencyProvider (0.82.1): - ReactCodegen - - ReactBrownfield (2.2.0): + - ReactBrownfield (3.0.0-rc.1): - boost - DoubleConversion - fast_float @@ -2843,7 +2843,7 @@ SPEC CHECKSUMS: React-utils: f06ff240e06e2bd4b34e48f1b34cac00866e8979 React-webperformancenativemodule: b3398f8175fa96d992c071b1fa59bd6f9646b840 ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176 - ReactBrownfield: 65d6bf15ccb7501d3982ec05f8d37b697e9b3480 + ReactBrownfield: ce231a9060b34e1fe8f91ec8416f21dc6da8b4b5 ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654 RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13 diff --git a/docs/docs/docs/getting-started/expo.mdx b/docs/docs/docs/getting-started/expo.mdx index 9f42d797..8666e27a 100644 --- a/docs/docs/docs/getting-started/expo.mdx +++ b/docs/docs/docs/getting-started/expo.mdx @@ -111,7 +111,7 @@ This should only take a few minutes. - Follow the step for adding the frameworks to your iOS App - [here](/docs/getting-started/ios#6-add-the-framework-to-your-ios-app)
-1. Call the `ReactNativeHostManager.shared.initialize()` from your Application Entry point: +1. Call the following functions from your Application Entry point: ```swift @main @@ -119,7 +119,11 @@ struct IosApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate init() { - ReactNativeHostManager.shared.initialize() + ReactNativeBrownfield.shared.bundle = ReactNativeBundle + ReactNativeBrownfield.shared.startReactNative { + print("React Native has been loaded") + } + ReactNativeBrownfield.shared.ensureExpoModulesProvider() } var body: some Scene { @@ -140,19 +144,19 @@ class AppDelegate: NSObject, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - return ReactNativeHostManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions) + return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions) } } ``` -3. Use the `ReactNativeHostManager.loadView` to present the UI +3. Use the `ReactNativeView` or `ReactNativeViewController` to present the UI -``` -let RNView = ReactNativeHostManager.shared.loadView(moduleName: "ExpoRNApp", initialProps: nil, launchOptions: nil) +```swift +ReactNativeView(moduleName: "ExpoRNApp") + .background(Color(UIColor.systemBackground)) ``` -> This returns a UIView, so if you're using SwiftUI, you will have to wrap this in UIViewRrepresentable . -You can see the demo integration in [Apple App](https://github.com/callstack/react-native-brownfield/blob/main/apps/AppleApp/Brownfield%20Apple%20App/BrownfieldAppleApp.swift) as well +> You can also use `ReactNativeBrownfield.shared.view(moduleName, initialProps)` which returns a UIView. 4. Build and install the iOS application 🚀 diff --git a/packages/react-native-brownfield/ReactBrownfield.podspec b/packages/react-native-brownfield/ReactBrownfield.podspec index b65c4cf6..d839b157 100644 --- a/packages/react-native-brownfield/ReactBrownfield.podspec +++ b/packages/react-native-brownfield/ReactBrownfield.podspec @@ -22,6 +22,10 @@ Pod::Spec.new do |spec| spec.dependency 'ReactAppDependencyProvider' add_dependency(spec, "React-RCTAppDelegate") + + if ENV['REACT_NATIVE_BROWNFIELD_USE_EXPO_HOST'] == '1' + spec.dependency 'Expo' + end install_modules_dependencies(spec) end diff --git a/packages/react-native-brownfield/ios/BrownfieldBundlePathResolver.swift b/packages/react-native-brownfield/ios/BrownfieldBundlePathResolver.swift new file mode 100644 index 00000000..446bd577 --- /dev/null +++ b/packages/react-native-brownfield/ios/BrownfieldBundlePathResolver.swift @@ -0,0 +1,21 @@ +import Foundation + +enum BrownfieldBundlePathResolver { + enum Error: Swift.Error { + case invalidBundlePath(String) + } + + static func resourceComponents(from bundlePath: String) throws -> ( + resourceName: String, + fileExtension: String + ) { + let fileExtension = (bundlePath as NSString).pathExtension + let resourceName = (bundlePath as NSString).deletingPathExtension + + guard !fileExtension.isEmpty, !resourceName.isEmpty else { + throw Error.invalidBundlePath(bundlePath) + } + + return (resourceName, fileExtension) + } +} diff --git a/packages/react-native-brownfield/ios/ExpoHostRuntime.swift b/packages/react-native-brownfield/ios/ExpoHostRuntime.swift new file mode 100644 index 00000000..2a7917cb --- /dev/null +++ b/packages/react-native-brownfield/ios/ExpoHostRuntime.swift @@ -0,0 +1,140 @@ +import UIKit +internal import React +internal import React_RCTAppDelegate +internal import ReactAppDependencyProvider + +#if canImport(Expo) +internal import Expo + +final class ExpoHostRuntime { + static let shared = ExpoHostRuntime() + + private let jsBundleLoadObserver = JSBundleLoadObserver() + private var delegate = ExpoHostRuntimeDelegate() + private var reactNativeFactory: RCTReactNativeFactory? + private var expoDelegate: ExpoAppDelegate? + + /** + * Starts React Native with default parameters. + */ + public func startReactNative() { + startReactNative(onBundleLoaded: nil) + } + /** + * Starts React Native with optional callback when bundle is loaded. + * + * @param onBundleLoaded Optional callback invoked after JS bundle is fully loaded. + */ + public func startReactNative(onBundleLoaded: (() -> Void)?) { + guard reactNativeFactory == nil else { return } + + let factory = ExpoReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeFactory = factory + + let appDelegate = ExpoAppDelegate() + appDelegate.bindReactNativeFactory(factory) + expoDelegate = appDelegate + + if let onBundleLoaded { + jsBundleLoadObserver.observeOnce(onBundleLoaded: onBundleLoaded) + } + } + + /** + * Path to JavaScript root. + * Default value: ".expo/.virtual-metro-entry" + */ + public var entryFile: String = ".expo/.virtual-metro-entry" { + didSet { + delegate.entryFile = entryFile + } + } + + /** + * Path to JavaScript bundle file. + * Default value: "main.jsbundle" + */ + public var bundlePath: String = "main.jsbundle" { + didSet { + delegate.bundlePath = bundlePath + } + } + /** + * Bundle instance to lookup the JavaScript bundle. + * Default value: Bundle.main + */ + public var bundle: Bundle = Bundle.main { + didSet { + delegate.bundle = bundle + } + } + /** + * Dynamic bundle URL provider called on every bundle load. + * When set, this overrides the default bundleURL() behavior in the delegate. + * Returns a URL to load a custom bundle, or nil to use default behavior. + * Default value: nil + */ + public var bundleURLOverride: (() -> URL?)? = nil { + didSet { + delegate.bundleURLOverride = bundleURLOverride + } + } + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + return expoDelegate?.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) != nil + } + + func view( + moduleName: String, + initialProps: [AnyHashable: Any]?, + launchOptions: [AnyHashable: Any]? + ) -> UIView? { + let bundleURL = delegate.bundleURL() + + return expoDelegate?.recreateRootView( + withBundleURL: bundleURL, + moduleName: moduleName, + initialProps: initialProps, + launchOptions: launchOptions + ) + } +} + +class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate { + var entryFile = ".expo/.virtual-metro-entry" + var bundlePath = "main.jsbundle" + var bundle = Bundle.main + var bundleURLOverride: (() -> URL?)? = nil + + override func sourceURL(for bridge: RCTBridge) -> URL? { + // needed to return the correct URL for expo-dev-client. + bridge.bundleURL ?? bundleURL() + } + + override func bundleURL() -> URL? { + if let bundleURLProvider = bundleURLOverride { return bundleURLProvider() } +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL( + forBundleRoot: entryFile) +#else + do { + let (resourceName, fileExtension) = try BrownfieldBundlePathResolver.resourceComponents( + from: bundlePath + ) + return bundle.url(forResource: resourceName, withExtension: fileExtension) + } catch { + assertionFailure("Invalid bundlePath '\(bundlePath)': \(error)") + return nil + } +#endif + } +} +#endif diff --git a/packages/react-native-brownfield/ios/JSBundleLoadObserver.swift b/packages/react-native-brownfield/ios/JSBundleLoadObserver.swift new file mode 100644 index 00000000..7c69fe21 --- /dev/null +++ b/packages/react-native-brownfield/ios/JSBundleLoadObserver.swift @@ -0,0 +1,38 @@ +import Foundation +internal import React + +final class JSBundleLoadObserver { + private var onBundleLoaded: (() -> Void)? + private var observerToken: NSObjectProtocol? + + func observeOnce(onBundleLoaded: @escaping () -> Void) { + removeObserverIfNeeded() + self.onBundleLoaded = onBundleLoaded + + observerToken = NotificationCenter.default.addObserver( + forName: NSNotification.Name("RCTInstanceDidLoadBundle"), + object: nil, + queue: nil + ) { [weak self] _ in + self?.notifyAndClear() + } + } + + deinit { + removeObserverIfNeeded() + } + + private func notifyAndClear() { + let callback = onBundleLoaded + onBundleLoaded = nil + removeObserverIfNeeded() + callback?() + } + + private func removeObserverIfNeeded() { + if let observerToken { + NotificationCenter.default.removeObserver(observerToken) + self.observerToken = nil + } + } +} diff --git a/packages/react-native-brownfield/ios/Notification+Brownfield.swift b/packages/react-native-brownfield/ios/Notification+Brownfield.swift new file mode 100644 index 00000000..cb167bde --- /dev/null +++ b/packages/react-native-brownfield/ios/Notification+Brownfield.swift @@ -0,0 +1,12 @@ +import Foundation + +extension Notification.Name { + /** + * Notification sent when React Native wants to navigate back to native screen. + */ + public static let popToNative = Notification.Name("PopToNativeNotification") + /** + * Notification sent to enable/disable the pop gesture recognizer. + */ + public static let togglePopGestureRecognizer = Notification.Name("TogglePopGestureRecognizerNotification") +} diff --git a/packages/react-native-brownfield/ios/ReactNativeBrownfield.swift b/packages/react-native-brownfield/ios/ReactNativeBrownfield.swift index 046e1cd9..0556de3f 100644 --- a/packages/react-native-brownfield/ios/ReactNativeBrownfield.swift +++ b/packages/react-native-brownfield/ios/ReactNativeBrownfield.swift @@ -1,74 +1,60 @@ import UIKit -internal import React -internal import React_RCTAppDelegate -internal import ReactAppDependencyProvider -class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { - var entryFile = "index" - var bundlePath = "main.jsbundle" - var bundle = Bundle.main - var bundleURLOverride: (() -> URL?)? = nil - // MARK: - RCTReactNativeFactoryDelegate Methods - - override func sourceURL(for bridge: RCTBridge) -> URL? { - return bundleURL() - } - - public override func bundleURL() -> URL? { - if let bundleURLProvider = bundleURLOverride { - return bundleURLProvider() - } - -#if DEBUG - return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: entryFile) -#else - let resourceURLComponents = bundlePath.components(separatedBy: ".") - let withoutLast = resourceURLComponents[..<(resourceURLComponents.count - 1)] - let resourceName = withoutLast.joined() - let fileExtension = resourceURLComponents.last ?? "" - - return bundle.url(forResource: resourceName, withExtension: fileExtension) +#if canImport(Expo) +internal import Expo #endif - } -} @objc public class ReactNativeBrownfield: NSObject { public static let shared = ReactNativeBrownfield() - private var onBundleLoaded: (() -> Void)? - private var delegate = ReactNativeBrownfieldDelegate() - + /** * Path to JavaScript root. - * Default value: "index" + * Default value: "index" for bare React Native, ".expo/.virtual-metro-entry" when built with Expo. */ - @objc public var entryFile: String = "index" { + @objc public var entryFile: String = { + #if canImport(Expo) + return ".expo/.virtual-metro-entry" + #else + return "index" + #endif + }() { didSet { - delegate.entryFile = entryFile + #if canImport(Expo) + ExpoHostRuntime.shared.entryFile = entryFile + #else + ReactNativeHostRuntime.shared.entryFile = entryFile + #endif } } - /** - * Path to bundle fallback resource. - * Default value: nil - */ - @objc public var fallbackResource: String? = nil + /** * Path to JavaScript bundle file. * Default value: "main.jsbundle" */ @objc public var bundlePath: String = "main.jsbundle" { didSet { - delegate.bundlePath = bundlePath + #if canImport(Expo) + ExpoHostRuntime.shared.bundlePath = bundlePath + #else + ReactNativeHostRuntime.shared.bundlePath = bundlePath + #endif } } + /** * Bundle instance to lookup the JavaScript bundle. * Default value: Bundle.main */ - @objc public var bundle: Bundle = Bundle.main { + @objc public var bundle: Bundle = .main { didSet { - delegate.bundle = bundle + #if canImport(Expo) + ExpoHostRuntime.shared.bundle = bundle + #else + ReactNativeHostRuntime.shared.bundle = bundle + #endif } } + /** * Dynamic bundle URL provider called on every bundle load. * When set, this overrides the default bundleURL() behavior in the delegate. @@ -77,95 +63,75 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { */ @objc public var bundleURLOverride: (() -> URL?)? = nil { didSet { - delegate.bundleURLOverride = bundleURLOverride + #if canImport(Expo) + ExpoHostRuntime.shared.bundleURLOverride = bundleURLOverride + #else + ReactNativeHostRuntime.shared.bundleURLOverride = bundleURLOverride + #endif } } - /** - * React Native factory instance created when starting React Native. - * Default value: nil - */ - private var reactNativeFactory: RCTReactNativeFactory? = nil - /** - * Root view factory used to create React Native views. - */ - lazy private var rootViewFactory: RCTRootViewFactory? = { - return reactNativeFactory?.rootViewFactory - }() - + /** * Starts React Native with default parameters. */ @objc public func startReactNative() { - startReactNative(onBundleLoaded: nil) + #if canImport(Expo) + ExpoHostRuntime.shared.startReactNative() + #else + ReactNativeHostRuntime.shared.startReactNative() + #endif } - + @objc public func view( moduleName: String, initialProps: [AnyHashable: Any]?, launchOptions: [AnyHashable: Any]? = nil ) -> UIView? { - rootViewFactory?.view( - withModuleName: moduleName, - initialProperties: initialProps, + #if canImport(Expo) + ExpoHostRuntime.shared.view( + moduleName: moduleName, + initialProps: initialProps, + launchOptions: launchOptions + ) + #else + ReactNativeHostRuntime.shared.view( + moduleName: moduleName, + initialProps: initialProps, launchOptions: launchOptions ) + #endif } - + /** - * Starts React Native with optional callback when bundle is loaded. - * - * @param onBundleLoaded Optional callback invoked after JS bundle is fully loaded. + * Mirrors the host runtime app delegate API, forwarding to Expo or bare React Native as appropriate. */ - @objc public func startReactNative(onBundleLoaded: (() -> Void)?) { - startReactNative(onBundleLoaded: onBundleLoaded, launchOptions: nil) + @objc public func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + #if canImport(Expo) + return ExpoHostRuntime.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) + #else + return ReactNativeHostRuntime.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) + #endif } - + /** - * Starts React Native with optional callback and launch options. + * Starts React Native with optional callback when bundle is loaded. * * @param onBundleLoaded Optional callback invoked after JS bundle is fully loaded. - * @param launchOptions Launch options, typically passed from AppDelegate. */ - @objc public func startReactNative(onBundleLoaded: (() -> Void)?, launchOptions: [AnyHashable: Any]?) { - guard reactNativeFactory == nil else { return } - - delegate.dependencyProvider = RCTAppDependencyProvider() - self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate) - - if let onBundleLoaded { - self.onBundleLoaded = onBundleLoaded - if RCTIsNewArchEnabled() { - NotificationCenter.default.addObserver( - self, - selector: #selector(jsLoaded), - name: NSNotification.Name("RCTInstanceDidLoadBundle"), - object: nil - ) - } else { - NotificationCenter.default.addObserver( - self, - selector: #selector(jsLoaded), - name: NSNotification.Name("RCTJavaScriptDidLoadNotification"), - object: nil - ) - } - } - } - - @objc private func jsLoaded(_ notification: Notification) { - onBundleLoaded?() - onBundleLoaded = nil - NotificationCenter.default.removeObserver(self) + @objc public func startReactNative(onBundleLoaded: (() -> Void)?) { + #if canImport(Expo) + ExpoHostRuntime.shared.startReactNative(onBundleLoaded: onBundleLoaded) + #else + ReactNativeHostRuntime.shared.startReactNative(onBundleLoaded: onBundleLoaded) + #endif } } - -extension Notification.Name { - /** - * Notification sent when React Native wants to navigate back to native screen. - */ - public static let popToNative = Notification.Name("PopToNativeNotification") - /** - * Notification sent to enable/disable the pop gesture recognizer. - */ - public static let togglePopGestureRecognizer = Notification.Name("TogglePopGestureRecognizerNotification") -} diff --git a/packages/react-native-brownfield/ios/ReactNativeHostRuntime.swift b/packages/react-native-brownfield/ios/ReactNativeHostRuntime.swift new file mode 100644 index 00000000..f6715a81 --- /dev/null +++ b/packages/react-native-brownfield/ios/ReactNativeHostRuntime.swift @@ -0,0 +1,138 @@ +import UIKit +internal import React +internal import React_RCTAppDelegate +internal import ReactAppDependencyProvider + +class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { + var entryFile = "index" + var bundlePath = "main.jsbundle" + var bundle = Bundle.main + var bundleURLOverride: (() -> URL?)? = nil + // MARK: - RCTReactNativeFactoryDelegate Methods + + override func sourceURL(for bridge: RCTBridge) -> URL? { + return bundleURL() + } + + public override func bundleURL() -> URL? { + if let bundleURLProvider = bundleURLOverride { + return bundleURLProvider() + } + +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: entryFile) +#else + do { + let (resourceName, fileExtension) = try BrownfieldBundlePathResolver.resourceComponents( + from: bundlePath + ) + return bundle.url(forResource: resourceName, withExtension: fileExtension) + } catch { + assertionFailure("Invalid bundlePath '\(bundlePath)': \(error)") + return nil + } +#endif + } +} + +final class ReactNativeHostRuntime { + public static let shared = ReactNativeHostRuntime() + private let jsBundleLoadObserver = JSBundleLoadObserver() + private var delegate = ReactNativeBrownfieldDelegate() + + /** + * Path to JavaScript root. + * Default value: "index" + */ + public var entryFile: String = "index" { + didSet { + delegate.entryFile = entryFile + } + } + + /** + * Path to JavaScript bundle file. + * Default value: "main.jsbundle" + */ + public var bundlePath: String = "main.jsbundle" { + didSet { + delegate.bundlePath = bundlePath + } + } + /** + * Bundle instance to lookup the JavaScript bundle. + * Default value: Bundle.main + */ + public var bundle: Bundle = Bundle.main { + didSet { + delegate.bundle = bundle + } + } + /** + * Dynamic bundle URL provider called on every bundle load. + * When set, this overrides the default bundleURL() behavior in the delegate. + * Returns a URL to load a custom bundle, or nil to use default behavior. + * Default value: nil + */ + public var bundleURLOverride: (() -> URL?)? = nil { + didSet { + delegate.bundleURLOverride = bundleURLOverride + } + } + /** + * React Native factory instance created when starting React Native. + * Default value: nil + */ + private var reactNativeFactory: RCTReactNativeFactory? = nil + /** + * Root view factory used to create React Native views. + */ + lazy private var rootViewFactory: RCTRootViewFactory? = { + return reactNativeFactory?.rootViewFactory + }() + + /** + * Starts React Native with default parameters. + */ + public func startReactNative() { + startReactNative(onBundleLoaded: nil) + } + + public func view( + moduleName: String, + initialProps: [AnyHashable: Any]?, + launchOptions: [AnyHashable: Any]? = nil + ) -> UIView? { + rootViewFactory?.view( + withModuleName: moduleName, + initialProperties: initialProps, + launchOptions: launchOptions + ) + } + + /** + * Mirrors host manager app delegate API for bare React Native. + */ + public func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + return true + } + + /** + * Starts React Native with optional callback when bundle is loaded. + * + * @param onBundleLoaded Optional callback invoked after JS bundle is fully loaded. + */ + public func startReactNative(onBundleLoaded: (() -> Void)?) { + guard reactNativeFactory == nil else { return } + + delegate.dependencyProvider = RCTAppDependencyProvider() + self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate) + + if let onBundleLoaded { + jsBundleLoadObserver.observeOnce(onBundleLoaded: onBundleLoaded) + } + } +} diff --git a/packages/react-native-brownfield/src/expo-config-plugin/ios/withIosFrameworkFiles.ts b/packages/react-native-brownfield/src/expo-config-plugin/ios/withIosFrameworkFiles.ts index 58a3c7e9..46823be4 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +++ b/packages/react-native-brownfield/src/expo-config-plugin/ios/withIosFrameworkFiles.ts @@ -29,10 +29,6 @@ export function getFrameworkSourceFiles( '{{BUNDLE_IDENTIFIER}}': ios.bundleIdentifier, }), }, - { - relativePath: 'ReactNativeHostManager.swift', - content: renderTemplate('ios', 'ReactNativeHostManager.swift', {}), - }, ]; } diff --git a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/FrameworkInterface.swift b/packages/react-native-brownfield/src/expo-config-plugin/template/ios/FrameworkInterface.swift index 72a95566..9cec220c 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/FrameworkInterface.swift +++ b/packages/react-native-brownfield/src/expo-config-plugin/template/ios/FrameworkInterface.swift @@ -6,3 +6,9 @@ public let ReactNativeBundle = Bundle(for: InternalClassForBundle.self) class InternalClassForBundle {} + +extension ReactNativeBrownfield { + public func ensureExpoModulesProvider() { + let _ = ExpoModulesProvider() + } +} diff --git a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/PodfileTargetBlock.rb b/packages/react-native-brownfield/src/expo-config-plugin/template/ios/PodfileTargetBlock.rb index 315e9d29..68f64d12 100644 --- a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/PodfileTargetBlock.rb +++ b/packages/react-native-brownfield/src/expo-config-plugin/template/ios/PodfileTargetBlock.rb @@ -1,4 +1,5 @@ # Brownfield framework target for packaging as XCFramework target '{{FRAMEWORK_NAME}}' do + ENV['REACT_NATIVE_BROWNFIELD_USE_EXPO_HOST'] = '1' inherit! :complete end diff --git a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/ReactNativeHostManager.swift b/packages/react-native-brownfield/src/expo-config-plugin/template/ios/ReactNativeHostManager.swift deleted file mode 100644 index 1391f3cd..00000000 --- a/packages/react-native-brownfield/src/expo-config-plugin/template/ios/ReactNativeHostManager.swift +++ /dev/null @@ -1,63 +0,0 @@ -internal import Expo -internal import React -internal import ReactAppDependencyProvider -internal import React_RCTAppDelegate -import UIKit - -public class ReactNativeHostManager { - public static let shared = ReactNativeHostManager() - - private var reactNativeDelegate: ExpoReactNativeFactoryDelegate? - private var reactNativeFactory: RCTReactNativeFactory? - private var expoDelegate: ExpoAppDelegate? - - public func initialize() { - let delegate = ReactNativeDelegate() - let factory = ExpoReactNativeFactory(delegate: delegate) - delegate.dependencyProvider = RCTAppDependencyProvider() - - reactNativeDelegate = delegate - reactNativeFactory = factory - - expoDelegate = ExpoAppDelegate() - expoDelegate?.bindReactNativeFactory(factory) - - // required to avoid this being file be stripped by the swift compiler - let _ = ExpoModulesProvider() - } - - // propagate delegate methods to ExpoAppDelegate - public func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - ((expoDelegate?.application(application, didFinishLaunchingWithOptions: launchOptions)) != nil) - } - - // load & present RN UI - public func loadView( - moduleName: String, initialProps: [AnyHashable: Any]?, - launchOptions: [AnyHashable: Any]? - ) -> UIView { - let bundleURL = reactNativeDelegate?.bundleURL() - return (expoDelegate?.recreateRootView( - withBundleURL: bundleURL, moduleName: moduleName, initialProps: initialProps, - launchOptions: launchOptions))! - } -} - -class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { - override func sourceURL(for bridge: RCTBridge) -> URL? { - // needed to return the correct URL for expo-dev-client. - bridge.bundleURL ?? bundleURL() - } - - override func bundleURL() -> URL? { -#if DEBUG - return RCTBundleURLProvider.sharedSettings().jsBundleURL( - forBundleRoot: ".expo/.virtual-metro-entry") -#else - return ReactNativeBundle.url(forResource: "main", withExtension: "jsbundle") -#endif - } -}