@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.
- package/README.md +32 -0
- package/android/build.gradle +132 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/rcttabview/ImageSource.kt +86 -0
- package/android/src/main/java/com/rcttabview/LatestRequestTracker.kt +19 -0
- package/android/src/main/java/com/rcttabview/RCTTabView.kt +689 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +222 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +19 -0
- package/android/src/main/java/com/rcttabview/RCTTabViewPrewarm.kt +40 -0
- package/android/src/main/java/com/rcttabview/RebuildBatch.kt +31 -0
- package/android/src/main/java/com/rcttabview/TabBarVisibility.kt +14 -0
- package/android/src/main/java/com/rcttabview/Utils.kt +31 -0
- package/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt +25 -0
- package/android/src/main/java/com/rcttabview/events/OnTabBarMeasuredEvent.kt +24 -0
- package/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt +32 -0
- package/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt +24 -0
- package/android/src/test/java/com/rcttabview/LatestRequestTrackerTest.kt +37 -0
- package/android/src/test/java/com/rcttabview/RebuildBatchTest.kt +44 -0
- package/android/src/test/java/com/rcttabview/TabBarVisibilityTest.kt +58 -0
- package/app.plugin.js +1 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +33 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +19 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +35 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp +15 -0
- package/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +25 -0
- package/ios/BottomAccessoryProvider.swift +27 -0
- package/ios/Extensions.swift +101 -0
- package/ios/PlatformAliases.swift +43 -0
- package/ios/RCTBottomAccessoryComponentView.h +17 -0
- package/ios/RCTBottomAccessoryComponentView.mm +76 -0
- package/ios/RCTTabViewComponentView.h +16 -0
- package/ios/RCTTabViewComponentView.mm +285 -0
- package/ios/RepresentableView.swift +55 -0
- package/ios/SVG/CoreSVG.h +19 -0
- package/ios/SVG/CoreSVG.mm +177 -0
- package/ios/SVG/SvgDecoder.h +10 -0
- package/ios/SVG/SvgDecoder.mm +38 -0
- package/ios/TabAppearModifier.swift +38 -0
- package/ios/TabBarFontSize.swift +88 -0
- package/ios/TabItem.swift +35 -0
- package/ios/TabItemEventModifier.swift +163 -0
- package/ios/TabView/AnyTabView.swift +7 -0
- package/ios/TabView/LegacyTabView.swift +70 -0
- package/ios/TabView/NewTabView.swift +138 -0
- package/ios/TabViewImpl.swift +566 -0
- package/ios/TabViewProps.swift +109 -0
- package/ios/TabViewProvider.swift +368 -0
- package/ios/react-native-bottom-tabs-Bridging-Header.h +4 -0
- package/lib/module/BottomAccessoryView.js +47 -0
- package/lib/module/BottomAccessoryView.js.map +1 -0
- package/lib/module/BottomAccessoryViewNativeComponent.ts +27 -0
- package/lib/module/DelayedFreeze.js +31 -0
- package/lib/module/DelayedFreeze.js.map +1 -0
- package/lib/module/NativeSVGDecoder.js +5 -0
- package/lib/module/NativeSVGDecoder.js.map +1 -0
- package/lib/module/SceneMap.js +28 -0
- package/lib/module/SceneMap.js.map +1 -0
- package/lib/module/TabView.js +294 -0
- package/lib/module/TabView.js.map +1 -0
- package/lib/module/TabViewNativeComponent.ts +73 -0
- package/lib/module/expo.js +35 -0
- package/lib/module/expo.js.map +1 -0
- package/lib/module/index.js +20 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/BottomTabBarHeightContext.js +5 -0
- package/lib/module/utils/BottomTabBarHeightContext.js.map +1 -0
- package/lib/module/utils/useBottomTabBarHeight.js +12 -0
- package/lib/module/utils/useBottomTabBarHeight.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts +7 -0
- package/lib/typescript/src/BottomAccessoryView.d.ts.map +1 -0
- package/lib/typescript/src/BottomAccessoryView.js +24 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts +15 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/BottomAccessoryViewNativeComponent.js +4 -0
- package/lib/typescript/src/DelayedFreeze.d.ts +7 -0
- package/lib/typescript/src/DelayedFreeze.d.ts.map +1 -0
- package/lib/typescript/src/DelayedFreeze.js +21 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts +5 -0
- package/lib/typescript/src/NativeSVGDecoder.d.ts.map +1 -0
- package/lib/typescript/src/NativeSVGDecoder.js +2 -0
- package/lib/typescript/src/SceneMap.d.ts +9 -0
- package/lib/typescript/src/SceneMap.d.ts.map +1 -0
- package/lib/typescript/src/SceneMap.js +15 -0
- package/lib/typescript/src/TabView.d.ts +219 -0
- package/lib/typescript/src/TabView.d.ts.map +1 -0
- package/lib/typescript/src/TabView.js +157 -0
- package/lib/typescript/src/TabViewNativeComponent.d.ts +59 -0
- package/lib/typescript/src/TabViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/TabViewNativeComponent.js +4 -0
- package/lib/typescript/src/expo.d.ts +6 -0
- package/lib/typescript/src/expo.d.ts.map +1 -0
- package/lib/typescript/src/expo.js +34 -0
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/index.js +11 -0
- package/lib/typescript/src/types.d.ts +31 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/types.js +1 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts +2 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.d.ts.map +1 -0
- package/lib/typescript/src/utils/BottomTabBarHeightContext.js +2 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts +1 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.d.ts.map +1 -0
- package/lib/typescript/src/utils/useBottomTabBarHeight.js +9 -0
- package/lib/typescript/tsconfig.tsbuildinfo +1 -0
- package/package.json +135 -0
- package/react-native-bottom-tabs.podspec +43 -0
- package/src/BottomAccessoryView.tsx +59 -0
- package/src/BottomAccessoryViewNativeComponent.ts +27 -0
- package/src/DelayedFreeze.tsx +31 -0
- package/src/NativeSVGDecoder.ts +5 -0
- package/src/SceneMap.tsx +34 -0
- package/src/TabView.tsx +558 -0
- package/src/TabViewNativeComponent.ts +73 -0
- package/src/expo.ts +58 -0
- package/src/index.tsx +23 -0
- package/src/types.ts +36 -0
- package/src/utils/BottomTabBarHeightContext.ts +5 -0
- 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,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,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
|
+
}
|