Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-tools-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-native-bottom-tabs": major
---

Disable Apple-platform SVG tab icon support by default and add the `$RNBottomTabsEnableSVG` Podfile opt-in. The optional CoreSVG decoder is now scoped to bottom-tab icons instead of being registered app-wide with React Native's image loader.
1 change: 1 addition & 0 deletions apps/example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
$RNBottomTabsEnableSVG = ENV['RN_BOTTOM_TABS_ENABLE_SVG'] == '1'

ws_dir = Pathname.new(__dir__)
ws_dir = ws_dir.parent until
Expand Down
3 changes: 1 addition & 2 deletions apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ function NativeBottomTabsEmbeddedStacks() {
name="Chat"
component={ChatStackScreen}
options={{
tabBarIcon: () =>
require('../../assets/icons/message-circle-code.svg'),
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
}}
/>
</Tab.Navigator>
Expand Down
4 changes: 1 addition & 3 deletions apps/example/src/Examples/TintColors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ export default function TintColorsExample() {
},
{
key: 'chat',
focusedIcon: {
uri: 'https://upload.wikimedia.org/wikipedia/commons/f/fa/Apple_logo_black.svg',
},
focusedIcon: require('../../assets/icons/chat_dark.png'),
title: 'Chat',
},
]);
Expand Down
32 changes: 32 additions & 0 deletions artifacts/issue-565/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Issue #565 verification

These artifacts were captured from the example app on an iPhone 17 Pro simulator running iOS 26.5. The native builds used Xcode 27.0, CocoaPods 1.17.0, React Native 0.81.4, and the new architecture. Device automation used `agent-device` 0.20.10.

## Default configuration

`pod install` ran without `RN_BOTTOM_TABS_ENABLE_SVG`. The generated Pods project contained no `ios/SVG` source references and React Native codegen generated an empty `imageDataDecoderClassNames` array. The built app, app dylib, and `libreact-native-bottom-tabs.a` contained none of these strings:

- `CoreSVGWrapper`
- `SvgDecoder`
- `CGSVGDocument`
- `_imageWithCGSVGDocument`
- the CoreSVG symbol-name base64 literals

`RCTTabViewComponentView` was present as a positive control. The app was then exercised with SF Symbol and ordinary PNG icons:

- [SF Symbols](default/sf-symbols.png)
- [Selected SF Symbol tab](default/sf-symbols-albums.png)
- [PNG icons](default/png-icons.png)
- [Non-SVG interaction recording](default/non-svg-icons.mp4)

## SVG opt-in configuration

`pod install` ran with `RN_BOTTOM_TABS_ENABLE_SVG=1`. The generated Pods project included exactly `CoreSVG.{h,mm}` and `SvgDecoder.{h,mm}` and set `RN_BOTTOM_TABS_ENABLE_SVG` for Objective-C++ and Swift compilation. React Native codegen still generated an empty `imageDataDecoderClassNames` array because the decoder is library-scoped.

The built static library exported `RNBottomTabsDecodeSVGData` and contained `CoreSVGWrapper`, `CGSVGDocument`, and the CoreSVG symbol-name base64 literals. Both local and remote SVG tab sources rendered, and tab selection was verified through accessibility state:

- [Local SVG icons](opt-in/local-svg-icons.png)
- [Remote SVG icons](opt-in/remote-svg-icons.png)
- [SVG interaction recording](opt-in/svg-icons.mp4)

The same podspec configuration checks also passed with React Native 0.71.19, 0.76.9, 0.79.7, 0.81.6, 0.84.1, and 0.87.1 under CocoaPods 1.17.0, for both default and opted-in evaluation.
Binary file added artifacts/issue-565/default/non-svg-icons.mp4
Binary file not shown.
Binary file added artifacts/issue-565/default/png-icons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/issue-565/default/sf-symbols.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/issue-565/opt-in/local-svg-icons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/issue-565/opt-in/remote-svg-icons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/issue-565/opt-in/svg-icons.mp4
Binary file not shown.
32 changes: 30 additions & 2 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,6 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon`
component={Albums}
options={{
tabBarIcon: () => require('person.png'),
// SVG is also supported
tabBarIcon: () => require('person.svg'),
// or
tabBarIcon: () => ({ sfSymbol: 'person' }),
// You can also pass a URL
Expand All @@ -286,6 +284,36 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon`
SF Symbols are only supported on Apple platforms.
:::

#### SVG icons on Apple platforms

SVG tab icons remain available by default on Android. On Apple platforms, SVG support is disabled by default because its native decoder uses private CoreSVG APIs. PNG, JPEG, and SF Symbol icons do not need this decoder.

To opt in, set the flag before React Native configures pods in your `Podfile`, then run `pod install` and rebuild the native app:

```ruby
$RNBottomTabsEnableSVG = true

# require React Native's pod helpers and call use_react_native! below
```

With the decoder enabled, local and remote SVG sources can be used like other image sources:

```tsx
tabBarIcon: () => require('person.svg')
```

Enabling this flag compiles the optional CoreSVG implementation into the Apple-platform pod. The decoder is scoped to bottom-tab icons and is not registered with React Native's app-wide image loader.

If you are upgrading from a version where Apple-platform SVG support was enabled automatically, add this flag before upgrading to preserve your existing SVG tab icons. Expo prebuild projects can use the package's `enableSVG` plugin option instead:

```json
{
"expo": {
"plugins": [["react-native-bottom-tabs", { "enableSVG": true }]]
}
}
```

#### `tabBarIconRenderingMode`

Rendering mode for image icons.
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-bottom-tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

The full documentation can be found on our [website](https://callstackincubator.github.io/react-native-bottom-tabs/).

SVG tab icons on Apple platforms are disabled by default. See the [icon configuration guide](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation#svg-icons-on-apple-platforms) for the Podfile and Expo opt-ins.

## Contributing

See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
Expand Down
26 changes: 21 additions & 5 deletions packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
#ifdef __cplusplus
#import <Foundation/Foundation.h>
#import <TargetConditionals.h>

#if TARGET_OS_OSX
#import <AppKit/AppKit.h>
#else
#import <UIKit/UIKit.h>
#endif

#import <RNCTabView/RNCTabView.h>
#import <React/RCTImageDataDecoder.h>
NS_ASSUME_NONNULL_BEGIN

@interface SvgDecoder : NSObject <RCTImageDataDecoder, NativeSVGDecoderSpec>
#ifdef __cplusplus
extern "C" {
#endif

@end
#if TARGET_OS_OSX
NSImage *_Nullable RNBottomTabsDecodeSVGData(NSData *data);
#else
UIImage *_Nullable RNBottomTabsDecodeSVGData(NSData *data);
#endif

#ifdef __cplusplus
}
#endif

NS_ASSUME_NONNULL_END
35 changes: 4 additions & 31 deletions packages/react-native-bottom-tabs/ios/SVG/SvgDecoder.mm
Original file line number Diff line number Diff line change
@@ -1,38 +1,11 @@
#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
PlatformImage *RNBottomTabsDecodeSVGData(NSData *data)
{
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);
if (![CoreSVGWrapper isSVGData:data]) {
return nil;
}
return ^{};
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeSVGDecoderSpecJSI>(params);
return [CoreSVGWrapper.shared imageFromSVGData:data];
}

@end
121 changes: 94 additions & 27 deletions packages/react-native-bottom-tabs/ios/TabViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@
}

@objc public protocol TabViewProviderDelegate {
func onPageSelected(key: String, reactTag: NSNumber?)

Check warning on line 48 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
func onLongPress(key: String, reactTag: NSNumber?)

Check warning on line 49 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
func onTabBarMeasured(height: Int, reactTag: NSNumber?)

Check warning on line 50 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
func onLayout(size: CGSize, reactTag: NSNumber?)

Check warning on line 51 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
}

@objc public class TabViewProvider: PlatformView {
Expand Down Expand Up @@ -256,7 +256,7 @@
props.children.remove(at: index)
}

private func loadIcons(_ icons: NSArray?, focused: Bool) {

Check warning on line 259 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Cyclomatic Complexity Violation: Function should have complexity 10 or less; currently complexity is 12 (cyclomatic_complexity)
guard let imageLoader else { return }

// TODO: Diff the arrays and update only changed items.
Expand All @@ -264,6 +264,21 @@
if let imageSources = icons as? [RCTImageSource?] {
for (index, imageSource) in imageSources.enumerated() {
guard let imageSource else { continue }

#if RN_BOTTOM_TABS_ENABLE_SVG
if isSVGRequest(imageSource.request) {
loadSVGIcon(with: imageSource.request) { [weak self] image in
guard let self else { return }
if let image {
updateIcon(image, at: index, focused: focused)
} else {
print("[TabView] Error loading SVG icon")
}
}
continue
}
#endif

imageLoader.loadImage(
with: imageSource.request,
size: imageSource.size,
Expand All @@ -272,41 +287,93 @@
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 { [weak self] in
guard let self else { return }
let icon = image.resizeImageTo(size: iconSize)
#if os(iOS)
if props.experimentalBakedTintColors {
if focused {
props.focusedIcons[index] = icon?.withRenderingMode(.alwaysTemplate)
} else {
props.icons[index] = icon?.withRenderingMode(.alwaysTemplate)
}
} else {
if focused {
props.focusedIcons[index] = icon
completionBlock: { [weak self] error, image in
guard let self else { return }
if let error {
#if RN_BOTTOM_TABS_ENABLE_SVG
loadSVGIcon(with: imageSource.request) { [weak self] svgImage in
guard let self else { return }
if let svgImage {
updateIcon(svgImage, at: index, focused: focused)
} else {
props.icons[index] = icon
print("[TabView] Error loading image: \(error.localizedDescription)")
}
}
props.iconsRevision += 1
#else
if focused {
props.focusedIcons[index] = icon
} else {
props.icons[index] = icon
}
props.iconsRevision += 1
print("[TabView] Error loading image: \(error.localizedDescription)")
#endif
return
}
guard let image else { return }
updateIcon(image, at: index, focused: focused)
})
}
}
}

private func updateIcon(_ image: PlatformImage, at index: Int, focused: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let icon = image.resizeImageTo(size: iconSize)
#if os(iOS)
if props.experimentalBakedTintColors {
if focused {
props.focusedIcons[index] = icon?.withRenderingMode(.alwaysTemplate)
} else {
props.icons[index] = icon?.withRenderingMode(.alwaysTemplate)
}
} else {
if focused {
props.focusedIcons[index] = icon
} else {
props.icons[index] = icon
}
}
props.iconsRevision += 1
#else
if focused {
props.focusedIcons[index] = icon
} else {
props.icons[index] = icon
}
props.iconsRevision += 1
#endif
}
}

#if RN_BOTTOM_TABS_ENABLE_SVG
private func isSVGRequest(_ request: URLRequest) -> Bool {
guard let url = request.url else { return false }
return url.pathExtension.lowercased() == "svg"
|| url.absoluteString.lowercased().hasPrefix("data:image/svg+xml")
}

private func loadSVGIcon(
with request: URLRequest,
completion: @escaping (PlatformImage?) -> Void
) {
guard let url = request.url else {
completion(nil)
return
}

let decode: (Data?) -> Void = { data in
guard let data else {
completion(nil)
return
}
completion(RNBottomTabsDecodeSVGData(data))
}

if url.scheme == "http" || url.scheme == "https" {
URLSession.shared.dataTask(with: request) { data, _, _ in
decode(data)
}.resume()
} else {
DispatchQueue.global(qos: .userInitiated).async {
decode(try? Data(contentsOf: url))
}
}
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require "minitest/autorun"
require_relative "../svg_configuration"

class SVGConfigurationTest < Minitest::Test
def test_svg_support_is_disabled_by_default
$RNBottomTabsEnableSVG = nil

refute RNBottomTabs::SVGConfiguration.enabled?
assert_equal "ios/SVG/**/*", RNBottomTabs::SVGConfiguration::SVG_SOURCES
assert_equal(
{ "DEFINES_MODULE" => "YES" },
RNBottomTabs::SVGConfiguration.xcconfig({ "DEFINES_MODULE" => "YES" })
)
end

def test_global_flag_enables_svg_support
$RNBottomTabsEnableSVG = true

assert RNBottomTabs::SVGConfiguration.enabled?
end

def test_opt_in_sets_compilation_conditions
xcconfig = RNBottomTabs::SVGConfiguration.xcconfig(
{ "DEFINES_MODULE" => "YES" },
enabled: true
)

assert_equal "YES", xcconfig["DEFINES_MODULE"]
assert_equal(
"$(inherited) RN_BOTTOM_TABS_ENABLE_SVG=1",
xcconfig["GCC_PREPROCESSOR_DEFINITIONS"]
)
assert_equal(
"$(inherited) RN_BOTTOM_TABS_ENABLE_SVG",
xcconfig["SWIFT_ACTIVE_COMPILATION_CONDITIONS"]
)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
#import <React/RCTUIManager.h>
#import <React/UIView+React.h>
#import <React/RCTUtils.h>

#if RN_BOTTOM_TABS_ENABLE_SVG
#import "SVG/SvgDecoder.h"
#endif
Loading
Loading