From ef13353cc6e400b76eb03be1c1ae2f9e9208f346 Mon Sep 17 00:00:00 2001 From: Nazar Sydiaha Date: Fri, 14 Nov 2025 18:51:09 +0100 Subject: [PATCH 1/4] feat: add stopReactNative method --- docs/OBJECTIVE_C.md | 10 ++++++++ docs/SWIFT.md | 10 ++++++++ ios/ReactNativeBrownfield.swift | 42 +++++++++++++++++++++++++++------ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/docs/OBJECTIVE_C.md b/docs/OBJECTIVE_C.md index 8e05779..5290a79 100644 --- a/docs/OBJECTIVE_C.md +++ b/docs/OBJECTIVE_C.md @@ -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. diff --git a/docs/SWIFT.md b/docs/SWIFT.md index 0668161..20fcb5a 100644 --- a/docs/SWIFT.md +++ b/docs/SWIFT.md @@ -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. diff --git a/ios/ReactNativeBrownfield.swift b/ios/ReactNativeBrownfield.swift index d52b5b2..0afdcdb 100644 --- a/ios/ReactNativeBrownfield.swift +++ b/ios/ReactNativeBrownfield.swift @@ -32,6 +32,13 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { private var onBundleLoaded: (() -> Void)? private var delegate = ReactNativeBrownfieldDelegate() + private func checkFactoryInitialized(launchOptions: [AnyHashable: Any]? = nil) { + if reactNativeFactory == nil { + delegate.dependencyProvider = RCTAppDependencyProvider() + self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate) + } + } + /** * Path to JavaScript root. * Default value: "index" @@ -71,10 +78,12 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { private var reactNativeFactory: RCTReactNativeFactory? = nil /** * Root view factory used to create React Native views. + * Always proxies the currently active React Native factory so restarting + * React Native yields a fresh root view factory instance. */ - lazy private var rootViewFactory: RCTRootViewFactory? = { - return reactNativeFactory?.rootViewFactory - }() + private var rootViewFactory: RCTRootViewFactory? { + reactNativeFactory?.rootViewFactory + } /** * Starts React Native with default parameters. @@ -88,7 +97,9 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { initialProps: [AnyHashable: Any]?, launchOptions: [AnyHashable: Any]? = nil ) -> UIView? { - rootViewFactory?.view( + checkFactoryInitialized(launchOptions: launchOptions) + + return rootViewFactory?.view( withModuleName: moduleName, initialProperties: initialProps, launchOptions: launchOptions @@ -112,9 +123,7 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { */ @objc public func startReactNative(onBundleLoaded: (() -> Void)?, launchOptions: [AnyHashable: Any]?) { guard reactNativeFactory == nil else { return } - - delegate.dependencyProvider = RCTAppDependencyProvider() - self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate) + checkFactoryInitialized(launchOptions: launchOptions) if let onBundleLoaded { self.onBundleLoaded = onBundleLoaded @@ -136,6 +145,25 @@ 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 + + reactNativeFactory = nil + } + @objc private func jsLoaded(_ notification: Notification) { onBundleLoaded?() onBundleLoaded = nil From 07451b6cc6f96394cd027043e24cb45b31b75fc1 Mon Sep 17 00:00:00 2001 From: Nazar Sydiaha Date: Tue, 9 Sep 2025 17:33:41 +0200 Subject: [PATCH 2/4] feat: add stopReactNative usage example --- example/swift/App.swift | 32 ++++++++++++++++++++------------ example/swift/Podfile.lock | 6 +++--- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/example/swift/App.swift b/example/swift/App.swift index 52ea537..f218d33 100644 --- a/example/swift/App.swift +++ b/example/swift/App.swift @@ -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) - } + 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() } - }.navigationViewStyle(StackNavigationViewStyle()) + .buttonStyle(PlainButtonStyle()) + .padding(.top) + .foregroundColor(.red) + } + } } } diff --git a/example/swift/Podfile.lock b/example/swift/Podfile.lock index a2f6d06..0058825 100644 --- a/example/swift/Podfile.lock +++ b/example/swift/Podfile.lock @@ -2322,7 +2322,7 @@ PODS: - SocketRocket - ReactAppDependencyProvider (0.82.1): - ReactCodegen - - ReactBrownfield (1.2.0): + - ReactBrownfield (2.0.1): - boost - DoubleConversion - fast_float @@ -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 From aab133981ccb4ab8b8d7e16ef8f55c68ae766049 Mon Sep 17 00:00:00 2001 From: Nazar Sydiaha Date: Fri, 14 Nov 2025 19:29:08 +0100 Subject: [PATCH 3/4] chore: refactor factory initializer --- example/swift/App.swift | 38 ++++++++++++++++----------------- ios/ReactNativeBrownfield.swift | 35 ++++++++++++++---------------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/example/swift/App.swift b/example/swift/App.swift index f218d33..83d194d 100644 --- a/example/swift/App.swift +++ b/example/swift/App.swift @@ -19,25 +19,25 @@ struct MyApp: App { struct ContentView: View { var body: some View { NavigationView { - 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) - } + 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) + } } } } diff --git a/ios/ReactNativeBrownfield.swift b/ios/ReactNativeBrownfield.swift index 0afdcdb..75a17d7 100644 --- a/ios/ReactNativeBrownfield.swift +++ b/ios/ReactNativeBrownfield.swift @@ -32,13 +32,6 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { private var onBundleLoaded: (() -> Void)? private var delegate = ReactNativeBrownfieldDelegate() - private func checkFactoryInitialized(launchOptions: [AnyHashable: Any]? = nil) { - if reactNativeFactory == nil { - delegate.dependencyProvider = RCTAppDependencyProvider() - self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate) - } - } - /** * Path to JavaScript root. * Default value: "index" @@ -76,15 +69,16 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { * Default value: nil */ private var reactNativeFactory: RCTReactNativeFactory? = nil - /** - * Root view factory used to create React Native views. - * Always proxies the currently active React Native factory so restarting - * React Native yields a fresh root view factory instance. - */ - private var rootViewFactory: RCTRootViewFactory? { - 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. */ @@ -97,9 +91,11 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { initialProps: [AnyHashable: Any]?, launchOptions: [AnyHashable: Any]? = nil ) -> UIView? { - checkFactoryInitialized(launchOptions: launchOptions) - - return rootViewFactory?.view( + let resolvedFactory = factory + + let rootViewFactory = resolvedFactory.rootViewFactory + + return rootViewFactory.view( withModuleName: moduleName, initialProperties: initialProps, launchOptions: launchOptions @@ -123,7 +119,8 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { */ @objc public func startReactNative(onBundleLoaded: (() -> Void)?, launchOptions: [AnyHashable: Any]?) { guard reactNativeFactory == nil else { return } - checkFactoryInitialized(launchOptions: launchOptions) + _ = launchOptions + _ = factory if let onBundleLoaded { self.onBundleLoaded = onBundleLoaded From 38bb35e70c834a280dcaf973021c1fe478013246 Mon Sep 17 00:00:00 2001 From: Nazar Sydiaha Date: Fri, 14 Nov 2025 19:43:20 +0100 Subject: [PATCH 4/4] chore: add storedLaunchOptions handler --- ios/ReactNativeBrownfield.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ios/ReactNativeBrownfield.swift b/ios/ReactNativeBrownfield.swift index 75a17d7..4034850 100644 --- a/ios/ReactNativeBrownfield.swift +++ b/ios/ReactNativeBrownfield.swift @@ -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. @@ -94,11 +95,12 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { let resolvedFactory = factory let rootViewFactory = resolvedFactory.rootViewFactory + let resolvedLaunchOptions = launchOptions ?? storedLaunchOptions return rootViewFactory.view( withModuleName: moduleName, initialProperties: initialProps, - launchOptions: launchOptions + launchOptions: resolvedLaunchOptions ) } @@ -118,8 +120,8 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { * @param launchOptions Launch options, typically passed from AppDelegate. */ @objc public func startReactNative(onBundleLoaded: (() -> Void)?, launchOptions: [AnyHashable: Any]?) { + storedLaunchOptions = launchOptions guard reactNativeFactory == nil else { return } - _ = launchOptions _ = factory if let onBundleLoaded { @@ -158,6 +160,7 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { NotificationCenter.default.removeObserver(self) onBundleLoaded = nil + storedLaunchOptions = nil reactNativeFactory = nil }