Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/OBJECTIVE_C.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ Examples:
}, launchOptions];
```

`stopReactNative`

Stops React Native and releases the underlying runtime. Safe to call multiple times. Call it after all React Native views are dismissed.

Examples:

```objc
[[ReactNativeBrownfield shared] stopReactNative];
```

`view`

Creates a React Native view for the specified module name.
Expand Down
10 changes: 10 additions & 0 deletions docs/SWIFT.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ ReactNativeBrownfield.shared.startReactNative(onBundleLoaded: {
}, launchOptions: launchOptions)
```

`stopReactNative`

Stops React Native and releases the underlying runtime. Safe to call multiple times. Call it after all React Native views are dismissed.

Examples:

```swift
ReactNativeBrownfield.shared.stopReactNative()
```

`view`

Creates a React Native view for the specified module name.
Expand Down
34 changes: 21 additions & 13 deletions example/swift/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,27 @@ struct MyApp: App {
}

struct ContentView: View {
var body: some View {
var body: some View {
NavigationView {
VStack {
Text("React Native Brownfield App")
.font(.title)
.bold()
.padding()

NavigationLink("Push React Native Screen") {
ReactNativeView(moduleName: "ReactNative")
.navigationBarHidden(true)
}
}
}.navigationViewStyle(StackNavigationViewStyle())
VStack {
Text("React Native Brownfield App")
.font(.title)
.bold()
.padding()
.multilineTextAlignment(.center)

NavigationLink("Push React Native Screen") {
ReactNativeView(moduleName: "ReactNative")
.navigationBarHidden(true)
}

Button("Stop React Native") {
ReactNativeBrownfield.shared.stopReactNative()
}
.buttonStyle(PlainButtonStyle())
.padding(.top)
.foregroundColor(.red)
}
}
}
}
6 changes: 3 additions & 3 deletions example/swift/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2322,7 +2322,7 @@ PODS:
- SocketRocket
- ReactAppDependencyProvider (0.82.1):
- ReactCodegen
- ReactBrownfield (1.2.0):
- ReactBrownfield (2.0.1):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2811,12 +2811,12 @@ SPEC CHECKSUMS:
React-utils: abf37b162f560cd0e3e5d037af30bb796512246d
React-webperformancenativemodule: 50a57c713a90d27ae3ab947a6c9c8859bcb49709
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
ReactBrownfield: ba90b7c2be36c3ef4ad47e777610ca7c2b0fdf06
ReactBrownfield: 10f9f7370cd8bd6ef30eb9c736d774f9d17565f7
ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9
ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424
RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 8e01cef9947ca77f0477a098f0b32848a8e448c6
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb

PODFILE CHECKSUM: c4add71d30d7b14523f41a732fbf4937f4edbe0f

Expand Down
52 changes: 40 additions & 12 deletions ios/ReactNativeBrownfield.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
public static let shared = ReactNativeBrownfield()
private var onBundleLoaded: (() -> Void)?
private var delegate = ReactNativeBrownfieldDelegate()
private var storedLaunchOptions: [AnyHashable: Any]?

/**
* Path to JavaScript root.
Expand Down Expand Up @@ -69,13 +70,16 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
* 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
}()

private var factory: RCTReactNativeFactory {
if let existingFactory = reactNativeFactory {
return existingFactory
}

delegate.dependencyProvider = RCTAppDependencyProvider()
let createdFactory = RCTReactNativeFactory(delegate: delegate)
reactNativeFactory = createdFactory
return createdFactory
}
/**
* Starts React Native with default parameters.
*/
Expand All @@ -88,10 +92,15 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
initialProps: [AnyHashable: Any]?,
launchOptions: [AnyHashable: Any]? = nil
) -> UIView? {
rootViewFactory?.view(
let resolvedFactory = factory

let rootViewFactory = resolvedFactory.rootViewFactory
let resolvedLaunchOptions = launchOptions ?? storedLaunchOptions

return rootViewFactory.view(
withModuleName: moduleName,
initialProperties: initialProps,
launchOptions: launchOptions
launchOptions: resolvedLaunchOptions
)
}

Expand All @@ -111,10 +120,9 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
* @param launchOptions Launch options, typically passed from AppDelegate.
*/
@objc public func startReactNative(onBundleLoaded: (() -> Void)?, launchOptions: [AnyHashable: Any]?) {
storedLaunchOptions = launchOptions
Copy link

Choose a reason for hiding this comment

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

Bug: Late Option Updates Cause React Native Inconsistency

The storedLaunchOptions assignment happens before the guard check that returns early if reactNativeFactory already exists. This allows launch options to be modified after React Native initialization, potentially causing inconsistent state when view() uses the updated options with an already-initialized factory.

Fix in Cursor Fix in Web

guard reactNativeFactory == nil else { return }

delegate.dependencyProvider = RCTAppDependencyProvider()
self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate)
_ = factory

if let onBundleLoaded {
self.onBundleLoaded = onBundleLoaded
Expand All @@ -136,6 +144,26 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
}
}

/**
* Stops React Native and releases the underlying factory instance.
*/
@objc public func stopReactNative() {
if !Thread.isMainThread {
DispatchQueue.main.async { [weak self] in self?.stopReactNative() }
return
}

guard let factory = reactNativeFactory else { return }

factory.bridge?.invalidate()

NotificationCenter.default.removeObserver(self)
onBundleLoaded = nil

storedLaunchOptions = nil
reactNativeFactory = nil
}

@objc private func jsLoaded(_ notification: Notification) {
onBundleLoaded?()
onBundleLoaded = nil
Expand Down