Skip to content

Conversation

t0maboro
Copy link
Contributor

@t0maboro t0maboro commented Sep 15, 2025

Description

This PR refactors the way icon resources are passed into components by merging platform-specific props into a single unified icon configuration structure. Previously, icons were defined using 3–4 separate props depending on the platform, which led to inconsistency and potential conflicts. Now, the icon is defined using a single object:

{
  android?: PlatformIconAndroid;
  ios?: PlatformIconIOS;
  shared?: PlatformIconShared;
}

Platform-specific types (PlatformIconAndroid, PlatformIconIOS, PlatformIconShared) are used to define supported icon formats and sources per platform.

Note:
This PR introduces a new approach to handling SVGs on Android. In #3216, I've started migrating to the Fresco image loading library, which unfortunately does not support SVGs directly.

As a result, SVG support will now be provided exclusively through the drawableResourceAndroid type.

⚠️ This is a breaking change that may affect developers currently using SVGs via remote URLs or external icon props. Please make sure to update your code accordingly to avoid runtime issues.

Changes

  • Updated types for icon prop.
  • Added logic for resolving props internally on Android and adapted the current logic for iOS.
  • Updated examples to use new APIs.

Test code and steps to reproduce

Verified that the updated TestBottomTabs example is showing icons as expected.

Screenshot 2025-09-15 at 19 30 32

Checklist

  • Included code example that can be used to test this change
  • Ensured that CI passes

Copy link
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this.

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda like the changes. Have few remarks regarding naming (will describe them in follow-up review).

We need to wait couple more days until I reach expo-router & coordinate this change (@Ubax is on PTO currently)

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd only change following suffixes & hav one remark regarding whether the logic should be run. The rest looks really nice.

@t0maboro t0maboro requested a review from kkafar September 23, 2025 12:29
Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the PR! Let's wait for getting in touch with expo though

t0maboro added a commit that referenced this pull request Sep 23, 2025
… tab icons (#3216)

## Description

In the initial approach, we used Coil image loading library. However,
after the 1st release, Coil has introduced several issues that had an
impact on the stability and maintainability.

After reevaluating other options, we've decided to try migrating to
Fresco. This choice aligns better because React Native also relies on
Fresco internally. Using Fresco should improve consistency with the RN
and reduce our maintenance overhead.

One known limitation is that Fresco doesn't support loading images in
SVG format. We’ll need to explore alternative strategies to adapt.

**Note**: 

Slightly depends on:
#3214

Fresco doesn't support SVGs, so we decided to handle them by passing
assets via the `AndroidStudio` as vector icons - this approach
simplifies a lot of things, especially that

```
PlatformIconAndroid =
  | {
      type: 'drawableResourceAndroid';
      name: string;
    }
```

will cover SVGs, so we can omit adding a support here. I'll ensure to
mention there that SVG should be passed with `drawableResourceAndroid`
only.

## Changes

- Removed Coil
- Replaced logic for Coil ImageLoader with Fresco APIs
- Extracted ImageLoader to a separate file

## Test code and steps to reproduce

Tested with BottomTabs example.

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
Comment on lines +349 to +322
* Remarks: `imageSource` type doesn't support SVGs on Android.
* For loading SVGs use `drawableResource` type.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that passing an SVG image as imageSource will not work anymore?

Currently, passing an SVG icon like this works on Android, which is very handy:

<Icon
	src={{
		default: require('./icon.svgx'),
	}}
/>

If only drawables are supported on Android, how are they supposed to be added when using a managed Expo workflow with CNG? Will there be an official config plugin to manage drawables? Either way, it seems like adding unnecessary complexity for the common use case of using SVG images as tab bar icons on Android.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! This PR does not change that, it only better documents current state of the lib.

The changes have already landed in #3216.

With latest stable 4.16 version the svgs are supported as we used coil lib for loading the resources, however we run into some issues, which led us to back out from that usage, migrating to fresco which is managed by react-native installation directly (from our PoV it comes bundled with the app), which does not have svg support.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're in touch with expo-router team, so that they're aware of this current limitation & can take appropriate steps on their end.

@kkafar
Copy link
Member

kkafar commented Oct 15, 2025

Can we update this PR after #2987 landed? To reuse existing types & unify the approach.

Comment on lines 20 to 41
export type PlatformIconShared = {
type: 'imageSource';
imageSource: ImageSourcePropType;
}
};

// iOS-specific: image as a template usage
export interface TemplateIcon {
templateSource: ImageSourcePropType;
}
export type PlatformIconIOS =
| {
type: 'sfSymbol';
name: string;
}
| {
type: 'templateSource';
templateSource: ImageSourcePropType;
}
| PlatformIconShared;

// iOS-specific: SFSymbol, image as a template usage
export type Icon = SFIcon | ImageIcon | TemplateIcon;
export type PlatformIconAndroid =
| {
type: 'drawableResource';
name: string;
}
| PlatformIconShared;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose merging imageSource and templateSource for better ergonomics:

type ImageSourceIcon = {
  type: 'imageSource',
  source: ImageSourcePropType,
  /**
   * Whether to use the icon as a template
   * The image will respect tint color.
   * Defaults to `true`.
   *
   * @platform ios
   */
  template?: boolean
}

Why:

  • We pass a ImageSourcePropType value, so calling it type: 'imageSource' feels more natural
  • The sfSymbol type doesn't include a sfSymbol property, but a name property. Similarly source property is shorter and nicer than duplicating the type for the property
  • On Android, images always respect the tint color. So having the same type that works on both in the same way is nicer. Otherwise, we'd need to do Platform.select({ android: { type: 'imageSource' ... }, ios: { type: 'templateSource' ... }) most of the time for no benefit
  • The additional iOS-specific template property still respects iOS nomenclature of calling it "template"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean here by "On Android, images always respect the tint color."?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides that I do not get fully that one part ☝🏻 I see your argumentation here & I think you make a good point, however I'm still sceptical, because on iOS, while you can specify any image for the template, it won't be handled nicely. If you just specify "regular" image it will get turned into colourful (tinted) rectangle & I like the separation from "regular image" here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to come up with something nice in few minutes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also you don't need to use Platform.select right now. It is enough to just specify icon for Android & icon for iOS. I'm gonna split the component in two tomorrow & approach with template?: boolean won't have any sense. I think we should stay with what we have now.

Copy link
Contributor

@satya164 satya164 Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean here by "On Android, images always respect the tint color."

I don't have full details on how the API works, for this was my experience (only tried with the old API in bottom tabs):

  • iOS: providing an image in imageSource didn't respect tabBarTintColor, templateSource did
  • Android: providing an image in iconResource respected tabBarItemIconColorActive, similar to templateSource on iOS

It is enough to just specify icon for Android & icon for iOS

Gotcha, but I was trying to demonstrate that in most cases when you pass an icon for tab bar, buttons etc., you're likely passing a monochrome png with transparency that should take on the tint color, and passing the same format one time is simpler instead of passing separate objects for Android & iOS that do the same thing in the end.

i.e. instead of icon: { shared: { type: 'imageSource', source: require('./my-icon.png') } }, I'll need icon: { android: { type: 'imageSource', source: require('./my-icon.png') }, ios: { type: 'templateSource', source: require('./my-icon.png') } }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but we then include template?: boolean, which is iOS specific, to shared icon object, which I do not like. I'd like to leave it in current form on react-native-screens side. We can surely wrap this and do otherwise on RN side.

@t0maboro t0maboro force-pushed the @t0maboro/unify-bottom-tabs-icon-apis branch from b923851 to c894a4e Compare October 16, 2025 13:01
@t0maboro t0maboro requested a review from kkafar October 16, 2025 13:43
Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few final remarks, beside that we're good

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@t0maboro t0maboro merged commit 3a588b1 into main Oct 17, 2025
8 checks passed
@t0maboro t0maboro deleted the @t0maboro/unify-bottom-tabs-icon-apis branch October 17, 2025 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants