react-native-navigation 8.8.5 → 8.8.6-snapshot.2569
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.
- package/android/src/main/java/com/reactnativenavigation/NavigationActivity.java +14 -1
- package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
- package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
- package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
- package/android/src/main/java/com/reactnativenavigation/options/NavigationBarOptions.java +19 -1
- package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
- package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
- package/android/src/main/java/com/reactnativenavigation/utils/SystemUiUtils.kt +63 -9
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +77 -6
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +1 -0
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentViewController.java +2 -5
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/Presenter.java +33 -13
- package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
- package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
- package/android/src/test/java/com/reactnativenavigation/presentation/PresenterTest.java +14 -0
- package/android/src/test/java/com/reactnativenavigation/utils/SystemUiUtilsTest.kt +64 -1
- package/ios/BottomTabPresenter.h +7 -0
- package/ios/BottomTabPresenter.mm +27 -0
- package/ios/RNNBottomTabOptions.h +2 -0
- package/ios/RNNBottomTabOptions.mm +5 -1
- package/ios/RNNBottomTabsController.h +2 -0
- package/ios/RNNBottomTabsController.mm +209 -1
- package/ios/RNNBottomTabsCustomRow.h +57 -0
- package/ios/RNNBottomTabsCustomRow.mm +252 -0
- package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
- package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
- package/ios/RNNBottomTabsOptions.h +2 -0
- package/ios/RNNBottomTabsOptions.mm +2 -0
- package/ios/RNNComponentViewCreator.h +2 -1
- package/ios/RNNCustomTabBarItemView.h +26 -0
- package/ios/RNNCustomTabBarItemView.mm +83 -0
- package/ios/RNNReactRootViewCreator.mm +1 -0
- package/ios/RNNViewControllerFactory.mm +1 -0
- package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
- package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
- package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
- package/lib/module/commands/Commands.js +8 -0
- package/lib/module/commands/Commands.js.map +1 -1
- package/lib/module/interfaces/Options.js.map +1 -1
- package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
- package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
- package/lib/typescript/commands/Commands.d.ts +1 -0
- package/lib/typescript/commands/Commands.d.ts.map +1 -1
- package/lib/typescript/interfaces/Options.d.ts +90 -0
- package/lib/typescript/interfaces/Options.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
- package/src/commands/Commands.ts +15 -0
- package/src/interfaces/Options.ts +92 -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;
|