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/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) } } } diff --git a/ios/ReactNativeBrownfield.swift b/ios/ReactNativeBrownfield.swift index da8b3c4..68a2e1a 100644 --- a/ios/ReactNativeBrownfield.swift +++ b/ios/ReactNativeBrownfield.swift @@ -69,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. @@ -88,7 +94,11 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate { initialProps: [AnyHashable: Any]?, launchOptions: [AnyHashable: Any]? = nil ) -> UIView? { - reactNativeFactory?.rootViewFactory.view( + let resolvedFactory = factory + + let rootViewFactory = resolvedFactory.rootViewFactory + + return rootViewFactory.view( withModuleName: moduleName, initialProperties: initialProps, launchOptions: launchOptions @@ -111,29 +121,54 @@ 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 } - - delegate.dependencyProvider = RCTAppDependencyProvider() - self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate) + 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 + } + + /** + * 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 { + hasStartedReactNative = false + return + } + + factory.bridge?.invalidate() + + NotificationCenter.default.removeObserver(self) + onBundleLoaded = nil + + hasStartedReactNative = false + reactNativeFactory = nil } @objc private func jsLoaded(_ notification: Notification) {