@soundtrackio/react-native-bottom-tabs 1.4.0

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 (125) hide show
  1. package/README.md +32 -0
  2. package/android/build.gradle +132 -0
  3. package/android/gradle.properties +5 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/AndroidManifestNew.xml +2 -0
  6. package/android/src/main/java/com/rcttabview/ImageSource.kt +86 -0
  7. package/android/src/main/java/com/rcttabview/LatestRequestTracker.kt +19 -0
  8. package/android/src/main/java/com/rcttabview/RCTTabView.kt +689 -0
  9. package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +222 -0
  10. package/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +19 -0
  11. package/android/src/main/java/com/rcttabview/RCTTabViewPrewarm.kt +40 -0
  12. package/android/src/main/java/com/rcttabview/RebuildBatch.kt +31 -0
  13. package/android/src/main/java/com/rcttabview/TabBarVisibility.kt +14 -0
  14. package/android/src/main/java/com/rcttabview/Utils.kt +31 -0
  15. package/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt +25 -0
  16. package/android/src/main/java/com/rcttabview/events/OnTabBarMeasuredEvent.kt +24 -0
  17. package/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt +32 -0
  18. package/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt +24 -0
  19. package/android/src/test/java/com/rcttabview/LatestRequestTrackerTest.kt +37 -0
  20. package/android/src/test/java/com/rcttabview/RebuildBatchTest.kt +44 -0
  21. package/android/src/test/java/com/rcttabview/TabBarVisibilityTest.kt +58 -0
  22. package/app.plugin.js +1 -0
  23. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +33 -0
  24. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +19 -0
  25. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +35 -0
  26. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp +15 -0
  27. package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +25 -0
  28. package/ios/BottomAccessoryProvider.swift +27 -0
  29. package/ios/Extensions.swift +101 -0
  30. package/ios/PlatformAliases.swift +43 -0
  31. package/ios/RCTBottomAccessoryComponentView.h +17 -0
  32. package/ios/RCTBottomAccessoryComponentView.mm +76 -0
  33. package/ios/RCTTabViewComponentView.h +16 -0
  34. package/ios/RCTTabViewComponentView.mm +285 -0
  35. package/ios/RepresentableView.swift +55 -0
  36. package/ios/SVG/CoreSVG.h +19 -0
  37. package/ios/SVG/CoreSVG.mm +177 -0
  38. package/ios/SVG/SvgDecoder.h +10 -0
  39. package/ios/SVG/SvgDecoder.mm +38 -0
  40. package/ios/TabAppearModifier.swift +38 -0
  41. package/ios/TabBarFontSize.swift +88 -0
  42. package/ios/TabItem.swift +35 -0
  43. package/ios/TabItemEventModifier.swift +163 -0
  44. package/ios/TabView/AnyTabView.swift +7 -0
  45. package/ios/TabView/LegacyTabView.swift +70 -0
  46. package/ios/TabView/NewTabView.swift +138 -0
  47. package/ios/TabViewImpl.swift +566 -0
  48. package/ios/TabViewProps.swift +109 -0
  49. package/ios/TabViewProvider.swift +368 -0
  50. package/ios/react-native-bottom-tabs-Bridging-Header.h +4 -0
  51. package/lib/module/BottomAccessoryView.js +47 -0
  52. package/lib/module/BottomAccessoryView.js.map +1 -0
  53. package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
  54. package/lib/module/DelayedFreeze.js +31 -0
  55. package/lib/module/DelayedFreeze.js.map +1 -0
  56. package/lib/module/NativeSVGDecoder.js +5 -0
  57. package/lib/module/NativeSVGDecoder.js.map +1 -0
  58. package/lib/module/SceneMap.js +28 -0
  59. package/lib/module/SceneMap.js.map +1 -0
  60. package/lib/module/TabView.js +294 -0
  61. package/lib/module/TabView.js.map +1 -0
  62. package/lib/module/TabViewNativeComponent.ts +73 -0
  63. package/lib/module/expo.js +35 -0
  64. package/lib/module/expo.js.map +1 -0
  65. package/lib/module/index.js +20 -0
  66. package/lib/module/index.js.map +1 -0
  67. package/lib/module/package.json +1 -0
  68. package/lib/module/types.js +4 -0
  69. package/lib/module/types.js.map +1 -0
  70. package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
  71. package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -0
  72. package/lib/module/utils/useBottomTabBarHeight.js +12 -0
  73. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -0
  74. package/lib/typescript/package.json +1 -0
  75. package/lib/typescript/src/BottomAccessoryView.d.ts +7 -0
  76. package/lib/typescript/src/BottomAccessoryView.d.ts.map +1 -0
  77. package/lib/typescript/src/BottomAccessoryView.js +24 -0
  78. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +15 -0
  79. package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts.map +1 -0
  80. package/lib/typescript/src/BottomAccessoryViewNativeComponent.js +4 -0
  81. package/lib/typescript/src/DelayedFreeze.d.ts +7 -0
  82. package/lib/typescript/src/DelayedFreeze.d.ts.map +1 -0
  83. package/lib/typescript/src/DelayedFreeze.js +21 -0
  84. package/lib/typescript/src/NativeSVGDecoder.d.ts +5 -0
  85. package/lib/typescript/src/NativeSVGDecoder.d.ts.map +1 -0
  86. package/lib/typescript/src/NativeSVGDecoder.js +2 -0
  87. package/lib/typescript/src/SceneMap.d.ts +9 -0
  88. package/lib/typescript/src/SceneMap.d.ts.map +1 -0
  89. package/lib/typescript/src/SceneMap.js +15 -0
  90. package/lib/typescript/src/TabView.d.ts +219 -0
  91. package/lib/typescript/src/TabView.d.ts.map +1 -0
  92. package/lib/typescript/src/TabView.js +157 -0
  93. package/lib/typescript/src/TabViewNativeComponent.d.ts +59 -0
  94. package/lib/typescript/src/TabViewNativeComponent.d.ts.map +1 -0
  95. package/lib/typescript/src/TabViewNativeComponent.js +4 -0
  96. package/lib/typescript/src/expo.d.ts +6 -0
  97. package/lib/typescript/src/expo.d.ts.map +1 -0
  98. package/lib/typescript/src/expo.js +34 -0
  99. package/lib/typescript/src/index.d.ts +15 -0
  100. package/lib/typescript/src/index.d.ts.map +1 -0
  101. package/lib/typescript/src/index.js +11 -0
  102. package/lib/typescript/src/types.d.ts +31 -0
  103. package/lib/typescript/src/types.d.ts.map +1 -0
  104. package/lib/typescript/src/types.js +1 -0
  105. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +2 -0
  106. package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts.map +1 -0
  107. package/lib/typescript/src/utils/BottomTabBarHeightContext.js +2 -0
  108. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +1 -0
  109. package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts.map +1 -0
  110. package/lib/typescript/src/utils/useBottomTabBarHeight.js +9 -0
  111. package/lib/typescript/tsconfig.tsbuildinfo +1 -0
  112. package/package.json +135 -0
  113. package/react-native-bottom-tabs.podspec +43 -0
  114. package/src/BottomAccessoryView.tsx +59 -0
  115. package/src/BottomAccessoryViewNativeComponent.ts +27 -0
  116. package/src/DelayedFreeze.tsx +31 -0
  117. package/src/NativeSVGDecoder.ts +5 -0
  118. package/src/SceneMap.tsx +34 -0
  119. package/src/TabView.tsx +558 -0
  120. package/src/TabViewNativeComponent.ts +73 -0
  121. package/src/expo.ts +58 -0
  122. package/src/index.tsx +23 -0
  123. package/src/types.ts +36 -0
  124. package/src/utils/BottomTabBarHeightContext.ts +5 -0
  125. package/src/utils/useBottomTabBarHeight.ts +15 -0
@@ -0,0 +1,19 @@
1
+ #if TARGET_OS_OSX
2
+ #import <AppKit/AppKit.h>
3
+ typedef NSImage PlatformImage;
4
+ #else
5
+ #import <UIKit/UIKit.h>
6
+ typedef UIImage PlatformImage;
7
+ #endif
8
+
9
+
10
+ @interface CoreSVGWrapper : NSObject
11
+
12
+ + (instancetype)shared;
13
+
14
+ - (PlatformImage *)imageFromSVGData:(NSData *)data;
15
+
16
+ + (BOOL)isSVGData:(NSData *)data;
17
+ + (BOOL)supportsVectorSVGImage;
18
+
19
+ @end
@@ -0,0 +1,177 @@
1
+ #import "CoreSVG.h"
2
+ #import <dlfcn.h>
3
+ #import <objc/runtime.h>
4
+
5
+ #define kSVGTagEnd @"</svg>"
6
+
7
+ typedef struct CF_BRIDGED_TYPE(id) CGSVGDocument *CGSVGDocumentRef;
8
+
9
+ static CGSVGDocumentRef (*CoreSVGDocumentRetain)(CGSVGDocumentRef);
10
+ static void (*CoreSVGDocumentRelease)(CGSVGDocumentRef);
11
+ static CGSVGDocumentRef (*CoreSVGDocumentCreateFromData)(CFDataRef data, CFDictionaryRef options);
12
+ static void (*CoreSVGContextDrawSVGDocument)(CGContextRef context, CGSVGDocumentRef document);
13
+ static CGSize (*CoreSVGDocumentGetCanvasSize)(CGSVGDocumentRef document);
14
+
15
+ #if !TARGET_OS_OSX
16
+ static SEL CoreSVGImageWithDocumentSEL = NULL;
17
+ static SEL CoreSVGDocumentSEL = NULL;
18
+ #endif
19
+ #if TARGET_OS_OSX
20
+ static Class CoreSVGImageRepClass = NULL;
21
+ static Ivar CoreSVGImageRepDocumentIvar = NULL;
22
+ #endif
23
+
24
+ static inline NSString *Base64DecodedString(NSString *base64String) {
25
+ NSData *data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSDataBase64DecodingIgnoreUnknownCharacters];
26
+ if (!data) {
27
+ return nil;
28
+ }
29
+ return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
30
+ }
31
+
32
+ @implementation CoreSVGWrapper
33
+
34
+ + (instancetype)shared {
35
+ static dispatch_once_t onceToken;
36
+ static CoreSVGWrapper *wrapper;
37
+ dispatch_once(&onceToken, ^{
38
+ wrapper = [[CoreSVGWrapper alloc] init];
39
+ });
40
+ return wrapper;
41
+ }
42
+
43
+ + (void)initialize {
44
+ CoreSVGDocumentRetain = (CGSVGDocumentRef (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJldGFpbg==").UTF8String);
45
+ CoreSVGDocumentRelease = (void (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJlbGVhc2U=").UTF8String);
46
+ CoreSVGDocumentCreateFromData = (CGSVGDocumentRef (*)(CFDataRef data, CFDictionaryRef options))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudENyZWF0ZUZyb21EYXRh").UTF8String);
47
+ CoreSVGContextDrawSVGDocument = (void (*)(CGContextRef context, CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dDb250ZXh0RHJhd1NWR0RvY3VtZW50").UTF8String);
48
+ CoreSVGDocumentGetCanvasSize = (CGSize (*)(CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudEdldENhbnZhc1NpemU=").UTF8String);
49
+
50
+ #if !TARGET_OS_OSX
51
+ CoreSVGImageWithDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X2ltYWdlV2l0aENHU1ZHRG9jdW1lbnQ6"));
52
+ CoreSVGDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X0NHU1ZHRG9jdW1lbnQ="));
53
+ #endif
54
+ #if TARGET_OS_OSX
55
+ CoreSVGImageRepClass = NSClassFromString(Base64DecodedString(@"X05TU1ZHSW1hZ2VSZXA="));
56
+ if (CoreSVGImageRepClass) {
57
+ CoreSVGImageRepDocumentIvar = class_getInstanceVariable(CoreSVGImageRepClass, Base64DecodedString(@"X2RvY3VtZW50").UTF8String);
58
+ }
59
+ #endif
60
+ }
61
+
62
+ - (PlatformImage *)imageFromSVGData:(NSData *)data {
63
+ if (!data) {
64
+ return nil;
65
+ }
66
+
67
+ if (![self.class supportsVectorSVGImage]) {
68
+ return nil;
69
+ }
70
+
71
+ return [self createVectorSVGWithData:data];
72
+ }
73
+
74
+ - (PlatformImage *)createVectorSVGWithData:(NSData *)data {
75
+ if (!data) return nil;
76
+
77
+ PlatformImage *image;
78
+
79
+ #if TARGET_OS_OSX
80
+ if (!CoreSVGImageRepClass) {
81
+ return nil;
82
+ }
83
+ Class imageRepClass = CoreSVGImageRepClass;
84
+ NSImageRep *imageRep = [[imageRepClass alloc] initWithData:data];
85
+ if (!imageRep) {
86
+ return nil;
87
+ }
88
+ image = [[NSImage alloc] initWithSize:imageRep.size];
89
+ [image addRepresentation:imageRep];
90
+ #else
91
+ if (!CoreSVGDocumentCreateFromData || !CoreSVGDocumentRelease) {
92
+ return nil;
93
+ }
94
+ CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL);
95
+
96
+ if (!document) {
97
+ return nil;
98
+ }
99
+
100
+ image = ((UIImage *(*)(id,SEL,CGSVGDocumentRef))[UIImage.class methodForSelector:CoreSVGImageWithDocumentSEL])(UIImage.class, CoreSVGImageWithDocumentSEL, document);
101
+ CoreSVGDocumentRelease(document);
102
+ #endif
103
+
104
+ #if TARGET_OS_OSX
105
+ // Test render to catch potential CoreSVG crashes on macOS
106
+ NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc]
107
+ initWithBitmapDataPlanes:NULL
108
+ pixelsWide:1
109
+ pixelsHigh:1
110
+ bitsPerSample:8
111
+ samplesPerPixel:4
112
+ hasAlpha:YES
113
+ isPlanar:NO
114
+ colorSpaceName:NSCalibratedRGBColorSpace
115
+ bytesPerRow:0
116
+ bitsPerPixel:0];
117
+
118
+ NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap];
119
+ [NSGraphicsContext saveGraphicsState];
120
+ [NSGraphicsContext setCurrentContext:context];
121
+
122
+ @try {
123
+ [image drawInRect:NSMakeRect(0, 0, 1, 1)];
124
+ } @catch (...) {
125
+ [NSGraphicsContext restoreGraphicsState];
126
+ return nil;
127
+ }
128
+
129
+ [NSGraphicsContext restoreGraphicsState];
130
+ #else
131
+ // Test render to catch potential CoreSVG crashes
132
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1.0);
133
+ @try {
134
+ [image drawInRect:CGRectMake(0, 0, 1, 1)];
135
+ } @catch (...) {
136
+ UIGraphicsEndImageContext();
137
+ return nil;
138
+ }
139
+ UIGraphicsEndImageContext();
140
+ #endif
141
+
142
+
143
+ return image;
144
+ }
145
+
146
+ + (BOOL)isSVGData:(NSData *)data {
147
+ if (!data) {
148
+ return NO;
149
+ }
150
+ // Check end with SVG tag
151
+ return [data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range: NSMakeRange(data.length - MIN(100, data.length), MIN(100, data.length))].location != NSNotFound;
152
+ }
153
+
154
+ + (BOOL)supportsVectorSVGImage {
155
+ static dispatch_once_t onceToken;
156
+ static BOOL supports;
157
+ dispatch_once(&onceToken, ^{
158
+ #if TARGET_OS_OSX
159
+ // macOS 10.15+ supports SVG built-in rendering, use selector to check is more accurate
160
+ if (CoreSVGImageRepClass) {
161
+ supports = YES;
162
+ } else {
163
+ supports = NO;
164
+ }
165
+ #else
166
+ // iOS 13+ supports SVG built-in rendering, use selector to check is more accurate
167
+ if ([UIImage respondsToSelector:CoreSVGImageWithDocumentSEL]) {
168
+ supports = YES;
169
+ } else {
170
+ supports = NO;
171
+ }
172
+ #endif
173
+ });
174
+ return supports;
175
+ }
176
+
177
+ @end
@@ -0,0 +1,10 @@
1
+ #ifdef __cplusplus
2
+
3
+ #import <RNCTabView/RNCTabView.h>
4
+ #import <React/RCTImageDataDecoder.h>
5
+
6
+ @interface SvgDecoder : NSObject <RCTImageDataDecoder, NativeSVGDecoderSpec>
7
+
8
+ @end
9
+
10
+ #endif
@@ -0,0 +1,38 @@
1
+ #import "SvgDecoder.h"
2
+ #import "CoreSVG.h"
3
+
4
+ @implementation SvgDecoder
5
+
6
+ RCT_EXPORT_MODULE()
7
+
8
+ - (BOOL)canDecodeImageData:(NSData *)imageData
9
+ {
10
+ return [CoreSVGWrapper isSVGData:imageData];
11
+ }
12
+
13
+ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
14
+ size:(CGSize)size
15
+ scale:(CGFloat)scale
16
+ resizeMode:(RCTResizeMode)resizeMode
17
+ completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
18
+ {
19
+ UIImage *image = [CoreSVGWrapper.shared imageFromSVGData:imageData];
20
+
21
+ if (image) {
22
+ completionHandler(nil, image);
23
+ } else {
24
+ NSError *error = [NSError errorWithDomain:@"SVGDecoderErrorDomain"
25
+ code:2
26
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to render SVG to image"}];
27
+ completionHandler(error, nil);
28
+ }
29
+ return ^{};
30
+ }
31
+
32
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
33
+ (const facebook::react::ObjCTurboModule::InitParams &)params
34
+ {
35
+ return std::make_shared<facebook::react::NativeSVGDecoderSpecJSI>(params);
36
+ }
37
+
38
+ @end
@@ -0,0 +1,38 @@
1
+ import SwiftUI
2
+
3
+ struct TabAppearContext {
4
+ let index: Int
5
+ let tabData: TabInfo
6
+ let props: TabViewProps
7
+ let updateTabBarAppearance: () -> Void
8
+ let onSelect: (_ key: String) -> Void
9
+ }
10
+
11
+ struct TabAppearModifier: ViewModifier {
12
+ let context: TabAppearContext
13
+
14
+ func body(content: Content) -> some View {
15
+ content.onAppear {
16
+ #if !os(macOS)
17
+ context.updateTabBarAppearance()
18
+ #endif
19
+
20
+ #if os(iOS)
21
+ // Sync selection for tabs nested under the system "More" tab, which
22
+ // only exists when there are more than 5 visible tabs. Without the
23
+ // count guard the 5th tab (index 4) of a non-overflowing bar would
24
+ // force-select itself whenever its scene re-appears (e.g. when the tab
25
+ // bar is re-shown after `.hideTabBar`), hijacking the selection.
26
+ if context.props.filteredItems.count > 5, context.index >= 4, context.props.selectedPage != context.tabData.key {
27
+ context.onSelect(context.tabData.key)
28
+ }
29
+ #endif
30
+ }
31
+ }
32
+ }
33
+
34
+ extension View {
35
+ func tabAppear(using context: TabAppearContext) -> some View {
36
+ self.modifier(TabAppearModifier(context: context))
37
+ }
38
+ }
@@ -0,0 +1,88 @@
1
+ import React
2
+ #if os(macOS)
3
+ import AppKit
4
+ #else
5
+ import UIKit
6
+ #endif
7
+
8
+ enum TabBarFontSize {
9
+ /// Returns the default font size for tab bar item labels based on the current platform
10
+ #if os(tvOS)
11
+ static let defaultSize: CGFloat = 30.0
12
+ #elseif os(macOS)
13
+ static let defaultSize: CGFloat = 11.0
14
+ #else
15
+ static let defaultSize: CGFloat = {
16
+ if UIDevice.current.userInterfaceIdiom == .pad {
17
+ return 13.0
18
+ }
19
+
20
+ return 10.0
21
+ }()
22
+ #endif
23
+
24
+ /// Creates font attributes for tab bar items
25
+ /// - Parameters:
26
+ /// - size: Font size in points
27
+ /// - family: Optional font family name
28
+ /// - weight: Optional font weight string
29
+ /// - color: Optional text color
30
+ /// - Returns: Dictionary of NSAttributedString attributes
31
+ static func createFontAttributes(
32
+ size: CGFloat,
33
+ family: String? = nil,
34
+ weight: String? = nil,
35
+ color: PlatformColor? = nil
36
+ ) -> [NSAttributedString.Key: Any] {
37
+ var attributes: [NSAttributedString.Key: Any] = [:]
38
+
39
+ // Create font with React Native font handling if family or weight is specified
40
+ if family != nil || weight != nil {
41
+ attributes[.font] = RCTFont.update(
42
+ nil,
43
+ withFamily: family,
44
+ size: NSNumber(value: size),
45
+ weight: weight,
46
+ style: nil,
47
+ variant: nil,
48
+ scaleMultiplier: 1.0
49
+ )
50
+ } else {
51
+ // Fallback to system font
52
+ #if os(macOS)
53
+ attributes[.font] = NSFont.boldSystemFont(ofSize: size)
54
+ #else
55
+ attributes[.font] = UIFont.boldSystemFont(ofSize: size)
56
+ #endif
57
+ }
58
+
59
+ // Add color if provided
60
+ if let color {
61
+ attributes[.foregroundColor] = color
62
+ }
63
+
64
+ return attributes
65
+ }
66
+
67
+ /// Creates font attributes specifically for normal (unselected) tab state
68
+ /// - Parameters:
69
+ /// - fontSize: Optional font size (uses default if nil)
70
+ /// - fontFamily: Optional font family
71
+ /// - fontWeight: Optional font weight
72
+ /// - inactiveColor: Optional color for inactive state
73
+ /// - Returns: Font attributes dictionary
74
+ static func createNormalStateAttributes(
75
+ fontSize: Int? = nil,
76
+ fontFamily: String? = nil,
77
+ fontWeight: String? = nil,
78
+ inactiveColor: PlatformColor? = nil
79
+ ) -> [NSAttributedString.Key: Any] {
80
+ let size = fontSize.map(CGFloat.init) ?? defaultSize
81
+ return createFontAttributes(
82
+ size: size,
83
+ family: fontFamily,
84
+ weight: fontWeight,
85
+ color: inactiveColor
86
+ )
87
+ }
88
+ }
@@ -0,0 +1,35 @@
1
+ import SwiftUI
2
+
3
+ struct TabItem: View {
4
+ var title: String?
5
+ var icon: PlatformImage?
6
+ var sfSymbol: String?
7
+ var labeled: Bool?
8
+ var iconRenderingMode: String?
9
+
10
+ var body: some View {
11
+ if let icon {
12
+ #if os(macOS)
13
+ Image(nsImage: icon)
14
+ #else
15
+ Image(uiImage: renderedIcon(icon))
16
+ #endif
17
+ } else if let sfSymbol, !sfSymbol.isEmpty {
18
+ Image(systemName: sfSymbol)
19
+ .noneSymbolVariant()
20
+ }
21
+ if labeled != false {
22
+ Text(title ?? "")
23
+ }
24
+ }
25
+
26
+ #if !os(macOS)
27
+ private var preservesOriginalIconColors: Bool {
28
+ iconRenderingMode == "original"
29
+ }
30
+
31
+ private func renderedIcon(_ icon: UIImage) -> UIImage {
32
+ preservesOriginalIconColors ? icon.withRenderingMode(.alwaysOriginal) : icon
33
+ }
34
+ #endif
35
+ }
@@ -0,0 +1,163 @@
1
+ import SwiftUI
2
+ import SwiftUIIntrospect
3
+ #if !os(macOS)
4
+ import UIKit
5
+ #endif
6
+
7
+ #if !os(macOS) && !os(visionOS)
8
+
9
+ private final class TabBarDelegate: NSObject, UITabBarControllerDelegate {
10
+ var onClick: ((_ index: Int?, _ identifier: String?) -> Bool)?
11
+
12
+ func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
13
+ if #available(iOS 27.0, *) {
14
+ // iOS 27 routes SwiftUI TabView selection through shouldSelectTab.
15
+ return true
16
+ }
17
+
18
+ #if os(iOS)
19
+ // Handle "More" Tab
20
+ if tabBarController.moreNavigationController == viewController {
21
+ return true
22
+ }
23
+ #endif
24
+
25
+ let isReselectingSameTab = tabBarController.selectedViewController == viewController
26
+
27
+ if isReselectingSameTab {
28
+ if let index = tabBarController.viewControllers?.firstIndex(of: viewController) {
29
+ _ = onClick?(index, nil)
30
+ }
31
+
32
+ return false
33
+ }
34
+
35
+ // Unfortunately, due to iOS 26 new tab switching animations, controlling state from JavaScript is causing significant delays when switching tabs.
36
+ // See: https://github.com/callstackincubator/react-native-bottom-tabs/issues/383
37
+ // Due to this, whether the tab prevents default has to be defined statically.
38
+ if let index = tabBarController.viewControllers?.firstIndex(of: viewController) {
39
+ let defaultPrevented = onClick?(index, nil) ?? false
40
+
41
+ return !defaultPrevented
42
+ }
43
+
44
+ return false
45
+ }
46
+
47
+ @available(iOS 18.0, tvOS 18.0, visionOS 2.0, *)
48
+ func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
49
+ guard #available(iOS 27.0, *) else {
50
+ return true
51
+ }
52
+
53
+ let isReselectingSameTab =
54
+ tabBarController.selectedTab === tab ||
55
+ tabBarController.selectedTab?.identifier == tab.identifier
56
+
57
+ // Unfortunately, due to iOS 26 new tab switching animations, controlling state from JavaScript is causing significant delays when switching tabs.
58
+ // See: https://github.com/callstackincubator/react-native-bottom-tabs/issues/383
59
+ // Due to this, whether the tab prevents default has to be defined statically.
60
+ let defaultPrevented = onClick?(
61
+ tabIndex(for: tab, in: tabBarController),
62
+ tab.identifier
63
+ ) ?? false
64
+
65
+ return isReselectingSameTab ? false : !defaultPrevented
66
+ }
67
+
68
+ @available(iOS 18.0, tvOS 18.0, visionOS 2.0, *)
69
+ private func tabIndex(for tab: UITab, in tabBarController: UITabBarController) -> Int? {
70
+ tabBarController.tabs.firstIndex {
71
+ $0 === tab || $0.identifier == tab.identifier
72
+ }
73
+ }
74
+ }
75
+
76
+ struct TabItemEventModifier: ViewModifier {
77
+ let onTabEvent: (_ index: Int?, _ identifier: String?, _ isLongPress: Bool) -> Bool
78
+ private let delegate = TabBarDelegate()
79
+
80
+ func body(content: Content) -> some View {
81
+ content
82
+ .introspectTabView { tabController in
83
+ handle(tabController: tabController)
84
+ }
85
+ }
86
+
87
+ func handle(tabController: UITabBarController) {
88
+ delegate.onClick = { index, identifier in
89
+ onTabEvent(index, identifier, false)
90
+ }
91
+ tabController.delegate = delegate
92
+
93
+ // Don't register gesutre recognizer more than one time
94
+ if objc_getAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler) != nil {
95
+ return
96
+ }
97
+
98
+ // Remove existing long press gestures
99
+ if let existingGestures = tabController.tabBar.gestureRecognizers {
100
+ for gesture in existingGestures where gesture is UILongPressGestureRecognizer {
101
+ tabController.tabBar.removeGestureRecognizer(gesture)
102
+ }
103
+ }
104
+
105
+ // Create gesture handler
106
+ let handler = LongPressGestureHandler(tabBar: tabController.tabBar) { index, isLongPress in _ = onTabEvent(index, nil, isLongPress) }
107
+ let gesture = UILongPressGestureRecognizer(target: handler, action: #selector(LongPressGestureHandler.handleLongPress(_:)))
108
+ gesture.minimumPressDuration = 0.5
109
+
110
+ objc_setAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler, handler, .OBJC_ASSOCIATION_RETAIN)
111
+
112
+ tabController.tabBar.addGestureRecognizer(gesture)
113
+ }
114
+ }
115
+
116
+ private struct AssociatedKeys {
117
+ static var gestureHandler: UInt8 = 0
118
+ }
119
+
120
+ private class LongPressGestureHandler: NSObject {
121
+ private weak var tabBar: UITabBar?
122
+ private let handler: (Int, Bool) -> Void
123
+
124
+ init(tabBar: UITabBar, handler: @escaping (Int, Bool) -> Void) {
125
+ self.tabBar = tabBar
126
+ self.handler = handler
127
+ super.init()
128
+ }
129
+
130
+ @objc func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
131
+ guard recognizer.state == .began,
132
+ let tabBar else { return }
133
+
134
+ let location = recognizer.location(in: tabBar)
135
+
136
+ // Get buttons and sort them by frames
137
+ let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).contains("UITabBarButton") }.sorted { $0.frame.minX < $1.frame.minX }
138
+
139
+ for (index, button) in tabBarButtons.enumerated() {
140
+ if button.frame.contains(location) {
141
+ handler(index, true)
142
+ break
143
+ }
144
+ }
145
+ }
146
+
147
+ deinit {
148
+ if let tabBar {
149
+ objc_setAssociatedObject(tabBar, &AssociatedKeys.gestureHandler, nil, .OBJC_ASSOCIATION_RETAIN)
150
+ }
151
+ }
152
+ }
153
+
154
+ extension View {
155
+ /**
156
+ Event for tab items. Returns true if should prevent default (switching tabs).
157
+ */
158
+ func onTabItemEvent(_ handler: @escaping (Int?, String?, Bool) -> Bool) -> some View {
159
+ modifier(TabItemEventModifier(onTabEvent: handler))
160
+ }
161
+ }
162
+
163
+ #endif
@@ -0,0 +1,7 @@
1
+ import SwiftUI
2
+
3
+ public protocol AnyTabView: View {
4
+ var onLayout: (_ size: CGSize) -> Void { get }
5
+ var onSelect: (_ key: String) -> Void { get }
6
+ var updateTabBarAppearance: () -> Void { get }
7
+ }
@@ -0,0 +1,70 @@
1
+ import SwiftUI
2
+
3
+ struct LegacyTabView: AnyTabView {
4
+ @ObservedObject var props: TabViewProps
5
+
6
+ var onLayout: (CGSize) -> Void
7
+ var onSelect: (String) -> Void
8
+ var updateTabBarAppearance: () -> Void
9
+
10
+ private var effectiveLayoutDirection: LayoutDirection {
11
+ let dir = props.layoutDirection ?? "locale"
12
+ if let mapped = ["rtl": LayoutDirection.rightToLeft,
13
+ "ltr": LayoutDirection.leftToRight][dir] {
14
+ return mapped
15
+ }
16
+ let system = UIView.userInterfaceLayoutDirection(for: .unspecified)
17
+ return system == .rightToLeft ? .rightToLeft : .leftToRight
18
+ }
19
+
20
+ @ViewBuilder
21
+ var body: some View {
22
+ TabView(selection: $props.selectedPage) {
23
+ ForEach(props.children) { child in
24
+ if let index = props.children.firstIndex(of: child) {
25
+ renderTabItem(at: index)
26
+ }
27
+ }
28
+ .measureView { size in
29
+ onLayout(size)
30
+ }
31
+ }
32
+ .environment(\.layoutDirection, effectiveLayoutDirection)
33
+ .hideTabBar(props.tabBarHidden)
34
+ }
35
+
36
+ @ViewBuilder
37
+ private func renderTabItem(at index: Int) -> some View {
38
+ if let tabData = props.items[safe: index] {
39
+ let isFocused = props.selectedPage == tabData.key
40
+
41
+ if !tabData.hidden || isFocused {
42
+ let icon = props.icons[index]
43
+ let child = props.children[safe: index]?.view ?? PlatformView()
44
+ let context = TabAppearContext(
45
+ index: index,
46
+ tabData: tabData,
47
+ props: props,
48
+ updateTabBarAppearance: updateTabBarAppearance,
49
+ onSelect: onSelect
50
+ )
51
+
52
+ RepresentableView(view: child)
53
+ .ignoresSafeArea(.container, edges: .all)
54
+ .tabItem {
55
+ TabItem(
56
+ title: tabData.title,
57
+ icon: icon,
58
+ sfSymbol: tabData.sfSymbol,
59
+ labeled: props.labeled,
60
+ iconRenderingMode: tabData.iconRenderingMode
61
+ )
62
+ .accessibilityIdentifier(tabData.testID ?? "")
63
+ }
64
+ .tag(tabData.key)
65
+ .tabBadge(tabData.badge)
66
+ .tabAppear(using: context)
67
+ }
68
+ }
69
+ }
70
+ }