From fdde3cae2e785588609497930103fd440c38de1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Sun, 14 Sep 2025 15:30:01 +0800 Subject: [PATCH 1/3] feat: drop sdwebimage, implement svg decoder --- .changeset/sour-cases-appear.md | 5 + CONTRIBUTING.md | 10 +- apps/example/ios/Podfile | 4 +- apps/example/ios/Podfile.lock | 17 +- .../docs/docs/getting-started/quick-start.mdx | 85 -------- .../ios/Fabric/RCTTabViewComponentView.mm | 4 +- .../ios/RCTTabViewViewManager.mm | 3 +- .../ios/SVG/CoreSVG.h | 14 ++ .../ios/SVG/CoreSVG.mm | 204 ++++++++++++++++++ .../ios/SVG/SvgDecoder.h | 10 + .../ios/SVG/SvgDecoder.mm | 55 +++++ .../ios/TabViewProvider.swift | 62 +++--- .../react-native-bottom-tabs/package.json | 7 +- .../react-native-bottom-tabs.podspec | 2 - .../src/NativeSVGDecoder.ts | 5 + 15 files changed, 333 insertions(+), 154 deletions(-) create mode 100644 .changeset/sour-cases-appear.md create mode 100644 packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h create mode 100644 packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm create mode 100644 packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h create mode 100644 packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm create mode 100644 packages/react-native-bottom-tabs/src/NativeSVGDecoder.ts 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..5cec1c0 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -1748,7 +1748,7 @@ 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 @@ -1774,8 +1774,6 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - SDWebImage (>= 5.19.1) - - SDWebImageSVGCoder (>= 1.7.0) - SocketRocket - SwiftUIIntrospect (~> 1.0) - Yoga @@ -2520,11 +2518,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 +2607,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - SDWebImage - - SDWebImageSVGCoder - SocketRocket - SwiftUIIntrospect @@ -2821,7 +2812,7 @@ SPEC CHECKSUMS: React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48 React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696 React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b - react-native-bottom-tabs: fa973f009e321d7d11dbdb761192ce185948a05a + react-native-bottom-tabs: ca7796411ccc78911e66cca41a97a18cede4d582 react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616 React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3 React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d @@ -2859,12 +2850,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/ios/Fabric/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm index 9b5a2b3..ffddecf 100644 --- a/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm @@ -69,7 +69,9 @@ - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); - _tabViewProvider = [[TabViewProvider alloc] initWithDelegate:self]; + // TODO: Find a better way to retrieve ImageLoader module. + RCTImageLoader *imageLoader = [[RCTBridge currentBridge] moduleForName:@"RCTImageLoader" lazilyLoadIfNecessary:YES]; + _tabViewProvider = [[TabViewProvider alloc] initWithDelegate:self imageLoader:imageLoader]; self.contentView = _tabViewProvider; _props = defaultProps; } diff --git a/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm b/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm index 491bdc6..7c15ddc 100644 --- a/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm +++ b/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm @@ -73,7 +73,8 @@ - (NSView *)view - (UIView *) view #endif { - return [[TabViewProvider alloc] initWithDelegate:self]; + RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]]; + return [[TabViewProvider alloc] initWithDelegate:self imageLoader:imageLoader]; } @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..60730c9 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h @@ -0,0 +1,14 @@ +#import + +@interface CoreSVGWrapper : NSObject + ++ (instancetype)sharedWrapper; + +- (UIImage *)imageFromSVGData:(NSData *)data; +- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize; +- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio; + +- (BOOL)isSVGData:(NSData *)data; ++ (BOOL)supportsVectorSVG; + +@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..747d2fa --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm @@ -0,0 +1,204 @@ +#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_IOS || TARGET_OS_WATCH +static SEL CoreSVGImageWithDocumentSEL = 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)sharedWrapper { + 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_IOS || TARGET_OS_WATCH + CoreSVGImageWithDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X2ltYWdlV2l0aENHU1ZHRG9jdW1lbnQ6")); +#endif +} + +- (UIImage *)imageFromSVGData:(NSData *)data { + return [self imageFromSVGData:data targetSize:CGSizeZero preserveAspectRatio:YES]; +} + +- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize { + return [self imageFromSVGData:data targetSize:targetSize preserveAspectRatio:YES]; +} + +- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { + if (!data) { + return nil; + } + + if (CGSizeEqualToSize(targetSize, CGSizeZero) && [self.class supportsVectorSVG]) { + return [self createVectorSVGWithData:data]; + } else { + return [self createBitmapSVGWithData:data targetSize:targetSize preserveAspectRatio:preserveAspectRatio]; + } +} + +- (UIImage *)createVectorSVGWithData:(NSData *)data { + if (!data) return nil; + +#if TARGET_OS_IOS || TARGET_OS_WATCH + CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL); + if (!document) { + return nil; + } + + UIImage *image = ((UIImage *(*)(id,SEL,CGSVGDocumentRef))[UIImage.class methodForSelector:CoreSVGImageWithDocumentSEL])(UIImage.class, CoreSVGImageWithDocumentSEL, document); + CoreSVGDocumentRelease(document); + + // 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(); + + return image; +#else + return [self createBitmapSVGWithData:data targetSize:CGSizeZero preserveAspectRatio:YES]; +#endif +} + +- (UIImage *)createBitmapSVGWithData:(NSData *)data targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { + if (!data) return nil; + + CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL); + if (!document) { + return nil; + } + + CGSize size = CoreSVGDocumentGetCanvasSize(document); + if (size.width <= 0 || size.height <= 0) { + CoreSVGDocumentRelease(document); + return nil; + } + + CGFloat xScale, yScale; + + if (CGSizeEqualToSize(targetSize, CGSizeZero)) { + targetSize = size; + xScale = yScale = 1.0; + } else { + CGFloat xRatio = targetSize.width / size.width; + CGFloat yRatio = targetSize.height / size.height; + + if (preserveAspectRatio) { + if (targetSize.width <= 0) { + yScale = yRatio; + xScale = yRatio; + targetSize.width = size.width * xScale; + } else if (targetSize.height <= 0) { + xScale = xRatio; + yScale = xRatio; + targetSize.height = size.height * yScale; + } else { + xScale = MIN(xRatio, yRatio); + yScale = MIN(xRatio, yRatio); + targetSize.width = size.width * xScale; + targetSize.height = size.height * yScale; + } + } else { + if (targetSize.width <= 0) { + targetSize.width = size.width; + yScale = yRatio; + xScale = 1.0; + } else if (targetSize.height <= 0) { + xScale = xRatio; + yScale = 1.0; + targetSize.height = size.height; + } else { + xScale = xRatio; + yScale = yRatio; + } + } + } + + CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale); + CGAffineTransform offsetTransform = CGAffineTransformIdentity; + + if (preserveAspectRatio) { + CGFloat offsetX = (targetSize.width / xScale - size.width) / 2; + CGFloat offsetY = (targetSize.height / yScale - size.height) / 2; + offsetTransform = CGAffineTransformMakeTranslation(offsetX, offsetY); + } + + UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + +#if TARGET_OS_IOS || TARGET_OS_WATCH + CGContextTranslateCTM(context, 0, targetSize.height); + CGContextScaleCTM(context, 1, -1); +#endif + + CGContextConcatCTM(context, scaleTransform); + CGContextConcatCTM(context, offsetTransform); + + CoreSVGContextDrawSVGDocument(context, document); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + CoreSVGDocumentRelease(document); + + return image; +} + +- (BOOL)isSVGData:(NSData *)data { + if (!data) return NO; + + NSRange searchRange = NSMakeRange(MAX(0, (NSInteger)data.length - 100), MIN(100, data.length)); + return [data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] + options:NSDataSearchBackwards + range:searchRange].location != NSNotFound; +} + ++ (BOOL)supportsVectorSVG { + static dispatch_once_t onceToken; + static BOOL supports; + dispatch_once(&onceToken, ^{ +#if TARGET_OS_IOS || TARGET_OS_WATCH + supports = [UIImage respondsToSelector:CoreSVGImageWithDocumentSEL]; +#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..1a886d0 --- /dev/null +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm @@ -0,0 +1,55 @@ +#import "SvgDecoder.h" +#import "CoreSVG.h" + +@implementation SvgDecoder + +RCT_EXPORT_MODULE() + +- (BOOL)canDecodeImageData:(NSData *)imageData +{ + if (!imageData || imageData.length == 0) { + return NO; + } + + NSString *dataString = [[NSString alloc] initWithData:imageData encoding:NSUTF8StringEncoding]; + + if (!dataString) { + return NO; + } + + NSString *lowercaseString = [dataString lowercaseString]; + BOOL containsSVGTag = [lowercaseString containsString:@")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..c1cc8d3 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? @@ -170,10 +169,10 @@ public final class TabInfo: NSObject { } } - @objc public convenience init(delegate: TabViewProviderDelegate) { + @objc public convenience init(delegate: TabViewProviderDelegate, imageLoader: RCTImageLoader) { self.init() self.delegate = delegate - SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) + self.imageLoader = imageLoader } override public func didUpdateReactSubviews() { @@ -238,40 +237,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..d4e9060 100644 --- a/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec +++ b/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec @@ -21,8 +21,6 @@ Pod::Spec.new do |s| s.static_framework = true 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'); From 12c197268483418f04b6065b7d373bc5a0ff91c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Sun, 14 Sep 2025 23:09:19 +0800 Subject: [PATCH 2/3] fix: properly retrive RCTImageLoader --- apps/example/ios/Podfile.lock | 32 ++++++++++++++++- .../RNCTabViewComponentDescriptor.h | 33 +++++++++++++++++ .../RNCTabView/RNCTabViewShadowNode.cpp | 19 ++++++++++ .../RNCTabView/RNCTabViewShadowNode.h | 35 +++++++++++++++++++ .../components/RNCTabView/RNCTabViewState.cpp | 15 ++++++++ .../components/RNCTabView/RNCTabViewState.h | 25 +++++++++++++ .../ios/Fabric/RCTTabViewComponentView.mm | 16 ++++++--- .../ios/RCTTabViewViewManager.mm | 4 ++- .../ios/SVG/CoreSVG.h | 4 +++ .../ios/SVG/CoreSVG.mm | 4 +++ .../ios/SVG/SvgDecoder.mm | 9 ++++- .../ios/TabViewProvider.swift | 6 +++- .../react-native-bottom-tabs.podspec | 8 +++++ .../src/TabViewNativeComponent.ts | 4 ++- 14 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h create mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp create mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h create mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.cpp create mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 5cec1c0..918be67 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -1749,6 +1749,36 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - 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 @@ -2812,7 +2842,7 @@ SPEC CHECKSUMS: React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48 React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696 React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b - react-native-bottom-tabs: ca7796411ccc78911e66cca41a97a18cede4d582 + react-native-bottom-tabs: d71dd2e1b69f11d3ed2da2db23016ebdc77f4ba1 react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616 React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3 React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d 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 ffddecf..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; @@ -69,9 +70,7 @@ - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); - // TODO: Find a better way to retrieve ImageLoader module. - RCTImageLoader *imageLoader = [[RCTBridge currentBridge] moduleForName:@"RCTImageLoader" lazilyLoadIfNecessary:YES]; - _tabViewProvider = [[TabViewProvider alloc] initWithDelegate:self imageLoader:imageLoader]; + _tabViewProvider = [[TabViewProvider alloc] initWithDelegate:self]; self.contentView = _tabViewProvider; _props = defaultProps; } @@ -202,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 7c15ddc..3a761d1 100644 --- a/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm +++ b/packages/react-native-bottom-tabs/ios/RCTTabViewViewManager.mm @@ -73,8 +73,10 @@ - (NSView *)view - (UIView *) view #endif { + TabViewProvider *tabview = [[TabViewProvider alloc] initWithDelegate:self]; RCTImageLoader *imageLoader = [self.bridge moduleForClass:[RCTImageLoader class]]; - return [[TabViewProvider alloc] initWithDelegate:self imageLoader:imageLoader]; + [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 index 60730c9..23e76e9 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h @@ -1,3 +1,5 @@ +#if !TARGET_OS_OSX + #import @interface CoreSVGWrapper : NSObject @@ -12,3 +14,5 @@ + (BOOL)supportsVectorSVG; @end + +#endif diff --git a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm index 747d2fa..3fb8836 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm @@ -1,3 +1,5 @@ +#if !TARGET_OS_OSX + #import "CoreSVG.h" #import #import @@ -202,3 +204,5 @@ + (BOOL)supportsVectorSVG { } @end + +#endif diff --git a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm index 1a886d0..a1ca855 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm @@ -7,6 +7,10 @@ @implementation SvgDecoder - (BOOL)canDecodeImageData:(NSData *)imageData { +#if TARGET_OS_OSX + return NO; +#endif + if (!imageData || imageData.length == 0) { return NO; } @@ -32,6 +36,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData resizeMode:(RCTResizeMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { +#if !TARGET_OS_OSX UIImage *image = [[CoreSVGWrapper alloc] imageFromSVGData:imageData targetSize:size]; if (image) { @@ -42,8 +47,10 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData userInfo:@{NSLocalizedDescriptionKey: @"Failed to render SVG to image"}]; completionHandler(error, nil); } - return ^{}; +#else + return ^{}; +#endif } - (std::shared_ptr)getTurboModule: diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index c1cc8d3..e207a16 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -169,10 +169,14 @@ public final class TabInfo: NSObject { } } - @objc public convenience init(delegate: TabViewProviderDelegate, imageLoader: RCTImageLoader) { + @objc public convenience init(delegate: TabViewProviderDelegate) { self.init() self.delegate = delegate + } + + @objc public func setImageLoader(_ imageLoader: RCTImageLoader) { self.imageLoader = imageLoader + loadIcons(icons) } override public func didUpdateReactSubviews() { 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 d4e9060..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,6 +21,13 @@ 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.pod_target_xcconfig = { 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, +}); From a862617c9c97fcf63b6528e17fcdde9708f2bf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 15 Sep 2025 00:11:29 +0800 Subject: [PATCH 3/3] fix: simplify the svg decoder --- .../ios/SVG/CoreSVG.h | 21 +- .../ios/SVG/CoreSVG.mm | 301 ++++++++---------- .../ios/SVG/SvgDecoder.mm | 28 +- 3 files changed, 148 insertions(+), 202 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h index 23e76e9..3b5ac54 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.h @@ -1,18 +1,19 @@ -#if !TARGET_OS_OSX - +#if TARGET_OS_OSX +#import +typedef NSImage PlatformImage; +#else #import +typedef UIImage PlatformImage; +#endif + @interface CoreSVGWrapper : NSObject -+ (instancetype)sharedWrapper; ++ (instancetype)shared; -- (UIImage *)imageFromSVGData:(NSData *)data; -- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize; -- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio; +- (PlatformImage *)imageFromSVGData:(NSData *)data; -- (BOOL)isSVGData:(NSData *)data; -+ (BOOL)supportsVectorSVG; ++ (BOOL)isSVGData:(NSData *)data; ++ (BOOL)supportsVectorSVGImage; @end - -#endif diff --git a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm index 3fb8836..44a554d 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm +++ b/packages/react-native-bottom-tabs/ios/SVG/CoreSVG.mm @@ -1,5 +1,3 @@ -#if !TARGET_OS_OSX - #import "CoreSVG.h" #import #import @@ -14,195 +12,166 @@ static void (*CoreSVGContextDrawSVGDocument)(CGContextRef context, CGSVGDocumentRef document); static CGSize (*CoreSVGDocumentGetCanvasSize)(CGSVGDocumentRef document); -#if TARGET_OS_IOS || TARGET_OS_WATCH +#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]; + NSData *data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSDataBase64DecodingIgnoreUnknownCharacters]; + if (!data) { + return nil; + } + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } @implementation CoreSVGWrapper -+ (instancetype)sharedWrapper { - static dispatch_once_t onceToken; - static CoreSVGWrapper *wrapper; - dispatch_once(&onceToken, ^{ - wrapper = [[CoreSVGWrapper alloc] init]; - }); - return wrapper; ++ (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_IOS || TARGET_OS_WATCH - CoreSVGImageWithDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X2ltYWdlV2l0aENHU1ZHRG9jdW1lbnQ6")); + 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 } -- (UIImage *)imageFromSVGData:(NSData *)data { - return [self imageFromSVGData:data targetSize:CGSizeZero preserveAspectRatio:YES]; -} - -- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize { - return [self imageFromSVGData:data targetSize:targetSize preserveAspectRatio:YES]; +- (PlatformImage *)imageFromSVGData:(NSData *)data { + if (!data) { + return nil; + } + + if (![self.class supportsVectorSVGImage]) { + return nil; + } + + return [self createVectorSVGWithData:data]; } -- (UIImage *)imageFromSVGData:(NSData *)data targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { - if (!data) { - return nil; - } - - if (CGSizeEqualToSize(targetSize, CGSizeZero) && [self.class supportsVectorSVG]) { - return [self createVectorSVGWithData:data]; - } else { - return [self createBitmapSVGWithData:data targetSize:targetSize preserveAspectRatio:preserveAspectRatio]; - } -} - -- (UIImage *)createVectorSVGWithData:(NSData *)data { - if (!data) return nil; - -#if TARGET_OS_IOS || TARGET_OS_WATCH - CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL); - if (!document) { - return nil; - } - - UIImage *image = ((UIImage *(*)(id,SEL,CGSVGDocumentRef))[UIImage.class methodForSelector:CoreSVGImageWithDocumentSEL])(UIImage.class, CoreSVGImageWithDocumentSEL, document); - CoreSVGDocumentRelease(document); - - // 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(); - - return image; +- (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 - return [self createBitmapSVGWithData:data targetSize:CGSizeZero preserveAspectRatio:YES]; -#endif -} - -- (UIImage *)createBitmapSVGWithData:(NSData *)data targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { - if (!data) return nil; - - CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL); - if (!document) { - return nil; - } - - CGSize size = CoreSVGDocumentGetCanvasSize(document); - if (size.width <= 0 || size.height <= 0) { - CoreSVGDocumentRelease(document); - return nil; - } - - CGFloat xScale, yScale; - - if (CGSizeEqualToSize(targetSize, CGSizeZero)) { - targetSize = size; - xScale = yScale = 1.0; - } else { - CGFloat xRatio = targetSize.width / size.width; - CGFloat yRatio = targetSize.height / size.height; - - if (preserveAspectRatio) { - if (targetSize.width <= 0) { - yScale = yRatio; - xScale = yRatio; - targetSize.width = size.width * xScale; - } else if (targetSize.height <= 0) { - xScale = xRatio; - yScale = xRatio; - targetSize.height = size.height * yScale; - } else { - xScale = MIN(xRatio, yRatio); - yScale = MIN(xRatio, yRatio); - targetSize.width = size.width * xScale; - targetSize.height = size.height * yScale; - } - } else { - if (targetSize.width <= 0) { - targetSize.width = size.width; - yScale = yRatio; - xScale = 1.0; - } else if (targetSize.height <= 0) { - xScale = xRatio; - yScale = 1.0; - targetSize.height = size.height; - } else { - xScale = xRatio; - yScale = yRatio; - } - } - } - - CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale); - CGAffineTransform offsetTransform = CGAffineTransformIdentity; - - if (preserveAspectRatio) { - CGFloat offsetX = (targetSize.width / xScale - size.width) / 2; - CGFloat offsetY = (targetSize.height / yScale - size.height) / 2; - offsetTransform = CGAffineTransformMakeTranslation(offsetX, offsetY); - } - - UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - -#if TARGET_OS_IOS || TARGET_OS_WATCH - CGContextTranslateCTM(context, 0, targetSize.height); - CGContextScaleCTM(context, 1, -1); + 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 - - CGContextConcatCTM(context, scaleTransform); - CGContextConcatCTM(context, offsetTransform); - - CoreSVGContextDrawSVGDocument(context, document); - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + +#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(); - - CoreSVGDocumentRelease(document); - - return image; + return nil; + } + UIGraphicsEndImageContext(); +#endif + + + return image; } -- (BOOL)isSVGData:(NSData *)data { - if (!data) return NO; - - NSRange searchRange = NSMakeRange(MAX(0, (NSInteger)data.length - 100), MIN(100, data.length)); - return [data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] - options:NSDataSearchBackwards - range:searchRange].location != NSNotFound; ++ (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)supportsVectorSVG { - static dispatch_once_t onceToken; - static BOOL supports; - dispatch_once(&onceToken, ^{ -#if TARGET_OS_IOS || TARGET_OS_WATCH - supports = [UIImage respondsToSelector:CoreSVGImageWithDocumentSEL]; ++ (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 - supports = NO; + // 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; + }); + return supports; } @end - -#endif diff --git a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm index a1ca855..1bbddd0 100644 --- a/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm +++ b/packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm @@ -7,37 +7,16 @@ @implementation SvgDecoder - (BOOL)canDecodeImageData:(NSData *)imageData { -#if TARGET_OS_OSX - return NO; -#endif - - if (!imageData || imageData.length == 0) { - return NO; - } - - NSString *dataString = [[NSString alloc] initWithData:imageData encoding:NSUTF8StringEncoding]; - - if (!dataString) { - return NO; - } - - NSString *lowercaseString = [dataString lowercaseString]; - BOOL containsSVGTag = [lowercaseString containsString:@")getTurboModule: