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 52ea537..83d194d 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) - } - } - }.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) + } + } } } 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 diff --git a/ios/ReactNativeBrownfield.swift b/ios/ReactNativeBrownfield.swift index d52b5b2..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. @@ -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. */ @@ -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 ) } @@ -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 guard reactNativeFactory == nil else { return } - - delegate.dependencyProvider = RCTAppDependencyProvider() - self.reactNativeFactory = RCTReactNativeFactory(delegate: delegate) + _ = factory if let onBundleLoaded { self.onBundleLoaded = onBundleLoaded @@ -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