From 04b875c3a90756e9d113f20e06a61b7d3cdb1d94 Mon Sep 17 00:00:00 2001 From: Nazar Sydiaha Date: Tue, 9 Sep 2025 17:32:50 +0200 Subject: [PATCH 1/3] feat: add stopReactNative method --- docs/OBJECTIVE_C.md | 10 ++++++++++ docs/SWIFT.md | 10 ++++++++++ ios/ReactNativeBrownfield.swift | 34 +++++++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 4 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 da8b3c4..cd2830e 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" @@ -88,7 +95,9 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { initialProps: [AnyHashable: Any]?, launchOptions: [AnyHashable: Any]? = nil ) -> UIView? { - reactNativeFactory?.rootViewFactory.view( + checkFactoryInitialized(launchOptions: launchOptions) + + return reactNativeFactory?.rootViewFactory.view( withModuleName: moduleName, initialProperties: initialProps, launchOptions: launchOptions @@ -112,9 +121,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 +143,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 9264071bddc59a55e2e0b67349f93956189eced0 Mon Sep 17 00:00:00 2001 From: Nazar Sydiaha Date: Tue, 9 Sep 2025 17:33:41 +0200 Subject: [PATCH 2/3] feat: add stopReactNative usage example --- example/swift/App.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example/swift/App.swift b/example/swift/App.swift index 4a76237..cb9e61e 100644 --- a/example/swift/App.swift +++ b/example/swift/App.swift @@ -24,11 +24,19 @@ struct ContentView: View { .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) } } } From 5bef4b4ba312f7c514f919d753e7fbead8ef39ab Mon Sep 17 00:00:00 2001 From: Nazar Sydiaha Date: Wed, 24 Sep 2025 20:56:20 +0200 Subject: [PATCH 3/3] chore: refactor factory initializer --- ios/ReactNativeBrownfield.swift | 75 ++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/ios/ReactNativeBrownfield.swift b/ios/ReactNativeBrownfield.swift index cd2830e..68a2e1a 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,12 +69,18 @@ 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 hasStartedReactNative = false + + 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. @@ -95,9 +94,11 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { initialProps: [AnyHashable: Any]?, launchOptions: [AnyHashable: Any]? = nil ) -> UIView? { - checkFactoryInitialized(launchOptions: launchOptions) - - return reactNativeFactory?.rootViewFactory.view( + let resolvedFactory = factory + + let rootViewFactory = resolvedFactory.rootViewFactory + + return rootViewFactory.view( withModuleName: moduleName, initialProperties: initialProps, launchOptions: launchOptions @@ -120,27 +121,31 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { * @param launchOptions Launch options, typically passed from AppDelegate. */ @objc public func startReactNative(onBundleLoaded: (() -> Void)?, launchOptions: [AnyHashable: Any]?) { - guard reactNativeFactory == nil else { return } - checkFactoryInitialized(launchOptions: launchOptions) + guard !hasStartedReactNative else { return } + _ = launchOptions + _ = factory 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 - ) + let notificationName: Notification.Name = RCTIsNewArchEnabled() + ? Notification.Name("RCTInstanceDidLoadBundle") + : Notification.Name("RCTJavaScriptDidLoadNotification") + + NotificationCenter.default.addObserver( + self, + selector: #selector(jsLoaded), + name: notificationName, + object: nil + ) + + if let bridge = reactNativeFactory?.bridge, !bridge.isLoading { + DispatchQueue.main.async { [weak self] in + self?.jsLoaded(Notification(name: notificationName)) + } } } + + hasStartedReactNative = true } /** @@ -152,13 +157,17 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { return } - guard let factory = reactNativeFactory else { return } + guard let factory = reactNativeFactory else { + hasStartedReactNative = false + return + } factory.bridge?.invalidate() NotificationCenter.default.removeObserver(self) onBundleLoaded = nil + hasStartedReactNative = false reactNativeFactory = nil }