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
- }
-}