react-native-navigation 8.8.5 → 8.8.6-snapshot.2571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationActivity.java +14 -1
  2. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  3. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  9. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  10. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  11. package/android/src/main/java/com/reactnativenavigation/options/NavigationBarOptions.java +19 -1
  12. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  13. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  14. package/android/src/main/java/com/reactnativenavigation/utils/SystemUiUtils.kt +63 -9
  15. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  16. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +77 -6
  17. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +1 -0
  18. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentViewController.java +2 -5
  19. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/Presenter.java +33 -13
  20. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  21. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  22. package/android/src/test/java/com/reactnativenavigation/presentation/PresenterTest.java +14 -0
  23. package/android/src/test/java/com/reactnativenavigation/utils/SystemUiUtilsTest.kt +64 -1
  24. package/ios/ARCHITECTURE.md +5 -0
  25. package/ios/BottomTabPresenter.h +7 -0
  26. package/ios/BottomTabPresenter.mm +27 -0
  27. package/ios/RNNAppDelegate.h +16 -0
  28. package/ios/RNNAppDelegate.mm +73 -0
  29. package/ios/RNNBottomTabOptions.h +2 -0
  30. package/ios/RNNBottomTabOptions.mm +5 -1
  31. package/ios/RNNBottomTabsController.h +2 -0
  32. package/ios/RNNBottomTabsController.mm +209 -1
  33. package/ios/RNNBottomTabsCustomRow.h +57 -0
  34. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  35. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  36. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  37. package/ios/RNNBottomTabsOptions.h +2 -0
  38. package/ios/RNNBottomTabsOptions.mm +2 -0
  39. package/ios/RNNComponentViewCreator.h +2 -1
  40. package/ios/RNNCustomTabBarItemView.h +26 -0
  41. package/ios/RNNCustomTabBarItemView.mm +83 -0
  42. package/ios/RNNReactRootViewCreator.mm +1 -0
  43. package/ios/RNNViewControllerFactory.mm +1 -0
  44. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  45. package/lib/module/ARCHITECTURE.md +30 -0
  46. package/lib/module/Navigation.js +34 -1
  47. package/lib/module/Navigation.js.map +1 -1
  48. package/lib/module/NavigationDelegate.js +21 -0
  49. package/lib/module/NavigationDelegate.js.map +1 -1
  50. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  51. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  52. package/lib/module/commands/Commands.js +8 -0
  53. package/lib/module/commands/Commands.js.map +1 -1
  54. package/lib/module/index.js +1 -0
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/interfaces/Options.js.map +1 -1
  57. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  58. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  59. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  60. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  61. package/lib/module/linking/LinkingHandler.js +139 -0
  62. package/lib/module/linking/LinkingHandler.js.map +1 -0
  63. package/lib/module/linking/LinkingHandler.test.js +384 -0
  64. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  65. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  66. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  67. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  68. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  69. package/lib/module/linking/RouteMatcher.js +104 -0
  70. package/lib/module/linking/RouteMatcher.js.map +1 -0
  71. package/lib/module/linking/RouteMatcher.test.js +164 -0
  72. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  73. package/lib/module/linking/URLParser.js +56 -0
  74. package/lib/module/linking/URLParser.js.map +1 -0
  75. package/lib/module/linking/URLParser.test.js +100 -0
  76. package/lib/module/linking/URLParser.test.js.map +1 -0
  77. package/lib/module/linking/types.js +4 -0
  78. package/lib/module/linking/types.js.map +1 -0
  79. package/lib/typescript/Navigation.d.ts +22 -0
  80. package/lib/typescript/Navigation.d.ts.map +1 -1
  81. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  82. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  83. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  84. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  85. package/lib/typescript/commands/Commands.d.ts +1 -0
  86. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  87. package/lib/typescript/index.d.ts +1 -0
  88. package/lib/typescript/index.d.ts.map +1 -1
  89. package/lib/typescript/interfaces/Options.d.ts +90 -0
  90. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  91. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  92. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  93. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  94. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  95. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  96. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  97. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  98. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  99. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  100. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  101. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  102. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  103. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  104. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  105. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  106. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  107. package/lib/typescript/linking/URLParser.d.ts +16 -0
  108. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  109. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  110. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  111. package/lib/typescript/linking/types.d.ts +107 -0
  112. package/lib/typescript/linking/types.d.ts.map +1 -0
  113. package/package.json +1 -1
  114. package/src/ARCHITECTURE.md +30 -0
  115. package/src/Navigation.ts +36 -1
  116. package/src/NavigationDelegate.ts +22 -0
  117. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  118. package/src/commands/Commands.ts +15 -0
  119. package/src/index.ts +1 -0
  120. package/src/interfaces/Options.ts +92 -0
  121. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  122. package/src/linking/DeferredLinkQueue.ts +55 -0
  123. package/src/linking/LinkingHandler.test.ts +332 -0
  124. package/src/linking/LinkingHandler.ts +169 -0
  125. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  126. package/src/linking/ModalLayoutBuilder.ts +60 -0
  127. package/src/linking/RouteMatcher.test.ts +128 -0
  128. package/src/linking/RouteMatcher.ts +126 -0
  129. package/src/linking/URLParser.test.ts +105 -0
  130. package/src/linking/URLParser.ts +62 -0
  131. package/src/linking/types.ts +115 -0
@@ -8,6 +8,8 @@
8
8
  #import <React/RCTCxxBridgeDelegate.h>
9
9
  #endif
10
10
  #import <React/RCTLegacyViewManagerInteropComponentView.h>
11
+ #import <React/RCTLinkingManager.h>
12
+ #import <React/RCTRootView.h>
11
13
  #import <React/RCTSurfacePresenter.h>
12
14
  #if __has_include(<React/RCTSurfacePresenterStub.h>)
13
15
  #import <React/RCTSurfacePresenterStub.h>
@@ -36,6 +38,13 @@
36
38
 
37
39
  #import <React/RCTComponentViewFactory.h>
38
40
 
41
+ // Deep-link URLs that arrive (openURL, universal link, or external dispatch)
42
+ // before the React runtime is ready are queued here and flushed when Fabric
43
+ // posts `RCTContentDidAppearNotification` — by which point
44
+ // `RCTLinkingManager` is instantiated and JS subscribers are listening.
45
+ static NSMutableArray<NSURL *> *gRNNPendingDeepLinkURLs = nil;
46
+ static BOOL gRNNReactRuntimeReady = NO;
47
+
39
48
 
40
49
  static NSString *const kRNConcurrentRoot = @"concurrentRoot";
41
50
 
@@ -92,9 +101,73 @@ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
92
101
  [ReactNativeNavigation bootstrapWithHost:self.reactNativeFactory.rootViewFactory.reactHost];
93
102
  #endif
94
103
 
104
+ [self rnn_installDeepLinkObservers];
105
+
95
106
  return YES;
96
107
  }
97
108
 
109
+ #pragma mark - Deep linking
110
+
111
+ // Forward OS-delivered custom-scheme URLs to React Native's Linking module.
112
+ - (BOOL)application:(UIApplication *)application
113
+ openURL:(NSURL *)url
114
+ options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
115
+ [self dispatchDeepLinkURL:url];
116
+ return YES;
117
+ }
118
+
119
+ // Forward universal links (associated domains) to React Native's Linking
120
+ // module by extracting the underlying https URL and routing it through the
121
+ // same pre-bridge queue as everything else.
122
+ - (BOOL)application:(UIApplication *)application
123
+ continueUserActivity:(NSUserActivity *)userActivity
124
+ restorationHandler:
125
+ (void (^)(NSArray<id<UIUserActivityRestoring>> *_Nullable))restorationHandler {
126
+ if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
127
+ [self dispatchDeepLinkURL:userActivity.webpageURL];
128
+ return YES;
129
+ }
130
+ return NO;
131
+ }
132
+
133
+ - (void)dispatchDeepLinkURL:(NSURL *)url {
134
+ if (url == nil) {
135
+ return;
136
+ }
137
+ if (gRNNReactRuntimeReady) {
138
+ [RCTLinkingManager application:[UIApplication sharedApplication]
139
+ openURL:url
140
+ options:@{}];
141
+ return;
142
+ }
143
+ if (gRNNPendingDeepLinkURLs == nil) {
144
+ gRNNPendingDeepLinkURLs = [NSMutableArray array];
145
+ }
146
+ [gRNNPendingDeepLinkURLs addObject:url];
147
+ }
148
+
149
+ - (void)rnn_installDeepLinkObservers {
150
+ // `RCTContentDidAppearNotification` is posted by Fabric's root view
151
+ // once content has rendered. RNN forces bridgeless/new-arch, so the
152
+ // legacy `RCTJavaScriptDidLoadNotification` never fires; we rely on
153
+ // this Fabric signal exclusively.
154
+ [[NSNotificationCenter defaultCenter] addObserver:self
155
+ selector:@selector(rnn_handleReactRuntimeReady:)
156
+ name:RCTContentDidAppearNotification
157
+ object:nil];
158
+ }
159
+
160
+ - (void)rnn_handleReactRuntimeReady:(NSNotification *)notification {
161
+ gRNNReactRuntimeReady = YES;
162
+ NSArray<NSURL *> *pending = [gRNNPendingDeepLinkURLs copy];
163
+ [gRNNPendingDeepLinkURLs removeAllObjects];
164
+ for (NSURL *url in pending) {
165
+ [RCTLinkingManager application:[UIApplication sharedApplication]
166
+ openURL:url
167
+ options:@{}];
168
+ }
169
+ }
170
+
98
171
 
99
172
  #if !RNN_RN_VERSION_79_OR_NEWER
100
173
  - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
@@ -1,3 +1,4 @@
1
+ #import "RNNComponentOptions.h"
1
2
  #import "RNNOptions.h"
2
3
 
3
4
  @class DotIndicatorOptions;
@@ -5,6 +6,7 @@
5
6
  @interface RNNBottomTabOptions : RNNOptions
6
7
 
7
8
  @property(nonatomic) NSUInteger tag;
9
+ @property(nonatomic, strong) RNNComponentOptions *component;
8
10
  @property(nonatomic, strong) Text *text;
9
11
  @property(nonatomic, strong) Text *badge;
10
12
  @property(nonatomic, strong) Color *badgeColor;
@@ -8,6 +8,9 @@
8
8
  self = [super initWithDict:dict];
9
9
  self.tag = arc4random();
10
10
 
11
+ self.component =
12
+ [[RNNComponentOptions alloc] initWithDict:[dict objectForKey:@"component"]];
13
+
11
14
  self.text = [TextParser parse:dict key:@"text"];
12
15
  self.badge = [TextParser parse:dict key:@"badge"];
13
16
  self.fontFamily = [TextParser parse:dict key:@"fontFamily"];
@@ -38,6 +41,7 @@
38
41
 
39
42
  - (void)mergeOptions:(RNNBottomTabOptions *)options {
40
43
  [self.dotIndicator mergeOptions:options.dotIndicator];
44
+ [self.component mergeOptions:options.component];
41
45
 
42
46
  if (options.text.hasValue)
43
47
  self.text = options.text;
@@ -88,7 +92,7 @@
88
92
  self.iconColor.hasValue || self.selectedIconColor.hasValue ||
89
93
  self.selectedTextColor.hasValue || self.iconInsets.hasValue || self.textColor.hasValue ||
90
94
  self.visible.hasValue || self.selectTabOnPress.hasValue || self.sfSymbol.hasValue ||
91
- self.sfSelectedSymbol.hasValue || self.role.hasValue;
95
+ self.sfSelectedSymbol.hasValue || self.role.hasValue || self.component.hasValue;
92
96
  }
93
97
 
94
98
  @end
@@ -3,6 +3,7 @@
3
3
  #import "RNNBottomTabsPresenter.h"
4
4
  #import "RNNDotIndicatorPresenter.h"
5
5
  #import "RNNEventEmitter.h"
6
+ #import "RNNReactComponentRegistry.h"
6
7
  #import "UIViewController+LayoutProtocol.h"
7
8
  #import <UIKit/UIKit.h>
8
9
 
@@ -16,6 +17,7 @@
16
17
  presenter:(RNNBasePresenter *)presenter
17
18
  bottomTabPresenter:(BottomTabPresenter *)bottomTabPresenter
18
19
  dotIndicatorPresenter:(RNNDotIndicatorPresenter *)dotIndicatorPresenter
20
+ componentRegistry:(RNNReactComponentRegistry *)componentRegistry
19
21
  eventEmitter:(RNNEventEmitter *)eventEmitter
20
22
  childViewControllers:(NSArray *)childViewControllers
21
23
  bottomTabsAttacher:(BottomTabsBaseAttacher *)bottomTabsAttacher;
@@ -1,7 +1,10 @@
1
1
  #import "RNNBottomTabsController.h"
2
+ #import "RNNBottomTabsCustomRow.h"
3
+ #import "RNNCustomTabBarItemView.h"
2
4
  #import "RNNTabBarItemCreator.h"
3
5
  #import "UITabBarController+RNNOptions.h"
4
6
  #import "UITabBarController+RNNUtils.h"
7
+ #import <React/RCTLog.h>
5
8
 
6
9
  @interface RNNBottomTabsController ()
7
10
  @property(nonatomic, strong) BottomTabPresenter *bottomTabPresenter;
@@ -21,6 +24,10 @@
21
24
  BOOL _didFinishSetup;
22
25
  BOOL _rnnDidApplyInitialTabBarSelectionFix;
23
26
  BOOL _rnnSuppressTabSelectionEvents;
27
+ RNNReactComponentRegistry *_componentRegistry;
28
+ NSMutableArray<RNNCustomTabBarItemView *> *_customTabItemViews;
29
+ BOOL _useCustomItemViews;
30
+ RNNBottomTabsCustomRow *_customRow;
24
31
  }
25
32
 
26
33
  - (instancetype)initWithLayoutInfo:(RNNLayoutInfo *)layoutInfo
@@ -30,14 +37,18 @@
30
37
  presenter:(RNNBasePresenter *)presenter
31
38
  bottomTabPresenter:(BottomTabPresenter *)bottomTabPresenter
32
39
  dotIndicatorPresenter:(RNNDotIndicatorPresenter *)dotIndicatorPresenter
40
+ componentRegistry:(RNNReactComponentRegistry *)componentRegistry
33
41
  eventEmitter:(RNNEventEmitter *)eventEmitter
34
42
  childViewControllers:(NSArray *)childViewControllers
35
43
  bottomTabsAttacher:(BottomTabsBaseAttacher *)bottomTabsAttacher {
36
44
  _bottomTabsAttacher = bottomTabsAttacher;
37
45
  _bottomTabPresenter = bottomTabPresenter;
38
46
  _dotIndicatorPresenter = dotIndicatorPresenter;
47
+ _componentRegistry = componentRegistry;
39
48
  _options = options;
40
49
  _didFinishSetup = NO;
50
+ _customTabItemViews = [NSMutableArray new];
51
+ _useCustomItemViews = NO;
41
52
 
42
53
  IntNumber *currentTabIndex = options.bottomTabs.currentTabIndex;
43
54
  if ([currentTabIndex hasValue]) {
@@ -88,14 +99,20 @@
88
99
  [selectedChild pushViewController: [UIViewController new] animated:NO];
89
100
  [selectedChild popViewControllerAnimated:NO];
90
101
  }
102
+
103
+ if (_useCustomItemViews) {
104
+ [self ensureCustomRowAttached];
105
+ [self layoutCustomRow];
106
+ }
91
107
  }
92
108
 
93
109
  - (void)viewDidAppear:(BOOL)animated {
94
110
  [super viewDidAppear:animated];
95
111
  // iOS 26: first layout can misplace tab item titles; cycling selection (then restoring) forces a
96
112
  // correct layout without user interaction. Defer so all tab children are in the hierarchy.
113
+ // Skipped when custom item views are active — the native tab bar visuals are hidden anyway.
97
114
  if (@available(iOS 26.0, *)) {
98
- if (!_rnnDidApplyInitialTabBarSelectionFix) {
115
+ if (!_useCustomItemViews && !_rnnDidApplyInitialTabBarSelectionFix) {
99
116
  _rnnDidApplyInitialTabBarSelectionFix = YES;
100
117
  __weak RNNBottomTabsController *weakSelf = self;
101
118
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -128,13 +145,182 @@
128
145
 
129
146
  - (void)createTabBarItems:(NSArray<UIViewController *> *)childViewControllers {
130
147
  _bottomTabPresenter.tabCreator.searchRoleUsed = NO;
148
+ [self resolveCustomItemViewMode:childViewControllers];
149
+ _bottomTabPresenter.useCustomItemViews = _useCustomItemViews;
131
150
  for (UIViewController *child in childViewControllers) {
132
151
  [_bottomTabPresenter applyOptions:child.resolveOptions child:child];
133
152
  }
134
153
 
154
+ if (_useCustomItemViews) {
155
+ [self buildCustomTabItemViews:childViewControllers];
156
+ [self applyCustomItemViewsTabBarConfiguration];
157
+ [self ensureCustomRowAttached];
158
+ }
159
+
135
160
  [self syncTabBarItemTestIDs];
136
161
  }
137
162
 
163
+ - (void)applyCustomItemViewsTabBarConfiguration {
164
+ // Hide the native tab bar visuals so our custom row is the only thing
165
+ // shown. The bar itself stays in the view hierarchy so that
166
+ // `UITabBarController` keeps reserving the bottom safe-area inset for
167
+ // the selected child controller and exposes the right frame for the
168
+ // row to match.
169
+ for (UIView *subview in self.tabBar.subviews) {
170
+ subview.hidden = YES;
171
+ }
172
+ self.tabBar.tintColor = UIColor.clearColor;
173
+ self.tabBar.unselectedItemTintColor = UIColor.clearColor;
174
+ self.tabBar.backgroundColor = UIColor.clearColor;
175
+ }
176
+
177
+ - (void)resolveCustomItemViewMode:(NSArray<UIViewController *> *)childViewControllers {
178
+ if (childViewControllers.count == 0) {
179
+ _useCustomItemViews = NO;
180
+ return;
181
+ }
182
+
183
+ NSUInteger withComponent = 0;
184
+ for (UIViewController *child in childViewControllers) {
185
+ RNNNavigationOptions *resolved = child.resolveOptions;
186
+ if (resolved.bottomTab.component.name.hasValue) {
187
+ withComponent++;
188
+ }
189
+ }
190
+
191
+ if (withComponent == 0) {
192
+ _useCustomItemViews = NO;
193
+ return;
194
+ }
195
+
196
+ if (withComponent != childViewControllers.count) {
197
+ RCTLogWarn(
198
+ @"[RNN] Mixed bottomTab.component usage detected (%lu of %lu tabs). All tabs must "
199
+ @"declare a component or none — falling back to native rendering for all tabs.",
200
+ (unsigned long)withComponent, (unsigned long)childViewControllers.count);
201
+ _useCustomItemViews = NO;
202
+ return;
203
+ }
204
+
205
+ _useCustomItemViews = YES;
206
+ }
207
+
208
+ - (void)buildCustomTabItemViews:(NSArray<UIViewController *> *)childViewControllers {
209
+ [self destroyCustomTabItemViews];
210
+
211
+ NSString *parentComponentId = self.layoutInfo.componentId;
212
+ for (NSUInteger i = 0; i < childViewControllers.count; i++) {
213
+ UIViewController *child = childViewControllers[i];
214
+ RNNNavigationOptions *resolved = child.resolveOptions;
215
+ RNNComponentOptions *componentOptions = resolved.bottomTab.component;
216
+
217
+ RNNReactView *reactView =
218
+ [_componentRegistry createComponentIfNotExists:componentOptions
219
+ parentComponentId:parentComponentId
220
+ componentType:RNNComponentTypeBottomTabItem
221
+ reactViewReadyBlock:nil];
222
+
223
+ NSString *badge = [resolved.bottomTab.badge withDefault:nil];
224
+ RNNCustomTabBarItemView *itemView =
225
+ [[RNNCustomTabBarItemView alloc] initWithReactView:reactView
226
+ tabIndex:i
227
+ selected:(i == _currentTabIndex)
228
+ badge:badge];
229
+ [_customTabItemViews addObject:itemView];
230
+ }
231
+ }
232
+
233
+ - (void)destroyCustomTabItemViews {
234
+ for (RNNCustomTabBarItemView *itemView in _customTabItemViews) {
235
+ [itemView removeFromSuperview];
236
+ }
237
+ [_customTabItemViews removeAllObjects];
238
+ [_customRow removeFromSuperview];
239
+ _customRow = nil;
240
+ }
241
+
242
+ - (void)ensureCustomRowAttached {
243
+ if (!_useCustomItemViews) {
244
+ return;
245
+ }
246
+ if (!_customRow) {
247
+ _customRow = [[RNNBottomTabsCustomRow alloc] initWithFrame:CGRectZero];
248
+ __weak RNNBottomTabsController *weakSelf = self;
249
+ _customRow.onTapAtIndex = ^(NSUInteger index) {
250
+ [weakSelf handleCustomRowTapAtIndex:index];
251
+ };
252
+ }
253
+ [_customRow setItemViews:_customTabItemViews];
254
+ [_customRow applyOptions:_options.bottomTabs.customRow];
255
+ if (_customRow.superview != self.view) {
256
+ [self.view addSubview:_customRow];
257
+ } else {
258
+ [self.view bringSubviewToFront:_customRow];
259
+ }
260
+ }
261
+
262
+ - (void)layoutCustomRow {
263
+ if (!_useCustomItemViews || !_customRow) {
264
+ return;
265
+ }
266
+ CGRect tabBarFrame = self.tabBar.frame;
267
+ if (CGRectIsEmpty(tabBarFrame)) {
268
+ return;
269
+ }
270
+ CGRect tabBarInView = [self.view convertRect:tabBarFrame fromView:self.tabBar.superview];
271
+
272
+ CGFloat desiredHeight = [_customRow
273
+ desiredRowHeightForNativeTabBarHeight:tabBarInView.size.height
274
+ safeBottom:0];
275
+
276
+ CGFloat bottomMargin = [_customRow effectiveBottomMargin];
277
+ CGFloat rowBottom;
278
+ UIWindow *window = self.view.window;
279
+ if (window) {
280
+ // Map the window's safe-area bottom into this controller's view — reliable
281
+ // for modals where `self.view.safeAreaInsets` is often zero.
282
+ CGFloat yInWindow = CGRectGetHeight(window.bounds) - window.safeAreaInsets.bottom;
283
+ CGPoint pInView = [self.view convertPoint:CGPointMake(0, yInWindow) fromView:window];
284
+ rowBottom = pInView.y - bottomMargin;
285
+ } else {
286
+ rowBottom = CGRectGetMaxY(self.view.safeAreaLayoutGuide.layoutFrame) - bottomMargin;
287
+ }
288
+
289
+ CGRect rowFrame = CGRectMake(tabBarInView.origin.x, rowBottom - desiredHeight,
290
+ tabBarInView.size.width, desiredHeight);
291
+
292
+ _customRow.frame = rowFrame;
293
+ _customRow.hidden = self.tabBar.hidden;
294
+ [_customRow setSelectedIndex:_currentTabIndex];
295
+ }
296
+
297
+ - (void)handleCustomRowTapAtIndex:(NSUInteger)index {
298
+ if (index >= self.childViewControllers.count) {
299
+ return;
300
+ }
301
+ UIViewController *target = self.childViewControllers[index];
302
+ [self.eventEmitter sendBottomTabPressed:@(index)];
303
+ BOOL select = [[target resolveOptions].bottomTab.selectTabOnPress withDefault:YES];
304
+ if (!select) {
305
+ return;
306
+ }
307
+ NSUInteger previous = _currentTabIndex;
308
+ [self setSelectedIndex:index];
309
+ if (!_rnnSuppressTabSelectionEvents) {
310
+ [self.eventEmitter sendBottomTabSelected:@(index) unselected:@(previous)];
311
+ }
312
+ }
313
+
314
+ - (void)updateCustomTabItemSelection {
315
+ if (!_useCustomItemViews) {
316
+ return;
317
+ }
318
+ for (NSUInteger i = 0; i < _customTabItemViews.count; i++) {
319
+ [_customTabItemViews[i] setSelected:(i == _currentTabIndex)];
320
+ }
321
+ [_customRow setSelectedIndex:_currentTabIndex];
322
+ }
323
+
138
324
  - (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewController *)child {
139
325
  [super mergeChildOptions:options child:child];
140
326
  UIViewController *childViewController = [self findViewController:child];
@@ -145,6 +331,13 @@
145
331
  resolvedOptions:childViewController.resolveOptions
146
332
  child:childViewController];
147
333
 
334
+ if (_useCustomItemViews && options.bottomTab.badge.hasValue) {
335
+ NSUInteger index = [self.childViewControllers indexOfObject:childViewController];
336
+ if (index != NSNotFound && index < _customTabItemViews.count) {
337
+ [_customTabItemViews[index] setBadge:[options.bottomTab.badge withDefault:nil]];
338
+ }
339
+ }
340
+
148
341
  [self syncTabBarItemTestIDs];
149
342
  }
150
343
 
@@ -161,6 +354,12 @@
161
354
  [self syncTabBarItemTestIDs];
162
355
  [self.presenter viewDidLayoutSubviews];
163
356
  [_dotIndicatorPresenter bottomTabsDidLayoutSubviews:self];
357
+ if (_useCustomItemViews) {
358
+ // Re-hide native subviews; UIKit recreates them on bounds changes.
359
+ [self applyCustomItemViewsTabBarConfiguration];
360
+ [self ensureCustomRowAttached];
361
+ [self layoutCustomRow];
362
+ }
164
363
  }
165
364
 
166
365
  - (UIViewController *)getCurrentChild {
@@ -195,6 +394,7 @@
195
394
  }
196
395
 
197
396
  [super setSelectedIndex:_currentTabIndex];
397
+ [self updateCustomTabItemSelection];
198
398
  }
199
399
 
200
400
  - (UIViewController *)selectedViewController {
@@ -205,6 +405,7 @@
205
405
  _previousTabIndex = _currentTabIndex;
206
406
  _currentTabIndex = [self.childViewControllers indexOfObject:selectedViewController];
207
407
  [super setSelectedViewController:selectedViewController];
408
+ [self updateCustomTabItemSelection];
208
409
  }
209
410
 
210
411
  - (void)setTabBarVisible:(BOOL)visible animated:(BOOL)animated {
@@ -283,4 +484,11 @@
283
484
  return [self.presenter hidesBottomBarWhenPushed];
284
485
  }
285
486
 
487
+ - (void)dealloc {
488
+ [self destroyCustomTabItemViews];
489
+ if (_componentRegistry && self.layoutInfo.componentId) {
490
+ [_componentRegistry clearComponentsForParentId:self.layoutInfo.componentId];
491
+ }
492
+ }
493
+
286
494
  @end
@@ -0,0 +1,57 @@
1
+ #import "RNNBottomTabsCustomRowOptions.h"
2
+ #import "RNNCustomTabBarItemView.h"
3
+ #import <UIKit/UIKit.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ /**
8
+ * Replaces the visual content of `UITabBar` when `bottomTab.component` is
9
+ * declared on every tab. Hosts the React-rendered cell views in equal-width
10
+ * slots and dispatches taps back to the bottom-tabs controller.
11
+ *
12
+ * The native `UITabBar` is kept (with its visuals hidden) so that
13
+ * `UITabBarController` keeps managing the bottom safe-area inset for the
14
+ * selected child controller — this row is laid out on top of it.
15
+ *
16
+ * Visual chrome (height, background, corner radius, margins) is configured
17
+ * via `RNNBottomTabsCustomRowOptions` and pushed in by the controller.
18
+ */
19
+ @interface RNNBottomTabsCustomRow : UIView
20
+
21
+ /**
22
+ * Block invoked when the user taps a cell. The argument is the 0-based index
23
+ * of the tapped cell.
24
+ */
25
+ @property(nonatomic, copy, nullable) void (^onTapAtIndex)(NSUInteger index);
26
+
27
+ /**
28
+ * Replaces the cells displayed by this row. The previously held views are
29
+ * removed from the hierarchy. Each cell is rendered at equal width inside
30
+ * the content rect (safe area is reserved in the row frame, not inset here).
31
+ */
32
+ - (void)setItemViews:(NSArray<RNNCustomTabBarItemView *> *)itemViews;
33
+
34
+ /**
35
+ * Forwards the selected state to each hosted item view.
36
+ */
37
+ - (void)setSelectedIndex:(NSUInteger)selectedIndex;
38
+
39
+ /**
40
+ * Applies user-supplied chrome options (background, corner radius, margins).
41
+ * Pass `nil` (or an options instance with no values) to use defaults.
42
+ */
43
+ - (void)applyOptions:(nullable RNNBottomTabsCustomRowOptions *)options;
44
+
45
+ /**
46
+ * Returns the chrome height (plus bottom margin). The controller positions
47
+ * the row above the home-indicator safe area.
48
+ */
49
+ - (CGFloat)desiredRowHeightForNativeTabBarHeight:(CGFloat)nativeTabBarHeight
50
+ safeBottom:(CGFloat)safeBottom;
51
+
52
+ /** User `bottomMargin` option, or 0. */
53
+ - (CGFloat)effectiveBottomMargin;
54
+
55
+ @end
56
+
57
+ NS_ASSUME_NONNULL_END