Skip to content

Commit 721df6e

Browse files
committed
docs: update
1 parent 171dc28 commit 721df6e

5 files changed

Lines changed: 177 additions & 37 deletions

File tree

apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import BrownfieldNavigation
77

88
class AppDelegate: NSObject, UIApplicationDelegate {
99
var window: UIWindow?
10-
private let navigationDelegate = RNNavigationDelegate()
1110

1211
func application(
1312
_ application: UIApplication,
@@ -23,18 +22,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
2322
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
2423
return ReactNativeBrownfield.shared.application(application, willFinishLaunchingWithOptions: launchOptions)
2524
}
26-
27-
func applicationDidBecomeActive(_ application: UIApplication) {
28-
// Reclaim delegate ownership before RN can issue navigation requests.
29-
BrownfieldNavigationManager.shared.setDelegate(
30-
navigationDelegate: navigationDelegate
31-
)
32-
}
33-
34-
func applicationWillResignActive(_ application: UIApplication) {
35-
// Release the delegate so another native host can register cleanly.
36-
BrownfieldNavigationManager.shared.clearDelegate()
37-
}
3825
}
3926

4027
public class RNNavigationDelegate: BrownfieldNavigationDelegate {
@@ -105,7 +92,44 @@ struct BrownfieldAppleApp: App {
10592

10693
var body: some Scene {
10794
WindowGroup {
108-
ContentView()
95+
RootContentView(appDelegate: appDelegate)
96+
}
97+
}
98+
}
99+
100+
private struct RootContentView: View {
101+
@Environment(\.scenePhase) private var scenePhase
102+
103+
let appDelegate: AppDelegate
104+
105+
var body: some View {
106+
ContentView()
107+
.onAppear {
108+
syncNavigationDelegate(for: scenePhase)
109+
}
110+
.onChange(of: scenePhase) { newPhase in
111+
syncNavigationDelegate(for: newPhase)
112+
}
113+
}
114+
115+
private func registerNavigationDelegate() {
116+
BrownfieldNavigationManager.shared.setDelegate(
117+
navigationDelegate: RNNavigationDelegate()
118+
)
119+
}
120+
121+
private func clearNavigationDelegate() {
122+
BrownfieldNavigationManager.shared.clearDelegate()
123+
}
124+
125+
private func syncNavigationDelegate(for phase: ScenePhase) {
126+
switch phase {
127+
case .active:
128+
registerNavigationDelegate()
129+
case .inactive, .background:
130+
clearNavigationDelegate()
131+
@unknown default:
132+
clearNavigationDelegate()
109133
}
110134
}
111135
}

apps/AppleApp/Brownfield Apple App/components/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct ContentView: View {
2424
MessagesView()
2525

2626
ReactNativeView(
27-
moduleName: "main",
27+
moduleName: "RNApp",
2828
initialProperties: [
2929
"nativeOsVersionLabel":
3030
"\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)"

docs/docs/docs/api-reference/brownfield-navigation/native-integration.mdx

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Native Integration
22

3-
After codegen, implement the generated delegate interface in your host app and register it before JavaScript uses the module.
3+
After codegen, implement the generated delegate interface in your host app and register it before JavaScript uses the module. Clear that delegate when the host stops owning Brownfield navigation so another native owner can safely register.
4+
5+
On both platforms, the native manager API is intentionally explicit: use `setDelegate(...)` to claim ownership, `clearDelegate()` to release it, and rely on `getDelegate()` failing fast if no active host has registered yet.
6+
7+
Do not treat the delegate as an app-lifetime singleton unless your app truly has only one native owner for Brownfield navigation. The intended contract is ownership-based: register when a host becomes responsible for Brownfield navigation, then clear when that responsibility moves elsewhere or the host becomes inactive.
48

59
## Pre-Requisite:
610

@@ -31,21 +35,26 @@ class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
3135
}
3236
```
3337

34-
### 2) Register the delegate during startup
38+
### 2) Register and clear the delegate with host lifecycle
3539

36-
Register before any React Native screen can call `BrownfieldNavigation.*`:
40+
Register before any React Native screen can call `BrownfieldNavigation.*`, and clear the delegate when this host is no longer the active navigation owner:
3741

3842
```kotlin
39-
import android.os.Bundle
4043
import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationManager
4144

42-
override fun onCreate(savedInstanceState: Bundle?) {
43-
super.onCreate(savedInstanceState)
45+
override fun onResume() {
46+
super.onResume()
4447
BrownfieldNavigationManager.setDelegate(this)
45-
// Initialize React Native host
48+
}
49+
50+
override fun onPause() {
51+
BrownfieldNavigationManager.clearDelegate()
52+
super.onPause()
4653
}
4754
```
4855

56+
`BrownfieldNavigationManager.getDelegate()` still fails fast when no delegate is registered, so missing registration remains a host lifecycle bug rather than a silent no-op.
57+
4958
## iOS
5059

5160
### 1) Implement `BrownfieldNavigationDelegate`
@@ -74,30 +83,83 @@ public final class RNNavigationDelegate: BrownfieldNavigationDelegate {
7483
}
7584
```
7685

77-
### 2) Register the delegate at app startup
86+
### 2) Register and clear the delegate with host lifecycle
87+
88+
If your app can hand Brownfield navigation between multiple native owners, register in the object that currently owns navigation and clear it during the matching teardown phase.
89+
90+
In SwiftUI apps, prefer scene-driven lifecycle hooks such as `scenePhase`. In UIKit scene-based apps, use the matching `UISceneDelegate` callbacks. `UIApplicationDelegate` launch setup is still useful, but it is not always the right place to track foreground navigation ownership.
7891

7992
```swift
8093
import BrownfieldNavigation
94+
import SwiftUI
8195

82-
@main
83-
struct BrownfieldAppleApp: App {
84-
init() {
96+
final class AppDelegate: NSObject, UIApplicationDelegate {
97+
private let navigationDelegate = RNNavigationDelegate()
98+
99+
func registerNavigationDelegate() {
85100
BrownfieldNavigationManager.shared.setDelegate(
86-
navigationDelegate: RNNavigationDelegate()
101+
navigationDelegate: navigationDelegate
87102
)
88103
}
104+
105+
func clearNavigationDelegate() {
106+
BrownfieldNavigationManager.shared.clearDelegate()
107+
}
108+
}
109+
110+
@main
111+
struct BrownfieldAppleApp: App {
112+
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
113+
114+
var body: some Scene {
115+
WindowGroup {
116+
RootContentView(appDelegate: appDelegate)
117+
}
118+
}
119+
}
120+
121+
private struct RootContentView: View {
122+
@Environment(\.scenePhase) private var scenePhase
123+
124+
let appDelegate: AppDelegate
125+
126+
var body: some View {
127+
ContentView()
128+
.onAppear {
129+
syncNavigationDelegate(for: scenePhase)
130+
}
131+
.onChange(of: scenePhase) { newPhase in
132+
syncNavigationDelegate(for: newPhase)
133+
}
134+
}
135+
136+
private func syncNavigationDelegate(for phase: ScenePhase) {
137+
switch phase {
138+
case .active:
139+
appDelegate.registerNavigationDelegate()
140+
case .inactive, .background:
141+
appDelegate.clearNavigationDelegate()
142+
@unknown default:
143+
appDelegate.clearNavigationDelegate()
144+
}
145+
}
89146
}
90147
```
91148

149+
`BrownfieldNavigationManager.shared.getDelegate()` still fails fast when no delegate is registered, so lifecycle ownership bugs surface immediately during native integration.
150+
92151
## Lifecycle Requirements
93152

94153
- Register delegate before rendering JS that might call the module.
154+
- Clear delegate when that host stops owning Brownfield navigation; do not keep stale delegates registered for the full app lifetime by default.
95155
- Keep navigation on main/UI thread.
96-
- Re-register delegate if your host object is recreated.
97-
- Treat missing delegate as a startup bug: runtime calls require a registered delegate.
156+
- Re-register delegate if your host object is recreated or regains focus.
157+
- Treat missing delegate as a lifecycle bug: runtime calls require a registered delegate.
98158

99159
## Troubleshooting
100160

101161
- **Method added in TS but not visible natively**: rerun codegen and rebuild.
102162
- **Calls crash on app launch**: verify delegate registration happens before RN route rendering.
163+
- **Calls crash after switching hosts**: confirm the previous host clears the delegate and the active host re-registers it before RN can navigate.
164+
- **SwiftUI app never re-registers the delegate**: move ownership to `scenePhase` or `UISceneDelegate` instead of relying on `UIApplicationDelegate` active/resign callbacks.
103165
- **Wrong screen opens**: check native delegate method wiring and params mapping.

skills/brownfield-navigation/references/native-android-integration.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,31 @@
66
- Route each generated method to intended native destination
77
- Map params directly (for example through `Intent` extras)
88

9-
## Register delegate at startup
9+
## Register delegate with lifecycle ownership
1010

1111
- Call:
1212
- `BrownfieldNavigationManager.setDelegate(...)`
13-
- Register in startup flow (for example `onCreate`)
13+
- `BrownfieldNavigationManager.clearDelegate()` is part of the public API and should be called when this host stops owning navigation
14+
- Register in the lifecycle phase where this host becomes active (for example `onResume`)
15+
- Clear in the matching release phase (for example `onPause`)
1416
- Ensure registration happens before RN calls
17+
- Do not keep a backgrounded or inactive `Activity` registered for the full app lifetime unless it is truly the only Brownfield navigation owner
18+
- `BrownfieldNavigationManager.getDelegate()` still throws if no delegate is registered
1519

1620
## Minimal pattern
1721

1822
```kotlin
1923
class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
20-
override fun onCreate(savedInstanceState: Bundle?) {
21-
super.onCreate(savedInstanceState)
24+
override fun onResume() {
25+
super.onResume()
2226
BrownfieldNavigationManager.setDelegate(this)
2327
}
2428

29+
override fun onPause() {
30+
BrownfieldNavigationManager.clearDelegate()
31+
super.onPause()
32+
}
33+
2534
override fun openNativeProfile(userId: String) {
2635
val intent = Intent(this, ProfileActivity::class.java).apply {
2736
putExtra("userId", userId)

skills/brownfield-navigation/references/native-ios-integration.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,68 @@
66
- Route each generated method to intended UIKit/SwiftUI destination
77
- Execute presentation on main thread
88

9-
## Register delegate at startup
9+
## Register delegate with lifecycle ownership
1010

1111
- Call:
1212
- `BrownfieldNavigationManager.shared.setDelegate(navigationDelegate: ...)`
13+
- `BrownfieldNavigationManager.shared.clearDelegate()`
1314
- Register before RN screens that call Brownfield navigation are shown
14-
- Keep a strong delegate reference for app lifetime
15+
- Keep a strong delegate reference while this host owns Brownfield navigation
16+
- Clear the delegate when this host stops owning navigation
17+
- Do not keep the delegate registered for the full app lifetime by default; reassign ownership when another native host takes over
18+
- In SwiftUI apps, prefer `scenePhase` for foreground ownership; in UIKit scene-based apps, prefer `UISceneDelegate`
19+
- Do not rely on `UIApplicationDelegate` active/resign callbacks as the only ownership signal in SwiftUI
20+
- `BrownfieldNavigationManager.shared.getDelegate()` still traps if no delegate is registered
1521

1622
## Minimal pattern
1723

1824
```swift
25+
final class AppDelegate: NSObject, UIApplicationDelegate {
26+
private let navigationDelegate = AppNavigationDelegate()
27+
28+
func registerNavigationDelegate() {
29+
BrownfieldNavigationManager.shared.setDelegate(
30+
navigationDelegate: navigationDelegate
31+
)
32+
}
33+
34+
func clearNavigationDelegate() {
35+
BrownfieldNavigationManager.shared.clearDelegate()
36+
}
37+
}
38+
39+
struct RootContentView: View {
40+
@Environment(\.scenePhase) private var scenePhase
41+
42+
let appDelegate: AppDelegate
43+
44+
var body: some View {
45+
ContentView()
46+
.onAppear {
47+
syncNavigationDelegate(for: scenePhase)
48+
}
49+
.onChange(of: scenePhase) { newPhase in
50+
syncNavigationDelegate(for: newPhase)
51+
}
52+
}
53+
54+
private func syncNavigationDelegate(for phase: ScenePhase) {
55+
switch phase {
56+
case .active:
57+
appDelegate.registerNavigationDelegate()
58+
case .inactive, .background:
59+
appDelegate.clearNavigationDelegate()
60+
@unknown default:
61+
appDelegate.clearNavigationDelegate()
62+
}
63+
}
64+
}
65+
1966
final class AppNavigationDelegate: BrownfieldNavigationDelegate {
2067
func openNativeProfile(userId: String) {
2168
DispatchQueue.main.async {
2269
// present native screen
2370
}
2471
}
2572
}
26-
27-
BrownfieldNavigationManager.shared.setDelegate(navigationDelegate: navigationDelegate)
2873
```

0 commit comments

Comments
 (0)