react-native-navigation 8.8.6 → 8.8.7

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 (126) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  2. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  3. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  9. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  10. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  11. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  12. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  13. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +59 -0
  14. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  15. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  16. package/android/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleBarReactButtonView.java +27 -11
  17. package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +15 -1
  18. package/android/src/test/java/com/reactnativenavigation/views/TitleBarReactButtonViewTest.java +135 -0
  19. package/ios/ARCHITECTURE.md +5 -0
  20. package/ios/BottomTabPresenter.h +7 -0
  21. package/ios/BottomTabPresenter.mm +27 -0
  22. package/ios/RNNAppDelegate.h +16 -0
  23. package/ios/RNNAppDelegate.mm +73 -0
  24. package/ios/RNNBottomTabOptions.h +2 -0
  25. package/ios/RNNBottomTabOptions.mm +5 -1
  26. package/ios/RNNBottomTabsController.h +2 -0
  27. package/ios/RNNBottomTabsController.mm +209 -1
  28. package/ios/RNNBottomTabsCustomRow.h +57 -0
  29. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  30. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  31. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  32. package/ios/RNNBottomTabsOptions.h +2 -0
  33. package/ios/RNNBottomTabsOptions.mm +2 -0
  34. package/ios/RNNComponentViewCreator.h +2 -1
  35. package/ios/RNNCustomTabBarItemView.h +26 -0
  36. package/ios/RNNCustomTabBarItemView.mm +83 -0
  37. package/ios/RNNReactRootViewCreator.mm +1 -0
  38. package/ios/RNNViewControllerFactory.mm +1 -0
  39. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  40. package/lib/module/ARCHITECTURE.md +30 -0
  41. package/lib/module/Navigation.js +34 -1
  42. package/lib/module/Navigation.js.map +1 -1
  43. package/lib/module/NavigationDelegate.js +21 -0
  44. package/lib/module/NavigationDelegate.js.map +1 -1
  45. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  46. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  47. package/lib/module/commands/Commands.js +8 -0
  48. package/lib/module/commands/Commands.js.map +1 -1
  49. package/lib/module/index.js +1 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/interfaces/Options.js.map +1 -1
  52. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  53. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  54. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  55. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  56. package/lib/module/linking/LinkingHandler.js +139 -0
  57. package/lib/module/linking/LinkingHandler.js.map +1 -0
  58. package/lib/module/linking/LinkingHandler.test.js +384 -0
  59. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  60. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  61. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  62. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  63. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  64. package/lib/module/linking/RouteMatcher.js +104 -0
  65. package/lib/module/linking/RouteMatcher.js.map +1 -0
  66. package/lib/module/linking/RouteMatcher.test.js +164 -0
  67. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  68. package/lib/module/linking/URLParser.js +56 -0
  69. package/lib/module/linking/URLParser.js.map +1 -0
  70. package/lib/module/linking/URLParser.test.js +100 -0
  71. package/lib/module/linking/URLParser.test.js.map +1 -0
  72. package/lib/module/linking/types.js +4 -0
  73. package/lib/module/linking/types.js.map +1 -0
  74. package/lib/typescript/Navigation.d.ts +22 -0
  75. package/lib/typescript/Navigation.d.ts.map +1 -1
  76. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  77. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  78. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  79. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  80. package/lib/typescript/commands/Commands.d.ts +1 -0
  81. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  82. package/lib/typescript/index.d.ts +1 -0
  83. package/lib/typescript/index.d.ts.map +1 -1
  84. package/lib/typescript/interfaces/Options.d.ts +85 -0
  85. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  86. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  87. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  88. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  89. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  90. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  91. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  92. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  93. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  94. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  95. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  96. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  97. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  98. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  99. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  100. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  101. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  102. package/lib/typescript/linking/URLParser.d.ts +16 -0
  103. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  104. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  105. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  106. package/lib/typescript/linking/types.d.ts +107 -0
  107. package/lib/typescript/linking/types.d.ts.map +1 -0
  108. package/package.json +1 -1
  109. package/src/ARCHITECTURE.md +30 -0
  110. package/src/Navigation.ts +36 -1
  111. package/src/NavigationDelegate.ts +22 -0
  112. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  113. package/src/commands/Commands.ts +15 -0
  114. package/src/index.ts +1 -0
  115. package/src/interfaces/Options.ts +87 -0
  116. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  117. package/src/linking/DeferredLinkQueue.ts +55 -0
  118. package/src/linking/LinkingHandler.test.ts +332 -0
  119. package/src/linking/LinkingHandler.ts +169 -0
  120. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  121. package/src/linking/ModalLayoutBuilder.ts +60 -0
  122. package/src/linking/RouteMatcher.test.ts +128 -0
  123. package/src/linking/RouteMatcher.ts +126 -0
  124. package/src/linking/URLParser.test.ts +105 -0
  125. package/src/linking/URLParser.ts +62 -0
  126. package/src/linking/types.ts +115 -0
@@ -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
@@ -0,0 +1,252 @@
1
+ #import "RNNBottomTabsCustomRow.h"
2
+
3
+ @interface RNNBottomTabsCustomRowCell : UIControl
4
+ @property(nonatomic, strong, nullable) RNNCustomTabBarItemView *itemView;
5
+ @property(nonatomic, assign) NSUInteger index;
6
+ @end
7
+
8
+ @implementation RNNBottomTabsCustomRowCell
9
+
10
+ - (void)setItemView:(RNNCustomTabBarItemView *)itemView {
11
+ if (_itemView == itemView) {
12
+ return;
13
+ }
14
+ [_itemView removeFromSuperview];
15
+ _itemView = itemView;
16
+ if (itemView) {
17
+ itemView.translatesAutoresizingMaskIntoConstraints = YES;
18
+ itemView.autoresizingMask =
19
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
20
+ itemView.frame = self.bounds;
21
+ [self addSubview:itemView];
22
+ }
23
+ }
24
+
25
+ - (void)layoutSubviews {
26
+ [super layoutSubviews];
27
+ self.itemView.frame = self.bounds;
28
+ }
29
+
30
+ @end
31
+
32
+ @interface RNNBottomTabsCustomRow ()
33
+ @property(nonatomic, strong) NSMutableArray<RNNBottomTabsCustomRowCell *> *cells;
34
+ @property(nonatomic, strong) UIVisualEffectView *backgroundEffectView;
35
+ @property(nonatomic, strong) UIView *backgroundColorView;
36
+ @property(nonatomic, strong) RNNBottomTabsCustomRowOptions *options;
37
+ @end
38
+
39
+ @implementation RNNBottomTabsCustomRow
40
+
41
+ - (instancetype)initWithFrame:(CGRect)frame {
42
+ self = [super initWithFrame:frame];
43
+ if (self) {
44
+ _cells = [NSMutableArray new];
45
+ self.backgroundColor = UIColor.clearColor;
46
+
47
+ // Solid background layer (only made visible if user sets backgroundColor).
48
+ _backgroundColorView = [[UIView alloc] init];
49
+ _backgroundColorView.hidden = YES;
50
+ _backgroundColorView.clipsToBounds = YES;
51
+ if (@available(iOS 13.0, *)) {
52
+ _backgroundColorView.layer.cornerCurve = kCACornerCurveContinuous;
53
+ }
54
+ [self addSubview:_backgroundColorView];
55
+
56
+ // Visual effect (blur / glass) layer. Default depends on iOS version.
57
+ UIVisualEffect *effect = [RNNBottomTabsCustomRow defaultBackgroundEffect];
58
+ _backgroundEffectView = [[UIVisualEffectView alloc] initWithEffect:effect];
59
+ _backgroundEffectView.clipsToBounds = YES;
60
+ if (@available(iOS 13.0, *)) {
61
+ _backgroundEffectView.layer.cornerCurve = kCACornerCurveContinuous;
62
+ }
63
+ if (@available(iOS 26.0, *)) {
64
+ _backgroundEffectView.layer.cornerRadius = 28.0;
65
+ }
66
+ [self addSubview:_backgroundEffectView];
67
+ }
68
+ return self;
69
+ }
70
+
71
+ // `UIGlassEffect` is a new visual effect introduced in iOS 26. Reference it
72
+ // at runtime so this file still compiles against older SDKs and so we get a
73
+ // usable fallback on older OS versions.
74
+ + (UIVisualEffect *)defaultBackgroundEffect {
75
+ Class glassClass = NSClassFromString(@"UIGlassEffect");
76
+ if (glassClass) {
77
+ UIVisualEffect *glass = [[glassClass alloc] init];
78
+ if (glass) {
79
+ return glass;
80
+ }
81
+ }
82
+ if (@available(iOS 13.0, *)) {
83
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemChromeMaterial];
84
+ }
85
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
86
+ }
87
+
88
+ + (UIVisualEffect *)blurBackgroundEffect {
89
+ if (@available(iOS 13.0, *)) {
90
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemChromeMaterial];
91
+ }
92
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
93
+ }
94
+
95
+ - (void)applyOptions:(RNNBottomTabsCustomRowOptions *)options {
96
+ self.options = options;
97
+
98
+ UIColor *solidColor =
99
+ options.backgroundColor.hasValue ? options.backgroundColor.get : nil;
100
+ NSString *effectName =
101
+ options.backgroundEffect.hasValue ? options.backgroundEffect.get : nil;
102
+
103
+ BOOL useSolidColor = solidColor != nil;
104
+ self.backgroundColorView.hidden = !useSolidColor;
105
+ if (useSolidColor) {
106
+ self.backgroundColorView.backgroundColor = solidColor;
107
+ }
108
+
109
+ // Effect view stays unless explicitly disabled or overridden by solid color.
110
+ BOOL hideEffect = useSolidColor || [effectName isEqualToString:@"none"];
111
+ self.backgroundEffectView.hidden = hideEffect;
112
+ if (!hideEffect) {
113
+ if ([effectName isEqualToString:@"blur"]) {
114
+ self.backgroundEffectView.effect = [RNNBottomTabsCustomRow blurBackgroundEffect];
115
+ } else if ([effectName isEqualToString:@"glass"]) {
116
+ self.backgroundEffectView.effect = [RNNBottomTabsCustomRow defaultBackgroundEffect];
117
+ }
118
+ }
119
+
120
+ [self setNeedsLayout];
121
+ }
122
+
123
+ - (CGFloat)effectiveCornerRadius {
124
+ if (self.options.cornerRadius.hasValue) {
125
+ return [self.options.cornerRadius.get doubleValue];
126
+ }
127
+ if (@available(iOS 26.0, *)) {
128
+ return 28.0;
129
+ }
130
+ return 0.0;
131
+ }
132
+
133
+ - (CGFloat)effectiveHorizontalMargin {
134
+ if (self.options.horizontalMargin.hasValue) {
135
+ return [self.options.horizontalMargin.get doubleValue];
136
+ }
137
+ if (@available(iOS 26.0, *)) {
138
+ return 16.0;
139
+ }
140
+ return 0.0;
141
+ }
142
+
143
+ - (CGFloat)effectiveBottomMargin {
144
+ if (self.options.bottomMargin.hasValue) {
145
+ return [self.options.bottomMargin.get doubleValue];
146
+ }
147
+ return 0.0;
148
+ }
149
+
150
+ - (CGFloat)desiredRowHeightForNativeTabBarHeight:(CGFloat)nativeTabBarHeight
151
+ safeBottom:(CGFloat)safeBottom {
152
+ (void)safeBottom;
153
+ CGFloat contentHeight = nativeTabBarHeight;
154
+ if (@available(iOS 26.0, *)) {
155
+ contentHeight += 18.0; // default extra for iOS 26 floating bar look
156
+ }
157
+ if (self.options.height.hasValue) {
158
+ contentHeight = [self.options.height.get doubleValue];
159
+ }
160
+ // Safe area is applied by the controller when positioning the row frame.
161
+ return contentHeight + [self effectiveBottomMargin];
162
+ }
163
+
164
+ - (void)setItemViews:(NSArray<RNNCustomTabBarItemView *> *)itemViews {
165
+ for (RNNBottomTabsCustomRowCell *cell in self.cells) {
166
+ [cell removeFromSuperview];
167
+ }
168
+ [self.cells removeAllObjects];
169
+
170
+ UIView *cellContainer = self.backgroundEffectView.contentView;
171
+ for (NSUInteger i = 0; i < itemViews.count; i++) {
172
+ RNNBottomTabsCustomRowCell *cell = [[RNNBottomTabsCustomRowCell alloc] init];
173
+ cell.index = i;
174
+ cell.itemView = itemViews[i];
175
+ [cell addTarget:self
176
+ action:@selector(handleCellTap:)
177
+ forControlEvents:UIControlEventTouchUpInside];
178
+ [cellContainer addSubview:cell];
179
+ [self.cells addObject:cell];
180
+ }
181
+ [self setNeedsLayout];
182
+ }
183
+
184
+ - (void)setSelectedIndex:(NSUInteger)selectedIndex {
185
+ for (NSUInteger i = 0; i < self.cells.count; i++) {
186
+ RNNCustomTabBarItemView *itemView = self.cells[i].itemView;
187
+ [itemView setSelected:(i == selectedIndex)];
188
+ }
189
+ }
190
+
191
+ - (void)handleCellTap:(RNNBottomTabsCustomRowCell *)cell {
192
+ if (self.onTapAtIndex) {
193
+ self.onTapAtIndex(cell.index);
194
+ }
195
+ }
196
+
197
+ - (void)layoutSubviews {
198
+ [super layoutSubviews];
199
+
200
+ // Bottom safe area is already included in the row frame height via
201
+ // `desiredRowHeightForNativeTabBarHeight:safeBottom:` — do not inset it
202
+ // again here or the chrome shrinks by ~home-indicator height.
203
+ CGFloat bottomInset = [self effectiveBottomMargin];
204
+ CGRect content =
205
+ UIEdgeInsetsInsetRect(self.bounds, UIEdgeInsetsMake(0, 0, bottomInset, 0));
206
+
207
+ CGFloat horizontalMargin = [self effectiveHorizontalMargin];
208
+ if (horizontalMargin > 0) {
209
+ content = CGRectInset(content, horizontalMargin, 0);
210
+ }
211
+
212
+ CGFloat cornerRadius = [self effectiveCornerRadius];
213
+ self.backgroundEffectView.layer.cornerRadius = cornerRadius;
214
+ self.backgroundColorView.layer.cornerRadius = cornerRadius;
215
+
216
+ self.backgroundEffectView.frame = content;
217
+ self.backgroundColorView.frame = content;
218
+
219
+ if (self.cells.count == 0) {
220
+ return;
221
+ }
222
+
223
+ // Cells are subviews of the effect view's contentView (so the rounded
224
+ // mask clips them); their frames are relative to the effect view's
225
+ // bounds. The solid color layer sits behind and doesn't host cells.
226
+ UIView *cellContainer = self.backgroundColorView.hidden
227
+ ? self.backgroundEffectView.contentView
228
+ : self.backgroundColorView;
229
+ // Make sure cells live in the visible container.
230
+ if (self.cells.firstObject.superview != cellContainer) {
231
+ for (RNNBottomTabsCustomRowCell *cell in self.cells) {
232
+ [cell removeFromSuperview];
233
+ [cellContainer addSubview:cell];
234
+ }
235
+ }
236
+
237
+ CGFloat totalWidth = content.size.width;
238
+ CGFloat totalHeight = content.size.height;
239
+ CGFloat width = totalWidth / (CGFloat)self.cells.count;
240
+ for (NSUInteger i = 0; i < self.cells.count; i++) {
241
+ CGFloat x = floor((CGFloat)i * width);
242
+ CGFloat nextX = floor((CGFloat)(i + 1) * width);
243
+ self.cells[i].frame = CGRectMake(x, 0, nextX - x, totalHeight);
244
+ }
245
+ }
246
+
247
+ - (void)safeAreaInsetsDidChange {
248
+ [super safeAreaInsetsDidChange];
249
+ [self setNeedsLayout];
250
+ }
251
+
252
+ @end
@@ -0,0 +1,42 @@
1
+ #import "RNNOptions.h"
2
+
3
+ /**
4
+ * Visual options for the floating row that hosts custom-component bottom tab
5
+ * cells. Applies only when every tab declares `bottomTab.component`. All
6
+ * fields are optional; if omitted the row uses sensible defaults (iOS 26
7
+ * glass pill on iOS 26+, blur with no inset on older versions).
8
+ *
9
+ * The same option keys are exposed in JS as `bottomTabs.customRow` on both
10
+ * platforms. Android applies them via `RNNBottomTabsCustomRowModule`.
11
+ */
12
+ @interface RNNBottomTabsCustomRowOptions : RNNOptions
13
+
14
+ /**
15
+ * Override the row's content height. The native tab bar (and its safe-area
16
+ * inset) is preserved underneath — this only changes how tall the visible
17
+ * floating row appears. Defaults to the native tab bar content height (+18pt
18
+ * on iOS 26+ to match the larger native floating bar).
19
+ */
20
+ @property(nonatomic, strong) Number *height;
21
+
22
+ /** Solid background color for the row. When set, overrides `backgroundEffect`. */
23
+ @property(nonatomic, strong) Color *backgroundColor;
24
+
25
+ /**
26
+ * Visual effect for the row background. Values: `glass` | `blur` | `none`.
27
+ * Default: `glass` on iOS 26+, `blur` on older versions.
28
+ */
29
+ @property(nonatomic, strong) Text *backgroundEffect;
30
+
31
+ /** Corner radius of the row. Default: 28 on iOS 26+, 0 below. */
32
+ @property(nonatomic, strong) Number *cornerRadius;
33
+
34
+ /** Horizontal inset of the row from the screen edges. Default: 16 on iOS 26+, 0 below. */
35
+ @property(nonatomic, strong) Number *horizontalMargin;
36
+
37
+ /** Distance between the row's bottom edge and the safe-area bottom. Default: 0. */
38
+ @property(nonatomic, strong) Number *bottomMargin;
39
+
40
+ - (BOOL)hasValue;
41
+
42
+ @end
@@ -0,0 +1,37 @@
1
+ #import "RNNBottomTabsCustomRowOptions.h"
2
+
3
+ @implementation RNNBottomTabsCustomRowOptions
4
+
5
+ - (instancetype)initWithDict:(NSDictionary *)dict {
6
+ self = [super initWithDict:dict];
7
+ self.height = [NumberParser parse:dict key:@"height"];
8
+ self.backgroundColor = [ColorParser parse:dict key:@"backgroundColor"];
9
+ self.backgroundEffect = [TextParser parse:dict key:@"backgroundEffect"];
10
+ self.cornerRadius = [NumberParser parse:dict key:@"cornerRadius"];
11
+ self.horizontalMargin = [NumberParser parse:dict key:@"horizontalMargin"];
12
+ self.bottomMargin = [NumberParser parse:dict key:@"bottomMargin"];
13
+ return self;
14
+ }
15
+
16
+ - (void)mergeOptions:(RNNBottomTabsCustomRowOptions *)options {
17
+ if (options.height.hasValue)
18
+ self.height = options.height;
19
+ if (options.backgroundColor.hasValue)
20
+ self.backgroundColor = options.backgroundColor;
21
+ if (options.backgroundEffect.hasValue)
22
+ self.backgroundEffect = options.backgroundEffect;
23
+ if (options.cornerRadius.hasValue)
24
+ self.cornerRadius = options.cornerRadius;
25
+ if (options.horizontalMargin.hasValue)
26
+ self.horizontalMargin = options.horizontalMargin;
27
+ if (options.bottomMargin.hasValue)
28
+ self.bottomMargin = options.bottomMargin;
29
+ }
30
+
31
+ - (BOOL)hasValue {
32
+ return self.height.hasValue || self.backgroundColor.hasValue ||
33
+ self.backgroundEffect.hasValue || self.cornerRadius.hasValue ||
34
+ self.horizontalMargin.hasValue || self.bottomMargin.hasValue;
35
+ }
36
+
37
+ @end
@@ -1,4 +1,5 @@
1
1
  #import "BottomTabsAttachMode.h"
2
+ #import "RNNBottomTabsCustomRowOptions.h"
2
3
  #import "RNNOptions.h"
3
4
  #import "RNNShadowOptions.h"
4
5
 
@@ -22,6 +23,7 @@
22
23
  @property(nonatomic, strong) Color *borderColor;
23
24
  @property(nonatomic, strong) Number *borderWidth;
24
25
  @property(nonatomic, strong) RNNShadowOptions *shadow;
26
+ @property(nonatomic, strong) RNNBottomTabsCustomRowOptions *customRow;
25
27
  @property(nonatomic, strong) BottomTabsAttachMode *tabsAttachMode;
26
28
 
27
29
  - (BOOL)shouldDrawBehind;