@onekeyfe/react-native-bottom-tabs 0.0.0-stage → 6.7.0-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 (77) hide show
  1. package/LICENSE +46 -0
  2. package/README.md +86 -2
  3. package/android/build.gradle +119 -0
  4. package/android/gradle.properties +4 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/AndroidManifestNew.xml +2 -0
  7. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +16 -0
  8. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +1424 -0
  9. package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +216 -0
  10. package/android/src/main/java/com/onekey/bottomtabs/FloatingTabBarStyle.kt +50 -0
  11. package/android/src/main/java/com/onekey/bottomtabs/ImageSource.kt +86 -0
  12. package/android/src/main/java/com/onekey/bottomtabs/TabInfo.kt +13 -0
  13. package/android/src/main/java/com/onekey/bottomtabs/Utils.kt +31 -0
  14. package/android/src/main/java/com/onekey/bottomtabs/events/OnNativeLayoutEvent.kt +20 -0
  15. package/android/src/main/java/com/onekey/bottomtabs/events/OnTabBarMeasuredEvent.kt +19 -0
  16. package/android/src/main/java/com/onekey/bottomtabs/events/PageSelectedEvent.kt +21 -0
  17. package/android/src/main/java/com/onekey/bottomtabs/events/TabDoublePressEvent.kt +19 -0
  18. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewComponentDescriptor.h +32 -0
  19. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.cpp +18 -0
  20. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewShadowNode.h +35 -0
  21. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.cpp +15 -0
  22. package/common/cpp/react/renderer/components/RNBottomTabs/RNBottomTabsViewState.h +25 -0
  23. package/docs/SPEC.md +104 -0
  24. package/ios/Extensions.swift +46 -0
  25. package/ios/RCTBottomAccessoryComponentView.h +12 -0
  26. package/ios/RCTBottomAccessoryComponentView.mm +67 -0
  27. package/ios/RCTBottomAccessoryContainerView.swift +51 -0
  28. package/ios/RCTBottomTabsComponentView.h +12 -0
  29. package/ios/RCTBottomTabsComponentView.mm +338 -0
  30. package/ios/RCTBottomTabsContainerView.swift +777 -0
  31. package/ios/RCTBottomTabsLog.h +7 -0
  32. package/ios/RCTBottomTabsLog.m +32 -0
  33. package/ios/SVG/CoreSVG.h +13 -0
  34. package/ios/SVG/CoreSVG.mm +177 -0
  35. package/ios/SVG/SvgDecoder.h +10 -0
  36. package/ios/SVG/SvgDecoder.mm +32 -0
  37. package/ios/TabBarFontSize.swift +55 -0
  38. package/lib/module/BottomAccessoryView.js +45 -0
  39. package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
  40. package/lib/module/BottomTabs.js +323 -0
  41. package/lib/module/BottomTabsNativeComponent.ts +69 -0
  42. package/lib/module/DelayedFreeze.js +31 -0
  43. package/lib/module/NativeSVGDecoder.js +5 -0
  44. package/lib/module/SceneMap.js +28 -0
  45. package/lib/module/codegen-types.d.js +2 -0
  46. package/lib/module/index.js +20 -0
  47. package/lib/module/package.json +1 -0
  48. package/lib/module/types.js +4 -0
  49. package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
  50. package/lib/module/utils/useBottomTabBarHeight.js +12 -0
  51. package/lib/typescript/package.json +1 -0
  52. package/lib/typescript/src/BottomAccessoryView.d.ts +8 -0
  53. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +16 -0
  54. package/lib/typescript/src/BottomTabs.d.ts +202 -0
  55. package/lib/typescript/src/BottomTabsNativeComponent.d.ts +58 -0
  56. package/lib/typescript/src/DelayedFreeze.d.ts +9 -0
  57. package/lib/typescript/src/NativeSVGDecoder.d.ts +6 -0
  58. package/lib/typescript/src/SceneMap.d.ts +10 -0
  59. package/lib/typescript/src/index.d.ts +16 -0
  60. package/lib/typescript/src/types.d.ts +29 -0
  61. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +3 -0
  62. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +2 -0
  63. package/package.json +117 -4
  64. package/react-native-bottom-tabs.podspec +36 -0
  65. package/react-native.config.js +13 -0
  66. package/src/BottomAccessoryView.tsx +58 -0
  67. package/src/BottomAccessoryViewNativeComponent.ts +27 -0
  68. package/src/BottomTabs.tsx +545 -0
  69. package/src/BottomTabsNativeComponent.ts +69 -0
  70. package/src/DelayedFreeze.tsx +33 -0
  71. package/src/NativeSVGDecoder.ts +5 -0
  72. package/src/SceneMap.tsx +34 -0
  73. package/src/codegen-types.d.ts +28 -0
  74. package/src/index.tsx +18 -0
  75. package/src/types.ts +31 -0
  76. package/src/utils/BottomTabBarHeightContext.ts +5 -0
  77. package/src/utils/useBottomTabBarHeight.ts +15 -0
package/docs/SPEC.md ADDED
@@ -0,0 +1,104 @@
1
+ # Native Tab View behavioral contract
2
+
3
+ ## Tab presses and double press
4
+
5
+ Status: implemented in module source. Runtime verification status is recorded
6
+ under Conformance and acceptance.
7
+
8
+ Every tab press that the native tab bar accepts emits `onPageSelected` with the
9
+ tab key, including a press on the already selected tab (reselection). A
10
+ reselection never changes the selected page. A tab with `preventsDefault` emits
11
+ `onPageSelected` but keeps the current page selected.
12
+
13
+ `onTabDoublePress` fires when two consecutive accepted presses target the same
14
+ tab within the platform double-tap timeout (iOS 0.3 s; Android
15
+ `ViewConfiguration.getDoubleTapTimeout()`, 300 ms by default). Both presses still
16
+ emit `onPageSelected` first; the double press is an additional event, emitted
17
+ after the second press. After it fires, tracking resets, so a third quick press
18
+ starts a new pair. Double press is derived from accepted selections only: no
19
+ extra gesture recognizer is attached and single-press latency is unchanged.
20
+ JS-driven `selectedPage` changes do not count as presses.
21
+
22
+ Tab long press is not supported. The former `onTabLongPress` prop/event and the
23
+ native long-press recognizers were removed.
24
+
25
+ ## iOS native tab bar gestures
26
+
27
+ The iOS tab bar keeps UIKit's own gesture handling. On iOS 26 this includes the
28
+ Liquid Glass press-and-drag selection: pressing a tab lifts the lens and dragging
29
+ scrubs across tabs, selecting on release through the normal
30
+ `tabBarController(_:shouldSelect:)` path. The module MUST NOT install recognizers
31
+ that delay, cancel, or require failure of the system tab bar gestures.
32
+
33
+ The previous drag-cancellation guard (which refused selection for any touch that
34
+ moved more than 10 points) was removed because it disabled drag-to-switch.
35
+
36
+ ## Android tab bar style
37
+
38
+ `androidTabBarStyle` selects the Android presentation; iOS ignores it.
39
+
40
+ - `default` (default): Material `BottomNavigationView` docked at the bottom,
41
+ unchanged behavior.
42
+ - `floating`: a capsule bar modeled on the iOS 26 tab bar. Scene content extends
43
+ underneath it.
44
+
45
+ Floating geometry (dp): bar height 62, side margin 20 plus the navigation-bar
46
+ side insets, bottom gap `max(bottomInset, 20) + 4`, fully rounded corners. The
47
+ bottom inset is the navigation-bar inset (0 when `ignoreBottomInsets`) or the
48
+ keyboard inset when `ignoreKeyboardInsets` is false, whichever is larger. Icons
49
+ are 26, labels 10 sp in both states unless `fontSize` is set, and item content is
50
+ vertically centered. The reported tab bar height is the bar height plus its
51
+ bottom gap, or 0 while hidden.
52
+
53
+ Floating appearance: the bar surface is `barTintColor` (or the theme background)
54
+ at 95% opacity (lightened slightly for dark colors), with
55
+ a hairline rim and an outline shadow. Android has no system backdrop material,
56
+ so no blur is applied. The selected tab sits on a capsule pill inset 4 dp
57
+ (`activeIndicatorColor` when provided). The Material ripple is disabled; the
58
+ lens provides press feedback.
59
+
60
+ Floating interaction (single finger; motion splitting is disabled):
61
+
62
+ 1. Touch down on any tab moves the pill to that tab and lifts it into a lens
63
+ (grows past the bar edges, gains a rim) and magnifies that tab's content.
64
+ 2. Movement beyond touch slop starts a drag: the lens follows the finger with
65
+ edge resistance and stretches with velocity; the tab nearest the lens center
66
+ becomes the candidate, is magnified, and ticks haptically when it changes.
67
+ No page change or event happens during the drag.
68
+ 3. Release without drag performs the tab's click (selection or reselection).
69
+ Release displacement is also checked against touch slop, including when no
70
+ MOVE was delivered or the last MOVE remained within slop. If release crosses
71
+ slop, it starts a drag and updates the candidate from the release position.
72
+ Release after a drag selects the candidate once, only if it differs from the
73
+ current tab. Cancel selects nothing.
74
+ 4. The lens then settles back into the pill on the selected tab, which stays on
75
+ the old tab for cancelled or `preventsDefault` presses.
76
+
77
+ JS-driven selection changes slide the pill with a spring and velocity stretch.
78
+ Switching style at runtime rebuilds the bar and restores Material's label sizes,
79
+ icon size, ripple, padding, and gravity when returning to `default`.
80
+
81
+ ## Threading and ownership
82
+
83
+ All gesture handling, animations, and timers run on the platform UI thread in
84
+ the main-runtime native view. The view owns no background-runtime or shared JS
85
+ objects. Retained state is bounded to the current touch sequence, the last
86
+ pressed tab and timestamp, and at most two running animators.
87
+
88
+ ## Conformance and acceptance
89
+
90
+ Implementation:
91
+ - iOS: `ios/RCTBottomTabsContainerView.swift` (`shouldSelectTab`,
92
+ `emitDoublePressIfNeeded`), `ios/RCTBottomTabsComponentView.mm` event forwarding.
93
+ - Android: `android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt` (floating
94
+ layout, `handleFloatingTabTouch`, `emitDoublePressIfNeeded`),
95
+ `FloatingTabBarStyle.kt`, `events/TabDoublePressEvent.kt`.
96
+
97
+ Required runtime cases per platform: stationary tap selects; reselect emits
98
+ `onPageSelected` without page change; two quick taps on one tab emit
99
+ `onTabDoublePress` exactly once; slow taps do not. iOS 26: press-and-drag across
100
+ tabs selects the tab under the finger on release. Android floating: press on a
101
+ non-selected tab lifts the lens there; drag crossing tab centers does not change
102
+ pages; release selects the nearest tab once; cancel restores; hidden bar, dark
103
+ mode, and 3-button navigation insets lay out correctly. Source compilation is
104
+ recorded separately from runtime acceptance.
@@ -0,0 +1,46 @@
1
+ import UIKit
2
+
3
+ extension Collection {
4
+ subscript(safe index: Index) -> Element? {
5
+ indices.contains(index) ? self[index] : nil
6
+ }
7
+ }
8
+
9
+ extension Collection where Element == TabItem {
10
+ func findByKey(_ key: String?) -> Element? {
11
+ guard let key else { return nil }
12
+ guard !isEmpty else { return nil }
13
+ return first { $0.key == key }
14
+ }
15
+ }
16
+
17
+ extension UIView {
18
+ func pinEdges(to other: UIView) {
19
+ NSLayoutConstraint.activate([
20
+ leadingAnchor.constraint(equalTo: other.leadingAnchor),
21
+ trailingAnchor.constraint(equalTo: other.trailingAnchor),
22
+ topAnchor.constraint(equalTo: other.topAnchor),
23
+ bottomAnchor.constraint(equalTo: other.bottomAnchor)
24
+ ])
25
+ }
26
+ }
27
+
28
+ extension UIImage {
29
+ func resizeImageTo(size: CGSize) -> UIImage? {
30
+ let renderer = UIGraphicsImageRenderer(size: size)
31
+ return renderer.image { _ in
32
+ self.draw(in: CGRect(origin: .zero, size: size))
33
+ }
34
+ }
35
+ }
36
+
37
+ extension UIColor {
38
+ convenience init(rgb: Int) {
39
+ self.init(
40
+ red: CGFloat((rgb >> 16) & 0xFF) / 255.0,
41
+ green: CGFloat((rgb >> 8) & 0xFF) / 255.0,
42
+ blue: CGFloat(rgb & 0xFF) / 255.0,
43
+ alpha: CGFloat((rgb >> 24) & 0xFF) / 255.0
44
+ )
45
+ }
46
+ }
@@ -0,0 +1,12 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+ #import <React/RCTViewComponentView.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface RCTBottomAccessoryComponentView: RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
12
+ #endif /* RCT_NEW_ARCH_ENABLED */
@@ -0,0 +1,67 @@
1
+ #import "RCTBottomAccessoryComponentView.h"
2
+
3
+ #import <react/renderer/components/RNBottomTabs/ComponentDescriptors.h>
4
+ #import <react/renderer/components/RNBottomTabs/EventEmitters.h>
5
+ #import <react/renderer/components/RNBottomTabs/Props.h>
6
+ #import <react/renderer/components/RNBottomTabs/RCTComponentViewHelpers.h>
7
+
8
+ #import <React/RCTFabricComponentsPlugins.h>
9
+
10
+ #if __has_include(<BottomTabsModule/BottomTabsModule-Swift.h>)
11
+ #import <BottomTabsModule/BottomTabsModule-Swift.h>
12
+ #elif __has_include("BottomTabsModule/BottomTabsModule-Swift.h")
13
+ #import "BottomTabsModule/BottomTabsModule-Swift.h"
14
+ #elif __has_include("BottomTabsModule-Swift.h")
15
+ #import "BottomTabsModule-Swift.h"
16
+ #else
17
+ #import "react_native_bottom_tabs-Swift.h"
18
+ #endif
19
+
20
+ using namespace facebook::react;
21
+
22
+ @implementation RCTBottomAccessoryComponentView {
23
+ UIView *_accessoryView;
24
+ }
25
+
26
+ + (ComponentDescriptorProvider)componentDescriptorProvider
27
+ {
28
+ return concreteComponentDescriptorProvider<RNBottomTabsAccessoryViewComponentDescriptor>();
29
+ }
30
+
31
+ - (instancetype)initWithFrame:(CGRect)frame
32
+ {
33
+ if (self = [super initWithFrame:frame]) {
34
+ static const auto defaultProps = std::make_shared<const RNBottomTabsAccessoryViewProps>();
35
+ _props = defaultProps;
36
+
37
+ Class containerClass = NSClassFromString(@"BottomTabsModule.RCTBottomAccessoryContainerView");
38
+ if (!containerClass) {
39
+ containerClass = NSClassFromString(@"RCTBottomAccessoryContainerView");
40
+ }
41
+ _accessoryView = [[containerClass alloc] init];
42
+ self.contentView = _accessoryView;
43
+ }
44
+
45
+ return self;
46
+ }
47
+
48
+ - (void)setFrame:(CGRect)frame
49
+ {
50
+ [super setFrame:frame];
51
+ if (frame.size.width > 0 && frame.size.height > 0) {
52
+ auto eventEmitter = std::static_pointer_cast<const RNBottomTabsAccessoryViewEventEmitter>(_eventEmitter);
53
+ if (eventEmitter) {
54
+ eventEmitter->onNativeLayout(RNBottomTabsAccessoryViewEventEmitter::OnNativeLayout {
55
+ .width = frame.size.width,
56
+ .height = frame.size.height
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ @end
63
+
64
+ Class<RCTComponentViewProtocol> RNBottomTabsAccessoryViewCls(void)
65
+ {
66
+ return RCTBottomAccessoryComponentView.class;
67
+ }
@@ -0,0 +1,51 @@
1
+ import Foundation
2
+ import UIKit
3
+ import React
4
+
5
+ extension Notification.Name {
6
+ static let bottomAccessoryPlacementChanged = Notification.Name("bottomAccessoryPlacementChanged")
7
+ }
8
+
9
+ class RCTBottomAccessoryContainerView: UIView {
10
+
11
+ // MARK: - Events
12
+ @objc var onNativeLayout: RCTDirectEventBlock?
13
+ @objc var onPlacementChanged: RCTDirectEventBlock?
14
+
15
+ // MARK: - Init
16
+
17
+ override init(frame: CGRect) {
18
+ super.init(frame: frame)
19
+
20
+ NotificationCenter.default.addObserver(
21
+ self,
22
+ selector: #selector(handlePlacementNotification(_:)),
23
+ name: .bottomAccessoryPlacementChanged,
24
+ object: nil
25
+ )
26
+ }
27
+
28
+ required init?(coder: NSCoder) {
29
+ fatalError("init(coder:) has not been implemented")
30
+ }
31
+
32
+ deinit {
33
+ NotificationCenter.default.removeObserver(self)
34
+ }
35
+
36
+ // MARK: - Layout
37
+
38
+ override func layoutSubviews() {
39
+ super.layoutSubviews()
40
+ let bounds = self.bounds
41
+ guard bounds.width > 0 && bounds.height > 0 else { return }
42
+ onNativeLayout?(["width": Double(bounds.width), "height": Double(bounds.height)])
43
+ }
44
+
45
+ // MARK: - Placement
46
+
47
+ @objc private func handlePlacementNotification(_ notification: Notification) {
48
+ guard let placement = notification.userInfo?["placement"] as? String else { return }
49
+ onPlacementChanged?(["placement": placement])
50
+ }
51
+ }
@@ -0,0 +1,12 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+ #import <React/RCTViewComponentView.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface RCTBottomTabsComponentView: RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
12
+ #endif /* RCT_NEW_ARCH_ENABLED */
@@ -0,0 +1,338 @@
1
+ #import "RCTBottomTabsComponentView.h"
2
+
3
+ #import <react/renderer/components/RNBottomTabs/RNBottomTabsViewComponentDescriptor.h>
4
+ #import <react/renderer/components/RNBottomTabs/EventEmitters.h>
5
+ #import <react/renderer/components/RNBottomTabs/Props.h>
6
+ #import <react/renderer/components/RNBottomTabs/RCTComponentViewHelpers.h>
7
+
8
+ #import <React/RCTFabricComponentsPlugins.h>
9
+ #import <React/RCTConversions.h>
10
+ #import <React/RCTImageLoader.h>
11
+ #import <React/RCTImageSource.h>
12
+ #import <React/RCTBridge+Private.h>
13
+ #import <react/utils/ManagedObjectWrapper.h>
14
+
15
+ #if __has_include(<BottomTabsModule/BottomTabsModule-Swift.h>)
16
+ #import <BottomTabsModule/BottomTabsModule-Swift.h>
17
+ #elif __has_include("BottomTabsModule/BottomTabsModule-Swift.h")
18
+ #import "BottomTabsModule/BottomTabsModule-Swift.h"
19
+ #elif __has_include("BottomTabsModule-Swift.h")
20
+ #import "BottomTabsModule-Swift.h"
21
+ #else
22
+ #import "react_native_bottom_tabs-Swift.h"
23
+ #endif
24
+
25
+ using namespace facebook::react;
26
+
27
+ static inline NSString* _Nullable NSStringFromStdStringNilIfEmpty(const std::string &string) {
28
+ return string.empty() ? nil : [NSString stringWithUTF8String:string.c_str()];
29
+ }
30
+
31
+ // Overload `==` and `!=` operators for `RNBottomTabsViewItemsStruct`
32
+ namespace facebook::react {
33
+
34
+ bool operator==(const RNBottomTabsViewItemsStruct& lhs, const RNBottomTabsViewItemsStruct& rhs) {
35
+ return lhs.key == rhs.key &&
36
+ lhs.title == rhs.title &&
37
+ lhs.sfSymbol == rhs.sfSymbol &&
38
+ lhs.badge == rhs.badge &&
39
+ lhs.activeTintColor == rhs.activeTintColor &&
40
+ lhs.hidden == rhs.hidden &&
41
+ lhs.testID == rhs.testID &&
42
+ lhs.role == rhs.role &&
43
+ lhs.preventsDefault == rhs.preventsDefault;
44
+ }
45
+
46
+ bool operator!=(const RNBottomTabsViewItemsStruct& lhs, const RNBottomTabsViewItemsStruct& rhs) {
47
+ return !(lhs == rhs);
48
+ }
49
+
50
+ }
51
+
52
+ static NSNumber* _Nullable colorToProcessedInt(const SharedColor &color) {
53
+ UIColor *uiColor = RCTUIColorFromSharedColor(color);
54
+ if (!uiColor) return nil;
55
+ CGFloat r, g, b, a;
56
+ [uiColor getRed:&r green:&g blue:&b alpha:&a];
57
+ int32_t colorInt = ((int)(a * 255) << 24) | ((int)(r * 255) << 16) | ((int)(g * 255) << 8) | (int)(b * 255);
58
+ return @(colorInt);
59
+ }
60
+
61
+ static NSArray<NSDictionary *>* convertItemsToArray(const std::vector<RNBottomTabsViewItemsStruct>& items) {
62
+ NSMutableArray *result = [NSMutableArray arrayWithCapacity:items.size()];
63
+
64
+ for (const auto& item : items) {
65
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
66
+ dict[@"key"] = [NSString stringWithUTF8String:item.key.c_str()];
67
+ dict[@"title"] = [NSString stringWithUTF8String:item.title.c_str()];
68
+
69
+ if (!item.sfSymbol.empty()) {
70
+ dict[@"sfSymbol"] = [NSString stringWithUTF8String:item.sfSymbol.c_str()];
71
+ }
72
+ if (!item.badge.empty()) {
73
+ dict[@"badge"] = [NSString stringWithUTF8String:item.badge.c_str()];
74
+ }
75
+
76
+ NSNumber *activeTint = colorToProcessedInt(item.activeTintColor);
77
+ if (activeTint) dict[@"activeTintColor"] = activeTint;
78
+
79
+ NSNumber *badgeBg = colorToProcessedInt(item.badgeBackgroundColor);
80
+ if (badgeBg) dict[@"badgeBackgroundColor"] = badgeBg;
81
+
82
+ NSNumber *badgeTxt = colorToProcessedInt(item.badgeTextColor);
83
+ if (badgeTxt) dict[@"badgeTextColor"] = badgeTxt;
84
+
85
+ dict[@"hidden"] = @(item.hidden);
86
+ dict[@"preventsDefault"] = @(item.preventsDefault);
87
+
88
+ if (!item.testID.empty()) {
89
+ dict[@"testID"] = [NSString stringWithUTF8String:item.testID.c_str()];
90
+ }
91
+ if (!item.role.empty()) {
92
+ dict[@"role"] = [NSString stringWithUTF8String:item.role.c_str()];
93
+ }
94
+
95
+ [result addObject:dict];
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ @interface RCTBottomTabsComponentView () <RCTRNBottomTabsViewViewProtocol>
102
+ @end
103
+
104
+ @implementation RCTBottomTabsComponentView {
105
+ UIView *_containerView;
106
+ }
107
+
108
+ + (ComponentDescriptorProvider)componentDescriptorProvider
109
+ {
110
+ return concreteComponentDescriptorProvider<RNBottomTabsViewComponentDescriptor>();
111
+ }
112
+
113
+ - (instancetype)initWithFrame:(CGRect)frame
114
+ {
115
+ if (self = [super initWithFrame:frame]) {
116
+ static const auto defaultProps = std::make_shared<const RNBottomTabsViewProps>();
117
+ _props = defaultProps;
118
+
119
+ Class containerClass = NSClassFromString(@"BottomTabsModule.RCTBottomTabsContainerView");
120
+ if (!containerClass) {
121
+ containerClass = NSClassFromString(@"RCTBottomTabsContainerView");
122
+ }
123
+ _containerView = [[containerClass alloc] init];
124
+ self.contentView = _containerView;
125
+
126
+ // Set up event forwarding from Swift container to Fabric EventEmitter.
127
+ // Swift container uses RCTDirectEventBlock (void (^)(NSDictionary *)) for events,
128
+ // but in Fabric mode we need to forward them through the C++ EventEmitter.
129
+ typedef void (^EventBlock)(NSDictionary *);
130
+ __weak auto weakSelf = self;
131
+
132
+ EventBlock onPageSelected = ^(NSDictionary *body) {
133
+ auto strongSelf = weakSelf;
134
+ if (!strongSelf) return;
135
+ auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
136
+ if (emitter) {
137
+ emitter->onPageSelected({.key = std::string([[body objectForKey:@"key"] UTF8String] ?: "")});
138
+ }
139
+ };
140
+ [_containerView setValue:onPageSelected forKey:@"onPageSelected"];
141
+
142
+ EventBlock onTabDoublePress = ^(NSDictionary *body) {
143
+ auto strongSelf = weakSelf;
144
+ if (!strongSelf) return;
145
+ auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
146
+ if (emitter) {
147
+ emitter->onTabDoublePress({.key = std::string([[body objectForKey:@"key"] UTF8String] ?: "")});
148
+ }
149
+ };
150
+ [_containerView setValue:onTabDoublePress forKey:@"onTabDoublePress"];
151
+
152
+ EventBlock onTabBarMeasured = ^(NSDictionary *body) {
153
+ auto strongSelf = weakSelf;
154
+ if (!strongSelf) return;
155
+ auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
156
+ if (emitter) {
157
+ emitter->onTabBarMeasured({.height = [[body objectForKey:@"height"] intValue]});
158
+ }
159
+ };
160
+ [_containerView setValue:onTabBarMeasured forKey:@"onTabBarMeasured"];
161
+
162
+ EventBlock onNativeLayout = ^(NSDictionary *body) {
163
+ auto strongSelf = weakSelf;
164
+ if (!strongSelf) return;
165
+ auto emitter = std::static_pointer_cast<const RNBottomTabsViewEventEmitter>(strongSelf->_eventEmitter);
166
+ if (emitter) {
167
+ emitter->onNativeLayout({
168
+ .width = [[body objectForKey:@"width"] doubleValue],
169
+ .height = [[body objectForKey:@"height"] doubleValue]
170
+ });
171
+ }
172
+ };
173
+ [_containerView setValue:onNativeLayout forKey:@"onNativeLayout"];
174
+ }
175
+
176
+ return self;
177
+ }
178
+
179
+ + (BOOL)shouldBeRecycled
180
+ {
181
+ return NO;
182
+ }
183
+
184
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
185
+ SEL sel = @selector(insertChild:atIndex:);
186
+ if ([_containerView respondsToSelector:sel]) {
187
+ NSMethodSignature *sig = [_containerView methodSignatureForSelector:sel];
188
+ NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
189
+ [inv setSelector:sel];
190
+ [inv setTarget:_containerView];
191
+ UIView *child = childComponentView;
192
+ [inv setArgument:&child atIndex:2];
193
+ [inv setArgument:&index atIndex:3];
194
+ [inv invoke];
195
+ }
196
+ }
197
+
198
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
199
+ SEL sel = @selector(removeChildAtIndex:);
200
+ if ([_containerView respondsToSelector:sel]) {
201
+ NSMethodSignature *sig = [_containerView methodSignatureForSelector:sel];
202
+ NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
203
+ [inv setSelector:sel];
204
+ [inv setTarget:_containerView];
205
+ [inv setArgument:&index atIndex:2];
206
+ [inv invoke];
207
+ }
208
+ [childComponentView removeFromSuperview];
209
+ }
210
+
211
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
212
+ {
213
+ const auto &oldViewProps = *std::static_pointer_cast<RNBottomTabsViewProps const>(_props);
214
+ const auto &newViewProps = *std::static_pointer_cast<RNBottomTabsViewProps const>(props);
215
+
216
+ #define SET_PROP(prop, value) \
217
+ if ([_containerView respondsToSelector:@selector(setProp:)]) { \
218
+ [_containerView setValue:value forKey:@#prop]; \
219
+ }
220
+
221
+ if (oldViewProps.items != newViewProps.items) {
222
+ [_containerView setValue:convertItemsToArray(newViewProps.items) forKey:@"items"];
223
+ }
224
+
225
+ if (oldViewProps.translucent != newViewProps.translucent) {
226
+ [_containerView setValue:@(newViewProps.translucent) forKey:@"translucent"];
227
+ }
228
+
229
+ if (oldViewProps.icons != newViewProps.icons) {
230
+ auto iconsArray = [[NSMutableArray alloc] init];
231
+ for (auto &source: newViewProps.icons) {
232
+ NSMutableDictionary *iconDict = [NSMutableDictionary dictionary];
233
+ iconDict[@"uri"] = [NSString stringWithUTF8String:source.uri.c_str()];
234
+ iconDict[@"width"] = @(source.size.width);
235
+ iconDict[@"height"] = @(source.size.height);
236
+ iconDict[@"scale"] = @(source.scale);
237
+ [iconsArray addObject:iconDict];
238
+ }
239
+ [_containerView setValue:iconsArray forKey:@"icons"];
240
+ }
241
+
242
+ if (oldViewProps.selectedIcons != newViewProps.selectedIcons) {
243
+ auto selectedIconsArray = [[NSMutableArray alloc] init];
244
+ for (auto &source: newViewProps.selectedIcons) {
245
+ NSMutableDictionary *iconDict = [NSMutableDictionary dictionary];
246
+ iconDict[@"uri"] = [NSString stringWithUTF8String:source.uri.c_str()];
247
+ iconDict[@"width"] = @(source.size.width);
248
+ iconDict[@"height"] = @(source.size.height);
249
+ iconDict[@"scale"] = @(source.scale);
250
+ [selectedIconsArray addObject:iconDict];
251
+ }
252
+ [_containerView setValue:selectedIconsArray forKey:@"selectedIcons"];
253
+ }
254
+
255
+ if (oldViewProps.sidebarAdaptable != newViewProps.sidebarAdaptable) {
256
+ [_containerView setValue:@(newViewProps.sidebarAdaptable) forKey:@"sidebarAdaptable"];
257
+ }
258
+
259
+ if (oldViewProps.minimizeBehavior != newViewProps.minimizeBehavior) {
260
+ [_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.minimizeBehavior) forKey:@"minimizeBehavior"];
261
+ }
262
+
263
+ if (oldViewProps.disablePageAnimations != newViewProps.disablePageAnimations) {
264
+ [_containerView setValue:@(newViewProps.disablePageAnimations) forKey:@"disablePageAnimations"];
265
+ }
266
+
267
+ if (oldViewProps.labeled != newViewProps.labeled) {
268
+ [_containerView setValue:@(newViewProps.labeled) forKey:@"labeled"];
269
+ }
270
+
271
+ if (oldViewProps.selectedPage != newViewProps.selectedPage) {
272
+ [_containerView setValue:[NSString stringWithUTF8String:newViewProps.selectedPage.c_str()] forKey:@"selectedPage"];
273
+ }
274
+
275
+ if (oldViewProps.scrollEdgeAppearance != newViewProps.scrollEdgeAppearance) {
276
+ [_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.scrollEdgeAppearance) forKey:@"scrollEdgeAppearance"];
277
+ }
278
+
279
+ if (oldViewProps.barTintColor != newViewProps.barTintColor) {
280
+ [_containerView setValue:RCTUIColorFromSharedColor(newViewProps.barTintColor) forKey:@"barTintColor"];
281
+ }
282
+
283
+ if (oldViewProps.activeTintColor != newViewProps.activeTintColor) {
284
+ [_containerView setValue:RCTUIColorFromSharedColor(newViewProps.activeTintColor) forKey:@"activeTintColor"];
285
+ }
286
+
287
+ if (oldViewProps.inactiveTintColor != newViewProps.inactiveTintColor) {
288
+ [_containerView setValue:RCTUIColorFromSharedColor(newViewProps.inactiveTintColor) forKey:@"inactiveTintColor"];
289
+ }
290
+
291
+ if (oldViewProps.hapticFeedbackEnabled != newViewProps.hapticFeedbackEnabled) {
292
+ [_containerView setValue:@(newViewProps.hapticFeedbackEnabled) forKey:@"hapticFeedbackEnabled"];
293
+ }
294
+
295
+ if (oldViewProps.fontSize != newViewProps.fontSize) {
296
+ [_containerView setValue:@(newViewProps.fontSize) forKey:@"fontSize"];
297
+ }
298
+
299
+ if (oldViewProps.fontWeight != newViewProps.fontWeight) {
300
+ [_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.fontWeight) forKey:@"fontWeight"];
301
+ }
302
+
303
+ if (oldViewProps.fontFamily != newViewProps.fontFamily) {
304
+ [_containerView setValue:NSStringFromStdStringNilIfEmpty(newViewProps.fontFamily) forKey:@"fontFamily"];
305
+ }
306
+
307
+ if (oldViewProps.tabBarHidden != newViewProps.tabBarHidden) {
308
+ [_containerView setValue:@(newViewProps.tabBarHidden) forKey:@"tabBarHidden"];
309
+ }
310
+
311
+ #undef SET_PROP
312
+
313
+ [super updateProps:props oldProps:oldProps];
314
+ }
315
+
316
+ - (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState
317
+ {
318
+ auto _state = std::static_pointer_cast<RNBottomTabsViewShadowNode::ConcreteState const>(state);
319
+ auto data = _state->getData();
320
+ if (auto imgLoaderPtr = _state.get()->getData().getImageLoader().lock()) {
321
+ id imageLoader = unwrapManagedObject(imgLoaderPtr);
322
+ if ([_containerView respondsToSelector:@selector(setImageLoader:)]) {
323
+ [_containerView performSelector:@selector(setImageLoader:) withObject:imageLoader];
324
+ }
325
+ }
326
+ }
327
+
328
+ - (void)prepareForRecycle
329
+ {
330
+ [super prepareForRecycle];
331
+ }
332
+
333
+ @end
334
+
335
+ Class<RCTComponentViewProtocol> RNBottomTabsViewCls(void)
336
+ {
337
+ return RCTBottomTabsComponentView.class;
338
+ }