From 1334a224330a799d9f540808ec81186450cc9868 Mon Sep 17 00:00:00 2001 From: "alen.vecerin" Date: Tue, 27 Oct 2026 15:19:38 +0100 Subject: [PATCH 1/5] feat: add per-tab icon sizing Adds `iconSize` / `getIconSize` for standalone usage and `tabBarIconSize` for React Navigation. Omitted, zero or invalid sizes keep the platform default. Originally opened as #562, rebased onto main and adapted to the tab bar item image cache. The SwiftPM bridge now forwards `iconSizes` and `iconSize` so the SwiftPM build compiles. --- .changeset/tall-icons-grow.md | 6 ++ docs/docs/docs/guides/standalone-usage.mdx | 8 +++ .../guides/usage-with-react-navigation.mdx | 15 +++++ .../NativeBottomTabsOriginalIcons.tsx | 1 + .../main/java/com/rcttabview/RCTTabView.kt | 53 ++++++++++++--- .../java/com/rcttabview/RCTTabViewManager.kt | 12 ++++ .../ios/Bridge/BottomTabsBridge.h | 2 + .../ios/Bridge/BottomTabsBridge.m | 2 + .../ios/RCTTabViewComponentView.mm | 9 +++ .../ios/TabViewImpl.swift | 14 +++- .../ios/TabViewProvider.swift | 66 +++++++++++++++++-- .../react-native-bottom-tabs/package.json | 3 + .../react-native-bottom-tabs/src/TabView.tsx | 12 ++++ .../src/TabViewNativeComponent.ts | 1 + .../src/__tests__/index.test.tsx | 58 +++++++++++++++- .../react-native-bottom-tabs/src/types.ts | 1 + packages/react-navigation/package.json | 2 + .../__tests__/NativeBottomTabView.test.tsx | 56 ++++++++++++++++ packages/react-navigation/src/types.ts | 6 ++ .../src/views/NativeBottomTabView.tsx | 8 ++- yarn.lock | 5 ++ 21 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 .changeset/tall-icons-grow.md create mode 100644 packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx diff --git a/.changeset/tall-icons-grow.md b/.changeset/tall-icons-grow.md new file mode 100644 index 00000000..3bb3ef65 --- /dev/null +++ b/.changeset/tall-icons-grow.md @@ -0,0 +1,6 @@ +--- +'react-native-bottom-tabs': minor +'@bottom-tabs/react-navigation': minor +--- + +Add a per-screen option for customizing tab image icon size. diff --git a/docs/docs/docs/guides/standalone-usage.mdx b/docs/docs/docs/guides/standalone-usage.mdx index 29c06bde..deec7fbd 100644 --- a/docs/docs/docs/guides/standalone-usage.mdx +++ b/docs/docs/docs/guides/standalone-usage.mdx @@ -236,6 +236,7 @@ Each route in the `routes` array can have the following properties: - `title`: Display title for the tab - `focusedIcon`: Icon to show when tab is active - `unfocusedIcon`: Icon to show when tab is inactive (optional) +- `iconSize`: Width and height of the image icon in logical pixels. Uses the native default when omitted. - `iconRenderingMode`: Rendering mode for icons. Use `'original'` to preserve multicolor icons instead of applying the native tab tint. - `badge`: Badge text to display on the tab - `activeTintColor`: Custom active tint color for this specific tab @@ -290,6 +291,13 @@ Function to get the rendering mode for an image icon. Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint. +#### `getIconSize` + +Function to get the width and height of an image icon in logical pixels. + +- Default: Uses `route.iconSize` +- When omitted: Uses the native platform default + #### `getHidden` Function to determine if a tab should be hidden. diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 8ff246ac..374e493d 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -286,6 +286,21 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` SF Symbols are only supported on Apple platforms. ::: +#### `tabBarIconSize` + +Width and height of the tab image icon in logical pixels. This can be set per screen, and uses the native platform default when omitted. + +```tsx + ({ uri: avatarFileUri }), + tabBarIconSize: 34, + }} +/> +``` + #### `tabBarIconRenderingMode` Rendering mode for image icons. diff --git a/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx b/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx index fddf733b..5acc660f 100644 --- a/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx +++ b/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx @@ -21,6 +21,7 @@ export default function NativeBottomTabsOriginalIcons() { component={Contacts} options={{ tabBarIcon: () => require('../../assets/avatar-1.png'), + tabBarIconSize: 34, tabBarIconRenderingMode: 'original', }} /> diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 66de5d2f..3c322dd2 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -37,10 +37,14 @@ import com.facebook.react.common.assets.ReactFontManager import com.facebook.react.modules.core.ReactChoreographer import com.facebook.react.views.text.ReactTypefaceUtils import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationBarItemView import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED import com.google.android.material.transition.platform.MaterialFadeThrough +import kotlin.math.roundToInt + +private data class DrawableCacheKey(val source: ImageSource, val size: Int) class ExtendedBottomNavigationView(context: Context) : BottomNavigationView(context) { override fun getMaxItemCount(): Int { @@ -59,7 +63,8 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { var disablePageAnimations = false var items: MutableList = mutableListOf() private val iconSources: MutableMap = mutableMapOf() - private val drawableCache: MutableMap = mutableMapOf() + private var iconSizes: List = emptyList() + private val drawableCache: MutableMap = mutableMapOf() private var isLayoutEnqueued = false private var selectedItem: String? = null @@ -241,6 +246,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } fun updateItems(items: MutableList) { + val items = items.mapIndexed { index, item -> + item.copy(iconSize = iconSizes.getOrNull(index)?.takeIf { it > 0 }) + }.toMutableList() // If an item got removed, let's re-add all items if (items.size < this.items.size) { bottomNavigation.menu.clear() @@ -255,9 +263,10 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { menuItem.isVisible = !item.hidden updateIconTintMode(menuItem, item) if (iconSources.containsKey(index)) { - getDrawable(iconSources[index]!!) { + getDrawable(iconSources[index]!!, getIconSize(item)) { menuItem.icon = it updateIconTintMode(menuItem, item) + updateIconSize(menuItem, item) } } @@ -288,6 +297,8 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { view.findViewById(com.google.android.material.R.id.navigation_bar_item_content_container) ?.setTabTestID(item.testID) + + updateIconSize(menuItem, item) } } } @@ -298,6 +309,11 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } } + fun setIconSizes(iconSizes: List) { + this.iconSizes = iconSizes + updateItems(items) + } + private fun getOrCreateItem(index: Int, title: String): MenuItem { return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title) } @@ -344,10 +360,12 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { // Update existing item if exists. bottomNavigation.menu.findItem(idx)?.let { menuItem -> - getDrawable(imageSource) { + val item = items.getOrNull(idx) + getDrawable(imageSource, getIconSize(item)) { menuItem.icon = it - items.getOrNull(idx)?.let { item -> + item?.let { updateIconTintMode(menuItem, item) + updateIconSize(menuItem, item) } } } @@ -374,12 +392,16 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } @SuppressLint("CheckResult") - private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) { - drawableCache[imageSource]?.let { + private fun getDrawable( + imageSource: ImageSource, + iconSizePx: Int, + onDrawableReady: (Drawable?) -> Unit + ) { + val cacheKey = DrawableCacheKey(imageSource, iconSizePx) + drawableCache[cacheKey]?.let { onDrawableReady(it) return } - val iconSizePx = bottomNavigation.itemIconSize val request = ImageRequest.Builder(context) .data(imageSource.getUri(context)) .size(CoilSize(iconSizePx, iconSizePx)) @@ -388,7 +410,7 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { .target { drawable -> post { val stateDrawable = drawable.asDrawable(context.resources) - drawableCache[imageSource] = stateDrawable + drawableCache[cacheKey] = stateDrawable onDrawableReady(stateDrawable) } } @@ -402,6 +424,21 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { imageLoader.enqueue(request) } + private fun getIconSize(item: TabInfo?): Int { + val iconSize = item?.iconSize ?: return bottomNavigation.itemIconSize + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + iconSize.toFloat(), + resources.displayMetrics + ).roundToInt() + } + + private fun updateIconSize(menuItem: MenuItem, item: TabInfo) { + val itemView = bottomNavigation.findViewById(menuItem.itemId) + ?: return + itemView.setIconSize(getIconSize(item)) + } + fun setBarTintColor(color: Int?) { // Set the color, either using the active background color or a default color. val backgroundColor = diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt index 3d0c872b..c0ebc44c 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt @@ -24,6 +24,7 @@ data class TabInfo( val badgeBackgroundColor: Int?, val badgeTextColor: Int?, val activeTintColor: Int?, + val iconSize: Double?, val iconRenderingMode: String?, val hidden: Boolean, val testID: String? @@ -105,6 +106,7 @@ class RCTTabViewManager(context: ReactApplicationContext) : badgeBackgroundColor = if (item.hasKey("badgeBackgroundColor")) item.getInt("badgeBackgroundColor") else null, badgeTextColor = if (item.hasKey("badgeTextColor")) item.getInt("badgeTextColor") else null, activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null, + iconSize = null, iconRenderingMode = if (item.hasKey("iconRenderingMode")) item.getString("iconRenderingMode") else null, hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false, testID = item.getString("testID") @@ -126,6 +128,16 @@ class RCTTabViewManager(context: ReactApplicationContext) : view.setIcons(value) } + override fun setIconSizes(view: ReactBottomNavigationView?, value: ReadableArray?) { + view?.setIconSizes( + if (value == null) { + emptyList() + } else { + (0 until value.size()).map { value.getDouble(it) } + } + ) + } + override fun setFocusedIcons(view: ReactBottomNavigationView?, value: ReadableArray?) { } diff --git a/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h b/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h index 798e2cf0..011b57b9 100644 --- a/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h +++ b/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h @@ -38,6 +38,7 @@ @property(nonatomic, strong) NSNumber *fontSize; @property(nonatomic) BOOL tabBarHidden; @property(nonatomic, copy) NSArray *itemsData; +@property(nonatomic, copy) NSArray *iconSizes; - (void)setImageLoader:(RCTImageLoader *)imageLoader; - (void)insertChild:(UIView *)child atIndex:(NSInteger)index; - (void)removeChildAtIndex:(NSInteger)index; @@ -53,6 +54,7 @@ FOUNDATION_EXPORT NSObject *RNCCreateBottomAccessoryProvider(id *iconSizes = [NSMutableArray arrayWithCapacity:newViewProps.iconSizes.size()]; + for (const auto iconSize : newViewProps.iconSizes) { + [iconSizes addObject:@(iconSize)]; + } + _tabViewProvider.iconSizes = iconSizes; + } + if (oldViewProps.translucent != newViewProps.translucent) { _tabViewProvider.translucent = newViewProps.translucent; } @@ -237,6 +245,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol) focusedSfSymbol:RCTNSStringFromStringNilIfEmpty(item.focusedSfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor) + iconSize:nil iconRenderingMode:RCTNSStringFromStringNilIfEmpty(item.iconRenderingMode) hidden:item.hidden testID:RCTNSStringFromStringNilIfEmpty(item.testID) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index ca8ff60a..ce05ab13 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -312,6 +312,7 @@ struct TabViewImpl: View { icon: assetIcon.map(IconSource.image) ?? .sfSymbol(tabData.sfSymbol), focusedIcon: focusedAssetIcon.map(IconSource.image) ?? tabData.focusedSfSymbol.map(IconSource.sfSymbol), title: tabData.title, + iconSize: iconSize(for: tabData), inactiveTintColor: props.inactiveTintColor, activeTintColor: tabData.activeTintColor ?? props.activeTintColor, preservesOriginalIconColors: tabData.iconRenderingMode == "original", @@ -351,6 +352,7 @@ struct TabViewImpl: View { icon: icon, title: inputs.title, color: inputs.inactiveTintColor, + iconSize: inputs.iconSize, preservesOriginalIconColors: inputs.preservesOriginalIconColors, props: props ), @@ -358,6 +360,7 @@ struct TabViewImpl: View { icon: selectedIcon, title: inputs.title, color: inputs.activeTintColor, + iconSize: inputs.iconSize, preservesOriginalIconColors: inputs.preservesOriginalIconColors, props: props ), @@ -405,6 +408,7 @@ struct TabViewImpl: View { let icon: IconSource let focusedIcon: IconSource? let title: String + let iconSize: CGSize let inactiveTintColor: UIColor? let activeTintColor: UIColor? let preservesOriginalIconColors: Bool @@ -466,11 +470,11 @@ struct TabViewImpl: View { icon: UIImage, title: String, color: UIColor?, + iconSize: CGSize, preservesOriginalIconColors: Bool = false, props: TabViewProps ) -> UIImage { let color = color ?? .label - let iconSize = CGSize(width: 27, height: 27) let font = TabBarFontSize.createFontAttributes( size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize, @@ -525,6 +529,14 @@ struct TabViewImpl: View { return image.withRenderingMode(.alwaysOriginal) } + private func iconSize(for tabData: TabInfo) -> CGSize { + guard let value = tabData.iconSize?.doubleValue, value > 0 else { + return CGSize(width: 27, height: 27) + } + + return CGSize(width: value, height: value) + } + private func aspectFitRect(size: CGSize, in rect: CGRect) -> CGRect { guard size.width > 0, size.height > 0 else { return rect diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index e5663453..f7cea8d0 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -10,6 +10,7 @@ public final class TabInfo: NSObject { public let sfSymbol: String public let focusedSfSymbol: String? public let activeTintColor: PlatformColor? + public var iconSize: NSNumber? public let iconRenderingMode: String? public let hidden: Bool public let testID: String? @@ -23,6 +24,7 @@ public final class TabInfo: NSObject { sfSymbol: String, focusedSfSymbol: String?, activeTintColor: PlatformColor?, + iconSize: NSNumber?, iconRenderingMode: String?, hidden: Bool, testID: String?, @@ -35,6 +37,7 @@ public final class TabInfo: NSObject { self.sfSymbol = sfSymbol self.focusedSfSymbol = focusedSfSymbol self.activeTintColor = activeTintColor + self.iconSize = iconSize self.iconRenderingMode = iconRenderingMode self.hidden = hidden self.testID = testID @@ -57,7 +60,7 @@ public final class TabInfo: NSObject { private var props = TabViewProps() private var hostingController: PlatformHostingController? private var coalescingKey: UInt16 = 0 - private var iconSize = CGSize(width: 27, height: 27) + private let defaultIconSize = CGSize(width: 27, height: 27) @objc var onPageSelected: RCTDirectEventBlock? @@ -180,7 +183,13 @@ public final class TabInfo: NSObject { @objc public var itemsData: [TabInfo] = [] { didSet { - props.items = itemsData + updateItems() + } + } + + @objc public var iconSizes: [NSNumber] = [] { + didSet { + updateItems() } } @@ -280,13 +289,20 @@ public final class TabInfo: NSObject { guard let image else { return } DispatchQueue.main.async { [weak self] in guard let self else { return } - let icon = image.resizeImageTo(size: iconSize) + guard let resizedIcon = image.resizeImageTo(size: iconSize(at: index)) else { + return + } + #if os(iOS) + let icon = alignedIcon(resizedIcon, at: index) + #else + let icon = resizedIcon + #endif #if os(iOS) if props.experimentalBakedTintColors { if focused { - props.focusedIcons[index] = icon?.withRenderingMode(.alwaysTemplate) + props.focusedIcons[index] = icon.withRenderingMode(.alwaysTemplate) } else { - props.icons[index] = icon?.withRenderingMode(.alwaysTemplate) + props.icons[index] = icon.withRenderingMode(.alwaysTemplate) } } else { if focused { @@ -309,4 +325,44 @@ public final class TabInfo: NSObject { } } } + + private func iconSize(at index: Int) -> CGSize { + guard let value = props.items[safe: index]?.iconSize?.doubleValue, value > 0 else { + return defaultIconSize + } + + return CGSize(width: value, height: value) + } + + private func updateItems() { + let previousIconSizes = props.items.map(\.iconSize) + for (index, item) in itemsData.enumerated() { + if let value = iconSizes[safe: index], value.doubleValue > 0 { + item.iconSize = value + } else { + item.iconSize = nil + } + } + props.items = itemsData + if previousIconSizes != itemsData.map(\.iconSize) { + loadIcons(icons, focused: false) + loadIcons(focusedIcons, focused: true) + } + } + + #if os(iOS) + private func alignedIcon(_ image: UIImage, at index: Int) -> UIImage { + let size = iconSize(at: index) + let horizontalInset = (size.width - defaultIconSize.width) / 2 + let verticalInset = (size.height - defaultIconSize.height) / 2 + return image.withAlignmentRectInsets( + UIEdgeInsets( + top: verticalInset, + left: horizontalInset, + bottom: verticalInset, + right: horizontalInset + ) + ) + } + #endif } diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json index 8a25bc6d..c1da2a89 100644 --- a/packages/react-native-bottom-tabs/package.json +++ b/packages/react-native-bottom-tabs/package.json @@ -74,9 +74,12 @@ "@react-native/eslint-config": "0.81.1", "@types/jest": "^29.5.5", "@types/react": "^18.2.44", + "@types/react-test-renderer": "^19.1.0", + "jest": "^29.7.0", "react": "^19.1.0", "react-native": "0.81.1", "react-native-builder-bob": "^0.40.13", + "react-test-renderer": "19.1.0", "typescript": "^5.9.2" }, "peerDependencies": { diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index 0acfb842..dafec055 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -146,6 +146,11 @@ interface Props { focused: boolean; }) => ImageSource | AppleIcon | undefined | null; + /** + * Get the size of the tab image icon, uses `route.iconSize` by default. + */ + getIconSize?: (props: { route: Route }) => number | undefined; + /** * Get the rendering mode for the tab icon, uses `route.iconRenderingMode` by default. * @@ -262,6 +267,7 @@ const TabView = ({ getActiveTintColor = ({ route }: { route: Route }) => route.activeTintColor, getTestID = ({ route }: { route: Route }) => route.testID, getRole = ({ route }: { route: Route }) => route.role, + getIconSize = ({ route }: { route: Route }) => route.iconSize, getIconRenderingMode = ({ route }: { route: Route }) => route.iconRenderingMode, getSceneStyle = ({ route }: { route: Route }) => route.style, @@ -383,6 +389,11 @@ const TabView = ({ ] ); + const iconSizes = React.useMemo( + () => trimmedRoutes.map((route) => getIconSize({ route }) ?? 0), + [getIconSize, trimmedRoutes] + ); + const resolvedIconAssets: ImageSource[] = React.useMemo( () => // Pass empty object for icons that are not provided to avoid index mismatch on native side. @@ -458,6 +469,7 @@ const TabView = ({ {...tabLabelStyle} style={styles.fullWidth} items={items} + iconSizes={iconSizes} // When rendering a custom tab bar, icons can be React elements, which will not be properly resolved. icons={renderCustomTabBar ? undefined : resolvedIconAssets} focusedIcons={ diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts index 50ecf7c4..bdd1ccb9 100644 --- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts +++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts @@ -40,6 +40,7 @@ export type TabViewItems = ReadonlyArray<{ export interface TabViewProps extends ViewProps { items: TabViewItems; + iconSizes?: ReadonlyArray; selectedPage: string; onPageSelected?: DirectEventHandler; onTabLongPress?: DirectEventHandler; diff --git a/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx b/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx index bf84291a..d842ce8e 100644 --- a/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx +++ b/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx @@ -1 +1,57 @@ -it.todo('write a test'); +import React from 'react'; +import TestRenderer, { act } from 'react-test-renderer'; +import TabView from '../TabView'; + +jest.mock('../TabViewNativeComponent', () => { + const ReactModule = require('react'); + return ReactModule.forwardRef((props: object, ref: React.Ref) => + ReactModule.createElement('NativeTabView', { ...props, ref }) + ); +}); + +const renderTabView = ( + routes: Array<{ key: string; title: string; iconSize?: number }>, + getIconSize?: (props: { + route: (typeof routes)[number]; + }) => number | undefined +) => { + let renderer: TestRenderer.ReactTestRenderer; + + act(() => { + renderer = TestRenderer.create( + null} + onIndexChange={jest.fn()} + getIconSize={getIconSize} + /> + ); + }); + + return renderer!.root.find( + (node) => (node.type as unknown) === 'NativeTabView' + ).props; +}; + +describe('icon size', () => { + it('leaves the size unset to preserve native defaults', () => { + const { iconSizes } = renderTabView([{ key: 'home', title: 'Home' }]); + + expect(iconSizes).toEqual([0]); + }); + + it('passes a per-route override to native', () => { + const { iconSizes } = renderTabView([ + { key: 'profile', title: 'Profile', iconSize: 34 }, + ]); + + expect(iconSizes).toEqual([34]); + }); + + it('supports custom route types through getIconSize', () => { + const routes = [{ key: 'profile', title: 'Profile' }]; + const { iconSizes } = renderTabView(routes, () => 36); + + expect(iconSizes).toEqual([36]); + }); +}); diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts index 84223786..4cf27cc4 100644 --- a/packages/react-native-bottom-tabs/src/types.ts +++ b/packages/react-native-bottom-tabs/src/types.ts @@ -20,6 +20,7 @@ export type BaseRoute = { lazy?: boolean; focusedIcon?: ImageSourcePropType | AppleIcon; unfocusedIcon?: ImageSourcePropType | AppleIcon; + iconSize?: number; iconRenderingMode?: IconRenderingMode; activeTintColor?: string; hidden?: boolean; diff --git a/packages/react-navigation/package.json b/packages/react-navigation/package.json index b0c082c1..1d40862c 100644 --- a/packages/react-navigation/package.json +++ b/packages/react-navigation/package.json @@ -58,11 +58,13 @@ "devDependencies": { "@react-navigation/native": "^7.3.0", "@types/color": "^4.2.0", + "@types/react-test-renderer": "^19.1.0", "jest": "^29.7.0", "react": "^19.1.0", "react-native": "0.81.1", "react-native-bottom-tabs": "*", "react-native-builder-bob": "^0.40.13", + "react-test-renderer": "19.1.0", "typescript": "^5.9.2" }, "dependencies": { diff --git a/packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx b/packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx new file mode 100644 index 00000000..f44506df --- /dev/null +++ b/packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx @@ -0,0 +1,56 @@ +import TestRenderer, { act } from 'react-test-renderer'; +import NativeBottomTabView from '../views/NativeBottomTabView'; + +jest.mock('@react-navigation/native', () => ({ + CommonActions: { navigate: jest.fn() }, +})); + +jest.mock('react-native-bottom-tabs', () => { + const React = require('react'); + return (props: object) => React.createElement('TabView', props); +}); + +describe('tabBarIconSize', () => { + it('maps per-screen overrides and preserves an unset default', () => { + const state = { + index: 0, + key: 'tabs', + routeNames: ['Home', 'Profile'], + history: [], + preloadedRouteKeys: [], + stale: false as const, + type: 'tab' as const, + routes: [ + { key: 'home', name: 'Home' }, + { key: 'profile', name: 'Profile' }, + ], + }; + const descriptors = { + home: { + options: {}, + render: jest.fn(() => null), + }, + profile: { + options: { tabBarIconSize: 34 }, + render: jest.fn(() => null), + }, + }; + let renderer: TestRenderer.ReactTestRenderer; + + act(() => { + renderer = TestRenderer.create( + + ); + }); + + const { navigationState } = renderer!.root.find( + (node) => (node.type as unknown) === 'TabView' + ).props; + expect(navigationState.routes[0].iconSize).toBeUndefined(); + expect(navigationState.routes[1].iconSize).toBe(34); + }); +}); diff --git a/packages/react-navigation/src/types.ts b/packages/react-navigation/src/types.ts index eb1967cd..1f8b208e 100644 --- a/packages/react-navigation/src/types.ts +++ b/packages/react-navigation/src/types.ts @@ -71,6 +71,11 @@ export type NativeBottomTabNavigationOptions = { */ tabBarIcon?: (props: { focused: boolean }) => ImageSourcePropType | AppleIcon; + /** + * Width and height of the tab image icon in logical pixels. + */ + tabBarIconSize?: number; + /** * Rendering mode for the tab icon. Use `original` to preserve multicolor image icons. */ @@ -159,6 +164,7 @@ export type NativeBottomTabNavigationConfig = Partial< | 'renderScene' | 'getLazy' | 'getIcon' + | 'getIconSize' | 'getIconRenderingMode' | 'getLabelText' | 'getBadge' diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx index 629bec4b..4ac7468c 100644 --- a/packages/react-navigation/src/views/NativeBottomTabView.tsx +++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx @@ -28,7 +28,13 @@ export default function NativeBottomTabView({ return ( ({ + ...route, + iconSize: descriptors[route.key]?.options.tabBarIconSize, + })), + }} renderScene={({ route }) => descriptors[route.key]?.render()} getActiveTintColor={({ route }) => { return descriptors[route.key]?.options.tabBarActiveTintColor; diff --git a/yarn.lock b/yarn.lock index d457de37..bac70817 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3938,12 +3938,14 @@ __metadata: dependencies: "@react-navigation/native": "npm:^7.3.0" "@types/color": "npm:^4.2.0" + "@types/react-test-renderer": "npm:^19.1.0" color: "npm:^5.0.0" jest: "npm:^29.7.0" react: "npm:^19.1.0" react-native: "npm:0.81.1" react-native-bottom-tabs: "npm:*" react-native-builder-bob: "npm:^0.40.13" + react-test-renderer: "npm:19.1.0" typescript: "npm:^5.9.2" peerDependencies: "@react-navigation/native": ">=7" @@ -19365,10 +19367,13 @@ __metadata: "@react-native/eslint-config": "npm:0.81.1" "@types/jest": "npm:^29.5.5" "@types/react": "npm:^18.2.44" + "@types/react-test-renderer": "npm:^19.1.0" + jest: "npm:^29.7.0" react: "npm:^19.1.0" react-freeze: "npm:^1.0.0" react-native: "npm:0.81.1" react-native-builder-bob: "npm:^0.40.13" + react-test-renderer: "npm:19.1.0" sf-symbols-typescript: "npm:^2.0.0" typescript: "npm:^5.9.2" use-latest-callback: "npm:^0.2.1" From b2ea12e9c05ce840e2468bb0b96fbea4d87f7e2e Mon Sep 17 00:00:00 2001 From: "alen.vecerin" Date: Tue, 27 Oct 2026 15:19:38 +0100 Subject: [PATCH 2/5] fix(android): keep custom icon size when icons finish loading Icon load callbacks applied the tab item captured when the request started. When `iconSizes` arrived while an icon was still loading, the late callback reset the icon to the default size. Read the latest item when the icon is ready and drop results loaded at an outdated size. --- .../main/java/com/rcttabview/RCTTabView.kt | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 3c322dd2..c257ea58 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -263,10 +263,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { menuItem.isVisible = !item.hidden updateIconTintMode(menuItem, item) if (iconSources.containsKey(index)) { - getDrawable(iconSources[index]!!, getIconSize(item)) { - menuItem.icon = it - updateIconTintMode(menuItem, item) - updateIconSize(menuItem, item) + val iconSizePx = getIconSize(item) + getDrawable(iconSources[index]!!, iconSizePx) { + applyIcon(menuItem, index, iconSizePx, it) } } @@ -360,13 +359,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { // Update existing item if exists. bottomNavigation.menu.findItem(idx)?.let { menuItem -> - val item = items.getOrNull(idx) - getDrawable(imageSource, getIconSize(item)) { - menuItem.icon = it - item?.let { - updateIconTintMode(menuItem, item) - updateIconSize(menuItem, item) - } + val iconSizePx = getIconSize(items.getOrNull(idx)) + getDrawable(imageSource, iconSizePx) { + applyIcon(menuItem, idx, iconSizePx, it) } } } @@ -433,6 +428,19 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { ).roundToInt() } + private fun applyIcon(menuItem: MenuItem, index: Int, iconSizePx: Int, drawable: Drawable?) { + // Icons load asynchronously, so read the latest item instead of the one captured at request time. + val item = items.getOrNull(index) + // The icon size changed while loading, a request for the new size is already in flight. + if (getIconSize(item) != iconSizePx) return + + menuItem.icon = drawable + item?.let { + updateIconTintMode(menuItem, it) + updateIconSize(menuItem, it) + } + } + private fun updateIconSize(menuItem: MenuItem, item: TabInfo) { val itemView = bottomNavigation.findViewById(menuItem.itemId) ?: return From 132e513ceb04e07a1698f277b0de14077cafc2fb Mon Sep 17 00:00:00 2001 From: "alen.vecerin" Date: Tue, 27 Oct 2026 15:19:38 +0100 Subject: [PATCH 3/5] chore(example): move icon sizes to a dedicated example Adds an "Icon sizes" example based on Four Tabs that shows the default size next to 20, 32 and 40, and removes `tabBarIconSize` from the original icons example. --- packages/example-shared/src/App.tsx | 5 ++ .../example-shared/src/Examples/IconSizes.tsx | 52 +++++++++++++++++++ .../NativeBottomTabsOriginalIcons.tsx | 1 - 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/example-shared/src/Examples/IconSizes.tsx diff --git a/packages/example-shared/src/App.tsx b/packages/example-shared/src/App.tsx index 420d3427..1dc55226 100644 --- a/packages/example-shared/src/App.tsx +++ b/packages/example-shared/src/App.tsx @@ -45,6 +45,7 @@ import TabBarHidden from './Examples/TabBarHidden'; import CustomTabBar from './Examples/CustomTabBar'; import NativeBottomTabsTabBarHidden from './Examples/NativeBottomTabsTabBarHidden'; import OriginalIconColors from './Examples/OriginalIconColors'; +import IconSizes from './Examples/IconSizes'; import NativeBottomTabsOriginalIcons from './Examples/NativeBottomTabsOriginalIcons'; import { useLogger } from '@react-navigation/devtools'; import LazyTabs from './Examples/LazyTabs'; @@ -126,6 +127,10 @@ const examples = [ component: OriginalIconColors, name: 'Original icon colors', }, + { + component: IconSizes, + name: 'Icon sizes', + }, { component: FourTabsRippleColor, name: 'Four Tabs with ripple Color', diff --git a/packages/example-shared/src/Examples/IconSizes.tsx b/packages/example-shared/src/Examples/IconSizes.tsx new file mode 100644 index 00000000..ef78821a --- /dev/null +++ b/packages/example-shared/src/Examples/IconSizes.tsx @@ -0,0 +1,52 @@ +import TabView, { SceneMap } from 'react-native-bottom-tabs'; +import { useState } from 'react'; +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; + +const renderScene = SceneMap({ + article: Article, + albums: Albums, + contacts: Contacts, + chat: Chat, +}); + +export default function IconSizes() { + const [index, setIndex] = useState(0); + const [routes] = useState([ + { + key: 'article', + title: 'Default', + focusedIcon: require('../../assets/icons/article_dark.png'), + }, + { + key: 'albums', + title: 'Size 20', + focusedIcon: require('../../assets/icons/grid_dark.png'), + iconSize: 20, + }, + { + key: 'contacts', + title: 'Size 32', + focusedIcon: require('../../assets/icons/person_dark.png'), + iconSize: 32, + }, + { + key: 'chat', + title: 'Size 40', + focusedIcon: require('../../assets/icons/chat_dark.png'), + iconSize: 40, + }, + ]); + + return ( + + ); +} diff --git a/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx b/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx index 5acc660f..fddf733b 100644 --- a/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx +++ b/packages/example-shared/src/Examples/NativeBottomTabsOriginalIcons.tsx @@ -21,7 +21,6 @@ export default function NativeBottomTabsOriginalIcons() { component={Contacts} options={{ tabBarIcon: () => require('../../assets/avatar-1.png'), - tabBarIconSize: 34, tabBarIconRenderingMode: 'original', }} /> From 398b6bc9673fc8a72bfef377bce7be754489a60f Mon Sep 17 00:00:00 2001 From: "alen.vecerin" Date: Tue, 27 Oct 2026 15:27:34 +0100 Subject: [PATCH 4/5] chore: remove icon size tests and test dependencies Restores the placeholder test in `react-native-bottom-tabs`, removes the React Navigation test and rolls back the added jest and react-test-renderer dev dependencies. --- .../react-native-bottom-tabs/package.json | 3 - .../src/__tests__/index.test.tsx | 58 +------------------ packages/react-navigation/package.json | 2 - .../__tests__/NativeBottomTabView.test.tsx | 56 ------------------ yarn.lock | 5 -- 5 files changed, 1 insertion(+), 123 deletions(-) delete mode 100644 packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json index c1da2a89..8a25bc6d 100644 --- a/packages/react-native-bottom-tabs/package.json +++ b/packages/react-native-bottom-tabs/package.json @@ -74,12 +74,9 @@ "@react-native/eslint-config": "0.81.1", "@types/jest": "^29.5.5", "@types/react": "^18.2.44", - "@types/react-test-renderer": "^19.1.0", - "jest": "^29.7.0", "react": "^19.1.0", "react-native": "0.81.1", "react-native-builder-bob": "^0.40.13", - "react-test-renderer": "19.1.0", "typescript": "^5.9.2" }, "peerDependencies": { diff --git a/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx b/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx index d842ce8e..bf84291a 100644 --- a/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx +++ b/packages/react-native-bottom-tabs/src/__tests__/index.test.tsx @@ -1,57 +1 @@ -import React from 'react'; -import TestRenderer, { act } from 'react-test-renderer'; -import TabView from '../TabView'; - -jest.mock('../TabViewNativeComponent', () => { - const ReactModule = require('react'); - return ReactModule.forwardRef((props: object, ref: React.Ref) => - ReactModule.createElement('NativeTabView', { ...props, ref }) - ); -}); - -const renderTabView = ( - routes: Array<{ key: string; title: string; iconSize?: number }>, - getIconSize?: (props: { - route: (typeof routes)[number]; - }) => number | undefined -) => { - let renderer: TestRenderer.ReactTestRenderer; - - act(() => { - renderer = TestRenderer.create( - null} - onIndexChange={jest.fn()} - getIconSize={getIconSize} - /> - ); - }); - - return renderer!.root.find( - (node) => (node.type as unknown) === 'NativeTabView' - ).props; -}; - -describe('icon size', () => { - it('leaves the size unset to preserve native defaults', () => { - const { iconSizes } = renderTabView([{ key: 'home', title: 'Home' }]); - - expect(iconSizes).toEqual([0]); - }); - - it('passes a per-route override to native', () => { - const { iconSizes } = renderTabView([ - { key: 'profile', title: 'Profile', iconSize: 34 }, - ]); - - expect(iconSizes).toEqual([34]); - }); - - it('supports custom route types through getIconSize', () => { - const routes = [{ key: 'profile', title: 'Profile' }]; - const { iconSizes } = renderTabView(routes, () => 36); - - expect(iconSizes).toEqual([36]); - }); -}); +it.todo('write a test'); diff --git a/packages/react-navigation/package.json b/packages/react-navigation/package.json index 1d40862c..b0c082c1 100644 --- a/packages/react-navigation/package.json +++ b/packages/react-navigation/package.json @@ -58,13 +58,11 @@ "devDependencies": { "@react-navigation/native": "^7.3.0", "@types/color": "^4.2.0", - "@types/react-test-renderer": "^19.1.0", "jest": "^29.7.0", "react": "^19.1.0", "react-native": "0.81.1", "react-native-bottom-tabs": "*", "react-native-builder-bob": "^0.40.13", - "react-test-renderer": "19.1.0", "typescript": "^5.9.2" }, "dependencies": { diff --git a/packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx b/packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx deleted file mode 100644 index f44506df..00000000 --- a/packages/react-navigation/src/__tests__/NativeBottomTabView.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import TestRenderer, { act } from 'react-test-renderer'; -import NativeBottomTabView from '../views/NativeBottomTabView'; - -jest.mock('@react-navigation/native', () => ({ - CommonActions: { navigate: jest.fn() }, -})); - -jest.mock('react-native-bottom-tabs', () => { - const React = require('react'); - return (props: object) => React.createElement('TabView', props); -}); - -describe('tabBarIconSize', () => { - it('maps per-screen overrides and preserves an unset default', () => { - const state = { - index: 0, - key: 'tabs', - routeNames: ['Home', 'Profile'], - history: [], - preloadedRouteKeys: [], - stale: false as const, - type: 'tab' as const, - routes: [ - { key: 'home', name: 'Home' }, - { key: 'profile', name: 'Profile' }, - ], - }; - const descriptors = { - home: { - options: {}, - render: jest.fn(() => null), - }, - profile: { - options: { tabBarIconSize: 34 }, - render: jest.fn(() => null), - }, - }; - let renderer: TestRenderer.ReactTestRenderer; - - act(() => { - renderer = TestRenderer.create( - - ); - }); - - const { navigationState } = renderer!.root.find( - (node) => (node.type as unknown) === 'TabView' - ).props; - expect(navigationState.routes[0].iconSize).toBeUndefined(); - expect(navigationState.routes[1].iconSize).toBe(34); - }); -}); diff --git a/yarn.lock b/yarn.lock index bac70817..d457de37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3938,14 +3938,12 @@ __metadata: dependencies: "@react-navigation/native": "npm:^7.3.0" "@types/color": "npm:^4.2.0" - "@types/react-test-renderer": "npm:^19.1.0" color: "npm:^5.0.0" jest: "npm:^29.7.0" react: "npm:^19.1.0" react-native: "npm:0.81.1" react-native-bottom-tabs: "npm:*" react-native-builder-bob: "npm:^0.40.13" - react-test-renderer: "npm:19.1.0" typescript: "npm:^5.9.2" peerDependencies: "@react-navigation/native": ">=7" @@ -19367,13 +19365,10 @@ __metadata: "@react-native/eslint-config": "npm:0.81.1" "@types/jest": "npm:^29.5.5" "@types/react": "npm:^18.2.44" - "@types/react-test-renderer": "npm:^19.1.0" - jest: "npm:^29.7.0" react: "npm:^19.1.0" react-freeze: "npm:^1.0.0" react-native: "npm:0.81.1" react-native-builder-bob: "npm:^0.40.13" - react-test-renderer: "npm:19.1.0" sf-symbols-typescript: "npm:^2.0.0" typescript: "npm:^5.9.2" use-latest-callback: "npm:^0.2.1" From 118d5083080cfd8d1b4f6de03c45d52a5a000163 Mon Sep 17 00:00:00 2001 From: "alen.vecerin" Date: Fri, 30 Oct 2026 10:38:06 +0100 Subject: [PATCH 5/5] refactor: address review feedback for per-tab icon sizing - Pass tabBarIconSize through getIconSize instead of rebuilding the navigation state on every render - Share a single default icon size constant between TabViewProvider and TabViewImpl - Rename the shadowing updateItems parameter on Android - Apply iconSizes before items in the iOS Fabric props update --- .../src/main/java/com/rcttabview/RCTTabView.kt | 4 ++-- .../ios/RCTTabViewComponentView.mm | 8 ++++---- .../react-native-bottom-tabs/ios/TabViewImpl.swift | 2 +- .../ios/TabViewProvider.swift | 3 ++- .../src/views/NativeBottomTabView.tsx | 11 ++++------- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index c257ea58..972b1b82 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -245,8 +245,8 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) { } } - fun updateItems(items: MutableList) { - val items = items.mapIndexed { index, item -> + fun updateItems(newItems: MutableList) { + val items = newItems.mapIndexed { index, item -> item.copy(iconSize = iconSizes.getOrNull(index)?.takeIf { it > 0 }) }.toMutableList() // If an item got removed, let's re-add all items diff --git a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm index 1e60f5fe..af1b46d0 100644 --- a/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm @@ -123,10 +123,6 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & const auto &oldViewProps = *std::static_pointer_cast(_props); const auto &newViewProps = *std::static_pointer_cast(props); - if (oldViewProps.items != newViewProps.items) { - _tabViewProvider.itemsData = convertItemsToArray(newViewProps.items); - } - if (oldViewProps.iconSizes != newViewProps.iconSizes) { NSMutableArray *iconSizes = [NSMutableArray arrayWithCapacity:newViewProps.iconSizes.size()]; for (const auto iconSize : newViewProps.iconSizes) { @@ -135,6 +131,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _tabViewProvider.iconSizes = iconSizes; } + if (oldViewProps.items != newViewProps.items) { + _tabViewProvider.itemsData = convertItemsToArray(newViewProps.items); + } + if (oldViewProps.translucent != newViewProps.translucent) { _tabViewProvider.translucent = newViewProps.translucent; } diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index ce05ab13..2e2f4672 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -531,7 +531,7 @@ struct TabViewImpl: View { private func iconSize(for tabData: TabInfo) -> CGSize { guard let value = tabData.iconSize?.doubleValue, value > 0 else { - return CGSize(width: 27, height: 27) + return defaultIconSize } return CGSize(width: value, height: value) diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index f7cea8d0..68292fba 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -54,13 +54,14 @@ public final class TabInfo: NSObject { func onLayout(size: CGSize, reactTag: NSNumber?) } +let defaultIconSize = CGSize(width: 27, height: 27) + @objc public class TabViewProvider: PlatformView { private var imageLoader: RCTImageLoaderProtocol? private weak var delegate: TabViewProviderDelegate? private var props = TabViewProps() private var hostingController: PlatformHostingController? private var coalescingKey: UInt16 = 0 - private let defaultIconSize = CGSize(width: 27, height: 27) @objc var onPageSelected: RCTDirectEventBlock? diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx index 4ac7468c..87c17b52 100644 --- a/packages/react-navigation/src/views/NativeBottomTabView.tsx +++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx @@ -28,14 +28,11 @@ export default function NativeBottomTabView({ return ( ({ - ...route, - iconSize: descriptors[route.key]?.options.tabBarIconSize, - })), - }} + navigationState={state} renderScene={({ route }) => descriptors[route.key]?.render()} + getIconSize={({ route }) => + descriptors[route.key]?.options.tabBarIconSize + } getActiveTintColor={({ route }) => { return descriptors[route.key]?.options.tabBarActiveTintColor; }}