diff --git a/.changeset/sour-cases-appear.md b/.changeset/sour-cases-appear.md new file mode 100644 index 0000000..1c2b40a --- /dev/null +++ b/.changeset/sour-cases-appear.md @@ -0,0 +1,5 @@ +--- +'react-native-bottom-tabs': minor +--- + +feat: drop SDWebImage, resolve bunch of build issues diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ca2202..b48763f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,15 +39,7 @@ To run the React Native example app on Android: yarn workspace react-native-bottom-tabs-example android ``` -To run the React Native example app on iOS: - -Make sure to install [`cocoapods-swift-modular-headers`](https://github.com/callstack/cocoapods-swift-modular-headers) gem, otherwise `pod install` will fail. - -```sh -gem install cocoapods-swift-modular-headers -``` - -Next you can install cocoapods. +To run the React Native example app on iOS, you need to install cocoapods. ```sh cd apps/example/ios diff --git a/apps/example/ios/Podfile b/apps/example/ios/Podfile index 3eb951a..939a118 100644 --- a/apps/example/ios/Podfile +++ b/apps/example/ios/Podfile @@ -1,4 +1,4 @@ -plugin 'cocoapods-swift-modular-headers' +ENV['RCT_NEW_ARCH_ENABLED'] = '1' ws_dir = Pathname.new(__dir__) ws_dir = ws_dir.parent until @@ -8,6 +8,4 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" workspace 'ReactNativeBottomTabsExample.xcworkspace' -apply_modular_headers_for_swift_dependencies() - use_test_app! :hermes_enabled => true, :fabric_enabled => true diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 6d6a375..918be67 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -1748,7 +1748,37 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - react-native-bottom-tabs (0.11.1): + - react-native-bottom-tabs (0.11.2): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-bottom-tabs/common (= 0.11.2) + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - SwiftUIIntrospect (~> 1.0) + - Yoga + - react-native-bottom-tabs/common (0.11.2): - boost - DoubleConversion - fast_float @@ -1774,8 +1804,6 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - SDWebImage (>= 5.19.1) - - SDWebImageSVGCoder (>= 1.7.0) - SocketRocket - SwiftUIIntrospect (~> 1.0) - Yoga @@ -2520,11 +2548,6 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - SDWebImage (5.21.1): - - SDWebImage/Core (= 5.21.1) - - SDWebImage/Core (5.21.1) - - SDWebImageSVGCoder (1.8.0): - - SDWebImage/Core (~> 5.6) - SocketRocket (0.7.1) - SwiftUIIntrospect (1.3.0) - Yoga (0.0.0) @@ -2614,8 +2637,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - SDWebImage - - SDWebImageSVGCoder - SocketRocket - SwiftUIIntrospect @@ -2821,7 +2842,7 @@ SPEC CHECKSUMS: React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48 React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696 React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b - react-native-bottom-tabs: fa973f009e321d7d11dbdb761192ce185948a05a + react-native-bottom-tabs: d71dd2e1b69f11d3ed2da2db23016ebdc77f4ba1 react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616 React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3 React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d @@ -2859,12 +2880,10 @@ SPEC CHECKSUMS: RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961 RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c RNVectorIcons: c13cc1db346e960ecd0aafcdd5d0bb458133b9c1 - SDWebImage: f29024626962457f3470184232766516dee8dfea - SDWebImageSVGCoder: 8e10c8f6cc879c7dfb317b284e13dd589379f01c SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d -PODFILE CHECKSUM: e4dd5fac8fa6e00534aac80dc857efbb13ef2723 +PODFILE CHECKSUM: d61a3255405492afdb755f6ddd335fd0f5a84cd3 COCOAPODS: 1.16.2 diff --git a/docs/docs/docs/getting-started/quick-start.mdx b/docs/docs/docs/getting-started/quick-start.mdx index 71e9076..aa93773 100644 --- a/docs/docs/docs/getting-started/quick-start.mdx +++ b/docs/docs/docs/getting-started/quick-start.mdx @@ -19,38 +19,6 @@ If you are going to use [React Navigation / Expo Router Integration](/docs/guide -
- If you use React Native version 0.75 or lower - -- For `@react-native-community/cli` users, open Podfile in ios folder and change minimum iOS version to `14.0` before `pod install` - -```diff -- platform :ios, min_ios_version_supported -+ platform :ios, '14.0' -``` - -- For Expo users, install `expo-build-properties`, open app.json file and update `deploymentTarget` for `ios` as below - -```json -{ - "expo": { - "plugins": [ - [ - "expo-build-properties", - { - "ios": { - "deploymentTarget": "14.0" - } - } - ] - ], - } -} -``` - -
- - ### Expo @@ -64,53 +32,6 @@ Add the library plugin in your `app.json` config file and [create a new build](h } ``` -Then install `expo-build-properties` to enable static linking for iOS by adding `"useFrameworks": "static"` in the plugin. - -```sh -npx expo install expo-build-properties -``` - -```diff -{ - "expo": { - "plugins": [ - "react-native-bottom-tabs", -+ [ -+ "expo-build-properties", -+ { -+ "ios": { -+ "useFrameworks": "static" -+ } -+ } -+ ] -+ ] - } -} -``` - -Alternatively, you can avoid enabling static linking (which can cause problems with your existing packages) by adding the following in the `expo-build-properties` plugin. - -```diff -{ - "expo": { - "plugins": [ - "react-native-bottom-tabs", -+ [ -+ "expo-build-properties", -+ { -+ "ios": { -+ "extraPods": [ -+ { name: "SDWebImage", modular_headers: true }, // Work around for not enabling static framework, required for react-native-bottom-tabs -+ { name: "SDWebImageSVGCoder", modular_headers: true } -+ ] -+ } -+ } -+ ] -+ ] - } -} -``` - :::warning This library is not supported in [Expo Go](https://expo.dev/go). @@ -133,12 +54,6 @@ Edit `android/app/src/main/res/values/styles.xml` to inherit from provided theme Here you can read more about [Android Native Styling](/docs/guides/android-native-styling). -To enable static linking for iOS, Open the `./ios/Podfile` file and add the following: - -```ruby -use_frameworks! :linkage => :static -``` - ## Example usage diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h new file mode 100644 index 0000000..3bd5d35 --- /dev/null +++ b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h @@ -0,0 +1,33 @@ +#ifdef __cplusplus + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class RNCTabViewComponentDescriptor final + : public ConcreteComponentDescriptor { + public: + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + + void adopt(ShadowNode& shadowNode) const override { + ConcreteComponentDescriptor::adopt(shadowNode); + +#if !defined(ANDROID) + auto &tabViewShadowNode = + static_cast(shadowNode); + + std::weak_ptr imageLoader = + contextContainer_->at>("RCTImageLoader"); + tabViewShadowNode.setImageLoader(imageLoader); +#endif + } +}; + + +} // namespace facebook::react + +#endif diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp new file mode 100644 index 0000000..dcdcaf2 --- /dev/null +++ b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp @@ -0,0 +1,19 @@ +#include + +namespace facebook::react { + +extern const char RNCTabViewComponentName[] = "RNCTabView"; + +void RNCTabViewShadowNode::setImageLoader( + std::weak_ptr imageLoader) { + getStateDataMutable().setImageLoader(imageLoader); +} + +RNCTabViewShadowNode::StateData & +RNCTabViewShadowNode::getStateDataMutable() { + ensureUnsealed(); + return const_cast(getStateData()); +} + + +} // namespace facebook::react diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h new file mode 100644 index 0000000..43e37fa --- /dev/null +++ b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h @@ -0,0 +1,35 @@ +#ifdef __cplusplus + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +JSI_EXPORT extern const char RNCTabViewComponentName[]; + +/* + * `ShadowNode` for component. + */ +class JSI_EXPORT RNCTabViewShadowNode final: public ConcreteViewShadowNode< +RNCTabViewComponentName, +RNCTabViewProps, +RNCTabViewEventEmitter, +RNCTabViewState> { + +public: + using ConcreteViewShadowNode::ConcreteViewShadowNode; + using StateData = ConcreteViewShadowNode::ConcreteStateData; + + void setImageLoader(std::weak_ptr imageLoader); + + StateData &getStateDataMutable(); +}; + +} + +#endif diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp new file mode 100644 index 0000000..e4a536d --- /dev/null +++ b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp @@ -0,0 +1,15 @@ +#include + +namespace facebook::react { + +void RNCTabViewState::setImageLoader( + std::weak_ptr imageLoader) { + imageLoader_ = imageLoader; +} + +std::weak_ptr RNCTabViewState::getImageLoader() + const noexcept { + return imageLoader_; +} + +} // namespace facebook::react diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h new file mode 100644 index 0000000..cb4e2d0 --- /dev/null +++ b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h @@ -0,0 +1,25 @@ +#ifdef __cplusplus + +#pragma once + +#include +#ifdef RN_SERIALIZABLE_STATE +#include +#endif + +namespace facebook::react { + +class RNCTabViewState final { + public: + RNCTabViewState() = default; + + void setImageLoader(std::weak_ptr imageLoader); + std::weak_ptr getImageLoader() const noexcept; + + private: + std::weak_ptr imageLoader_; +}; + +} // namespace facebook::react + +#endif diff --git a/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm index 9b5a2b3..c41f931 100644 --- a/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm @@ -1,7 +1,7 @@ #ifdef RCT_NEW_ARCH_ENABLED #import "RCTTabViewComponentView.h" -#import +#import #import #import #import @@ -19,6 +19,7 @@ #import #import "RCTImagePrimitivesConversions.h" #import "RCTConversions.h" +#import #if TARGET_OS_OSX typedef NSView PlatformView; @@ -200,6 +201,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & return result; } +- (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState +{ + auto _state = std::static_pointer_cast(state); + auto data = _state->getData(); + if (auto imgLoaderPtr = _state.get()->getData().getImageLoader().lock()) { + [_tabViewProvider setImageLoader:unwrapManagedObject(imgLoaderPtr)]; + } +} + // MARK: TabViewProviderDelegate - (void)onPageSelectedWithKey:(NSString *)key reactTag:(NSNumber *)reactTag { diff --git a/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm b/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm index 491bdc6..3a761d1 100644 --- a/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm +++ b/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm @@ -73,7 +73,10 @@ - (NSView *)view - (UIView *) view #endif { - return [[TabViewProvider alloc] initWithDelegate:self]; + TabViewProvider *tabview = [[TabViewProvider alloc] initWithDelegate:self]; + RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]]; + [tabview setImageLoader:imageLoader]; + return tabview; } @end diff --git a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h new file mode 100644 index 0000000..3b5ac54 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h @@ -0,0 +1,19 @@ +#if TARGET_OS_OSX +#import +typedef NSImage PlatformImage; +#else +#import +typedef UIImage PlatformImage; +#endif + + +@interface CoreSVGWrapper : NSObject + ++ (instancetype)shared; + +- (PlatformImage *)imageFromSVGData:(NSData *)data; + ++ (BOOL)isSVGData:(NSData *)data; ++ (BOOL)supportsVectorSVGImage; + +@end diff --git a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm new file mode 100644 index 0000000..44a554d --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm @@ -0,0 +1,177 @@ +#import "CoreSVG.h" +#import +#import + +#define kSVGTagEnd @"" + +typedef struct CF_BRIDGED_TYPE(id) CGSVGDocument *CGSVGDocumentRef; + +static CGSVGDocumentRef (*CoreSVGDocumentRetain)(CGSVGDocumentRef); +static void (*CoreSVGDocumentRelease)(CGSVGDocumentRef); +static CGSVGDocumentRef (*CoreSVGDocumentCreateFromData)(CFDataRef data, CFDictionaryRef options); +static void (*CoreSVGContextDrawSVGDocument)(CGContextRef context, CGSVGDocumentRef document); +static CGSize (*CoreSVGDocumentGetCanvasSize)(CGSVGDocumentRef document); + +#if !TARGET_OS_OSX +static SEL CoreSVGImageWithDocumentSEL = NULL; +static SEL CoreSVGDocumentSEL = NULL; +#endif +#if TARGET_OS_OSX +static Class CoreSVGImageRepClass = NULL; +static Ivar CoreSVGImageRepDocumentIvar = NULL; +#endif + +static inline NSString *Base64DecodedString(NSString *base64String) { + NSData *data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSDataBase64DecodingIgnoreUnknownCharacters]; + if (!data) { + return nil; + } + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + +@implementation CoreSVGWrapper + ++ (instancetype)shared { + static dispatch_once_t onceToken; + static CoreSVGWrapper *wrapper; + dispatch_once(&onceToken, ^{ + wrapper = [[CoreSVGWrapper alloc] init]; + }); + return wrapper; +} + ++ (void)initialize { + CoreSVGDocumentRetain = (CGSVGDocumentRef (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJldGFpbg==").UTF8String); + CoreSVGDocumentRelease = (void (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJlbGVhc2U=").UTF8String); + CoreSVGDocumentCreateFromData = (CGSVGDocumentRef (*)(CFDataRef data, CFDictionaryRef options))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudENyZWF0ZUZyb21EYXRh").UTF8String); + CoreSVGContextDrawSVGDocument = (void (*)(CGContextRef context, CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dDb250ZXh0RHJhd1NWR0RvY3VtZW50").UTF8String); + CoreSVGDocumentGetCanvasSize = (CGSize (*)(CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudEdldENhbnZhc1NpemU=").UTF8String); + +#if !TARGET_OS_OSX + CoreSVGImageWithDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X2ltYWdlV2l0aENHU1ZHRG9jdW1lbnQ6")); + CoreSVGDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X0NHU1ZHRG9jdW1lbnQ=")); +#endif +#if TARGET_OS_OSX + CoreSVGImageRepClass = NSClassFromString(Base64DecodedString(@"X05TU1ZHSW1hZ2VSZXA=")); + if (CoreSVGImageRepClass) { + CoreSVGImageRepDocumentIvar = class_getInstanceVariable(CoreSVGImageRepClass, Base64DecodedString(@"X2RvY3VtZW50").UTF8String); + } +#endif +} + +- (PlatformImage *)imageFromSVGData:(NSData *)data { + if (!data) { + return nil; + } + + if (![self.class supportsVectorSVGImage]) { + return nil; + } + + return [self createVectorSVGWithData:data]; +} + +- (PlatformImage *)createVectorSVGWithData:(NSData *)data { + if (!data) return nil; + + PlatformImage *image; + +#if TARGET_OS_OSX + if (!CoreSVGImageRepClass) { + return nil; + } + Class imageRepClass = CoreSVGImageRepClass; + NSImageRep *imageRep = [[imageRepClass alloc] initWithData:data]; + if (!imageRep) { + return nil; + } + image = [[NSImage alloc] initWithSize:imageRep.size]; + [image addRepresentation:imageRep]; +#else + if (!CoreSVGDocumentCreateFromData || !CoreSVGDocumentRelease) { + return nil; + } + CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL); + + if (!document) { + return nil; + } + + image = ((UIImage *(*)(id,SEL,CGSVGDocumentRef))[UIImage.class methodForSelector:CoreSVGImageWithDocumentSEL])(UIImage.class, CoreSVGImageWithDocumentSEL, document); + CoreSVGDocumentRelease(document); +#endif + +#if TARGET_OS_OSX + // Test render to catch potential CoreSVG crashes on macOS + NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:1 + pixelsHigh:1 + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bytesPerRow:0 + bitsPerPixel:0]; + + NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap]; + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:context]; + + @try { + [image drawInRect:NSMakeRect(0, 0, 1, 1)]; + } @catch (...) { + [NSGraphicsContext restoreGraphicsState]; + return nil; + } + + [NSGraphicsContext restoreGraphicsState]; +#else + // Test render to catch potential CoreSVG crashes + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1.0); + @try { + [image drawInRect:CGRectMake(0, 0, 1, 1)]; + } @catch (...) { + UIGraphicsEndImageContext(); + return nil; + } + UIGraphicsEndImageContext(); +#endif + + + return image; +} + ++ (BOOL)isSVGData:(NSData *)data { + if (!data) { + return NO; + } + // Check end with SVG tag + return [data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range: NSMakeRange(data.length - MIN(100, data.length), MIN(100, data.length))].location != NSNotFound; +} + ++ (BOOL)supportsVectorSVGImage { + static dispatch_once_t onceToken; + static BOOL supports; + dispatch_once(&onceToken, ^{ +#if TARGET_OS_OSX + // macOS 10.15+ supports SVG built-in rendering, use selector to check is more accurate + if (CoreSVGImageRepClass) { + supports = YES; + } else { + supports = NO; + } +#else + // iOS 13+ supports SVG built-in rendering, use selector to check is more accurate + if ([UIImage respondsToSelector:CoreSVGImageWithDocumentSEL]) { + supports = YES; + } else { + supports = NO; + } +#endif + }); + return supports; +} + +@end diff --git a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h new file mode 100644 index 0000000..5e7fde1 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h @@ -0,0 +1,10 @@ +#ifdef __cplusplus + +#import +#import + +@interface SvgDecoder : NSObject + +@end + +#endif diff --git a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm new file mode 100644 index 0000000..1bbddd0 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm @@ -0,0 +1,38 @@ +#import "SvgDecoder.h" +#import "CoreSVG.h" + +@implementation SvgDecoder + +RCT_EXPORT_MODULE() + +- (BOOL)canDecodeImageData:(NSData *)imageData +{ + return [CoreSVGWrapper isSVGData:imageData]; +} + +- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +{ + UIImage *image = [CoreSVGWrapper.shared imageFromSVGData:imageData]; + + if (image) { + completionHandler(nil, image); + } else { + NSError *error = [NSError errorWithDomain:@"SVGDecoderErrorDomain" + code:2 + userInfo:@{NSLocalizedDescriptionKey: @"Failed to render SVG to image"}]; + completionHandler(error, nil); + } + return ^{}; +} + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index 6a3e4ea..e207a16 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -1,7 +1,5 @@ import Foundation import React -import SDWebImage -import SDWebImageSVGCoder import SwiftUI @objcMembers @@ -48,6 +46,7 @@ public final class TabInfo: NSObject { } @objc public class TabViewProvider: PlatformView { + private var imageLoader: RCTImageLoaderProtocol? private weak var delegate: TabViewProviderDelegate? private var props = TabViewProps() private var hostingController: PlatformHostingController? @@ -173,7 +172,11 @@ public final class TabInfo: NSObject { @objc public convenience init(delegate: TabViewProviderDelegate) { self.init() self.delegate = delegate - SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) + } + + @objc public func setImageLoader(_ imageLoader: RCTImageLoader) { + self.imageLoader = imageLoader + loadIcons(icons) } override public func didUpdateReactSubviews() { @@ -238,40 +241,27 @@ public final class TabInfo: NSObject { private func loadIcons(_ icons: NSArray?) { // TODO: Diff the arrays and update only changed items. // Now if the user passes `unfocusedIcon` we update every item. - guard let imageSources = icons as? [RCTImageSource?] else { return } - - for (index, imageSource) in imageSources.enumerated() { - guard let imageSource, - let url = imageSource.request.url else { continue } - - let isSVG = url.pathExtension.lowercased() == "svg" - - var options: SDWebImageOptions = [.continueInBackground, - .scaleDownLargeImages, - .avoidDecodeImage, - .highPriority] - - if isSVG { - options.insert(.decodeFirstFrameOnly) - } - - let context: [SDWebImageContextOption: Any]? = isSVG ? [ - .imageThumbnailPixelSize: iconSize, - .imagePreserveAspectRatio: true - ] : nil - - SDWebImageManager.shared.loadImage( - with: url, - options: options, - context: context, - progress: nil - ) { [weak self] image, _, _, _, _, _ in - guard let self else { return } - DispatchQueue.main.async { - if let image { - self.props.icons[index] = image.resizeImageTo(size: self.iconSize) - } - } + if let imageSources = icons as? [RCTImageSource?] { + for (index, imageSource) in imageSources.enumerated() { + guard let imageSource, let imageLoader else { continue } + imageLoader.loadImage( + with: imageSource.request, + size: imageSource.size, + scale: imageSource.scale, + clipped: true, + resizeMode: RCTResizeMode.contain, + progressBlock: { _,_ in }, + partialLoad: { _ in }, + completionBlock: { error, image in + if error != nil { + print("[TabView] Error loading image: \(error!.localizedDescription)") + return + } + guard let image else { return } + DispatchQueue.main.async { + self.props.icons[index] = image.resizeImageTo(size: self.iconSize) + } + }) } } } diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json index f4390bb..cba0caa 100644 --- a/packages/react-native-bottom-tabs/package.json +++ b/packages/react-native-bottom-tabs/package.json @@ -118,7 +118,7 @@ }, "codegenConfig": { "name": "RNCTabView", - "type": "components", + "type": "all", "jsSrcsDir": "./src", "android": { "javaPackageName": "com.rcttabview" @@ -126,6 +126,11 @@ "ios": { "componentProvider": { "RNCTabView": "RCTTabViewComponentView" + }, + "modulesConformingToProtocol": { + "RCTImageDataDecoder": [ + "SvgDecoder" + ] } } } diff --git a/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec b/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec index d4b9289..befef4b 100644 --- a/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec +++ b/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec @@ -1,6 +1,7 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1' Pod::Spec.new do |s| s.name = "react-native-bottom-tabs" @@ -20,9 +21,14 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" s.static_framework = true + if new_arch_enabled + s.subspec "common" do |ss| + ss.source_files = "common/cpp/**/*.{cpp,h}" + ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/common/cpp\"" } + end + end + s.dependency "SwiftUIIntrospect", '~> 1.0' - s.dependency 'SDWebImage', '>= 5.19.1' - s.dependency 'SDWebImageSVGCoder', '>= 1.7.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' diff --git a/packages/react-native-bottom-tabs/src/NativeSVGDecoder.ts b/packages/react-native-bottom-tabs/src/NativeSVGDecoder.ts new file mode 100644 index 0000000..dad9f8f --- /dev/null +++ b/packages/react-native-bottom-tabs/src/NativeSVGDecoder.ts @@ -0,0 +1,5 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule {} + +export default TurboModuleRegistry.getEnforcing('SvgDecoder'); diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts index f374688..ddc8bee 100644 --- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts +++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts @@ -60,4 +60,6 @@ export interface TabViewProps extends ViewProps { fontSize?: Int32; } -export default codegenNativeComponent('RNCTabView'); +export default codegenNativeComponent('RNCTabView', { + interfaceOnly: true, +});