@tatil/react-native-app-icon
v0.0.2
Published
Switch your React Native app icon at runtime (seasonal, campaign or user-picked icons) on iOS and Android. Never crashes, no dependencies.
Readme
@tatil/react-native-app-icon
Switch your React Native app icon at runtime: seasonal icons, campaign icons, or an icon the user picks. iOS and Android, no dependencies.
import AppIcon from '@tatil/react-native-app-icon';
await AppIcon.setIcon('christmas'); // 🎄
await AppIcon.setIcon('default'); // back to normalNamed imports work too: import { setIcon } from '@tatil/react-native-app-icon'.
Why another icon package?
Changing the icon should never be the reason your app crashes. This package is built around that:
- Never throws, never rejects. Every call resolves. Native errors are logged, not thrown.
- Unknown names are safe.
setIcon('does-not-exist'),setIcon(null)orsetIcon(undefined)switch to the default icon, so a server can send any value. - No pointless switches. Setting the icon that is already active does nothing, so iOS users don't see the "You have changed the icon" alert on every launch.
- Android-safe timing. Swapping the launcher entry while the app is on screen makes some launchers close the app. On Android the switch is applied when the app goes to the background.
- Never iconless on Android. The new icon is enabled before the old one is disabled.
- No hardcoded list. Available icons are read from the app itself:
Info.pliston iOS,AndroidManifest.xmlon Android. - No dependencies. A plain TurboModule: Objective-C++ on iOS, Kotlin on Android.
Requirements
| Dependency | Version |
| --- | --- |
| react-native | >= 0.76 with the New Architecture (developed against 0.86) |
| iOS | 15.1+ |
| Android | API 24+ |
Installation
yarn add @tatil/react-native-app-icon
cd ios && bundle exec pod installThen add your icons on each platform as shown below and rebuild the app. Autolinking registers the native module.
Setup
Pick a short, lowercase name for each extra icon, for example autumn and christmas. Use the same name on iOS, on Android and in JS. The name default is reserved for your current icon.
iOS
In Xcode, open your asset catalog (
Images.xcassets) and add a new iOS App Icon set per icon, named exactly after the icon:autumn,christmas.Drop a 1024×1024 PNG (no transparency) into each set.
In the target's Build Settings, set Alternate App Icon Sets (
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES) to the names, separated by spaces. Do it for both Debug and Release:ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = autumn christmasXcode then writes
CFBundleAlternateIconsinto the builtInfo.plist, which is where the package finds your icons.
[!NOTE] iOS always shows a system alert after the icon changes. Apple does not let apps hide it.
Android
Android switches icons by turning launcher entries (<activity-alias>) on and off.
- Add the icon resources, for example
res/mipmap-*/ic_launcher_autumn.png, or an adaptive icon inres/mipmap-anydpi-v26/ic_launcher_autumn.xml. - In
android/app/src/main/AndroidManifest.xml, remove theMAIN/LAUNCHERintent filter fromMainActivity. Keep its other intent filters, such as deep links. - Add one
<activity-alias>per icon, includingdefault. Each alias name must end with.AppIcon.<icon name>, and onlydefaultis enabled:
<activity android:name=".MainActivity" ...>
<!-- MAIN/LAUNCHER intent filter removed from here; deep links stay -->
</activity>
<activity-alias
android:name=".AppIcon.default"
android:targetActivity=".MainActivity"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".AppIcon.autumn"
android:targetActivity=".MainActivity"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_autumn"
android:roundIcon="@mipmap/ic_launcher_autumn"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!-- same for .AppIcon.christmas -->If there is no .AppIcon.default alias, the package does nothing on Android and logs a warning instead of guessing.
Usage
Icon controlled by your backend
The most common case: your backend decides which icon is live, for example through a ping or config endpoint.
import AppIcon from '@tatil/react-native-app-icon';
const response = await api.ping();
// "christmas" -> christmas icon; "default", unknown or missing -> default icon
AppIcon.setIcon(response.iconName);You don't need to check whether the icon exists or is already active, and you don't need a try/catch.
Let the user choose
import AppIcon from '@tatil/react-native-app-icon';
const icons = AppIcon.getIcons(); // ['default', 'autumn', 'christmas']
const current = await AppIcon.getIcon(); // 'default'
const onPick = async name => {
const active = await AppIcon.setIcon(name);
console.log(`icon is now ${active}`);
};API
All functions are available on the default export (AppIcon.setIcon) and as named exports (setIcon).
| Function | Returns | Description |
| --- | --- | --- |
| setIcon(name?) | Promise<string> | Switches to name. Anything that is not a bundled icon switches to default. Resolves with the icon that is (iOS) or will be (Android) shown. Never rejects. |
| getIcon() | Promise<string> | The icon currently on the home screen. On Android, a switch still waiting for the app to go to the background is returned as current. |
| getIcons() | string[] | Every icon the app ships, default first. |
| DEFAULT_ICON | 'default' | The reserved name of your primary icon. |
TypeScript types are included.
Platform notes
iOS
- The system alert after a change cannot be hidden.
- iOS refuses the change while the app is not in the foreground. The call then resolves with the icon that is still active, so you can try again on the next launch.
Android
- The change is applied when the app leaves the foreground. If the app is already in the background, it is applied immediately.
- Some launchers take a few seconds to show the new icon. On some launchers a pinned home screen shortcut is removed and the user has to add the app again. This is launcher behavior and affects every icon-switching library.
- The first release that moves the launcher entry from
MainActivityto.AppIcon.defaultchanges the launcher component. Before shipping, test the update path (old version installed, then the new one) on the launchers your users have, such as Pixel and Samsung One UI.
Troubleshooting
| Problem | Fix |
| --- | --- |
| getIcons() returns only ['default'] on iOS | ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES is missing or the asset set names don't match. Clean the build folder and rebuild. |
| getIcons() returns only ['default'] on Android | Alias names must end with .AppIcon.<name> and have the MAIN/LAUNCHER intent filter. |
| Two icons on the Android home screen | More than one alias has android:enabled="true". Only .AppIcon.default should. |
| react-native run-android builds but fails with Activity class {…} does not exist | The CLI looks for the LAUNCHER filter on an <activity> and doesn't see aliases. Run it with --main-activity .MainActivity (add it to your android script). |
| native module is not linked warning | Rebuild the native app after installing; run pod install on iOS. |
License
MIT
